mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
Merge pull request #1130 from bbassie/build/vite-plus
Draft: Replace webpack with Vite and load plugins as ES modules (vote)
This commit is contained in:
94 files changed
+1196
-1212
No files matched your search
@@ -31,17 +31,15 @@ runs:
|
||||
key: pnpm-store-${{ runner.os }}-${{ steps.store.outputs.week }}-${{ hashFiles('**/package.json', 'pnpm-workspace.yaml') }}
|
||||
restore-keys: pnpm-store-${{ runner.os }}-
|
||||
|
||||
# The prepare scripts run tsc --build and webpack, both incremental when
|
||||
# their previous output is present. Every run saves its output and the
|
||||
# next run starts from the most recent one.
|
||||
# The prepare scripts run tsc --build, which is incremental when its
|
||||
# previous output is present. Every run saves its output and the next run
|
||||
# starts from the most recent one.
|
||||
- if: inputs.ignore-scripts != 'true'
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
packages/*/dist
|
||||
plugins/*/dist
|
||||
packages/*/node_modules/.cache
|
||||
plugins/*/node_modules/.cache
|
||||
key: build-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: build-${{ runner.os }}-
|
||||
|
||||
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
// Formatting rules come from @stylistic as Oxlint does not implement them.
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"node"
|
||||
],
|
||||
"jsPlugins": [
|
||||
"@stylistic/eslint-plugin"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"node": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/dist/",
|
||||
"dist/"
|
||||
],
|
||||
"rules": {
|
||||
"@stylistic/indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{
|
||||
"ignoredNodes": [
|
||||
"JSXFragment"
|
||||
],
|
||||
"ignoreComments": true,
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"accessor-pairs": "error",
|
||||
"array-callback-return": "error",
|
||||
"arrow-body-style": "error",
|
||||
"block-scoped-var": "error",
|
||||
"complexity": [
|
||||
"error",
|
||||
{
|
||||
"max": 25
|
||||
}
|
||||
],
|
||||
"curly": "error",
|
||||
"default-case": "error",
|
||||
"default-case-last": "error",
|
||||
"eqeqeq": "error",
|
||||
"func-name-matching": "error",
|
||||
"func-style": [
|
||||
"error",
|
||||
"declaration",
|
||||
{
|
||||
"allowArrowFunctions": true
|
||||
}
|
||||
],
|
||||
"grouped-accessor-pairs": "error",
|
||||
"guard-for-in": "error",
|
||||
"id-match": "error",
|
||||
"max-depth": "error",
|
||||
"max-nested-callbacks": "error",
|
||||
"new-cap": [
|
||||
"error",
|
||||
{
|
||||
"capIsNewExceptions": [
|
||||
"StringEnum",
|
||||
"StringKey"
|
||||
],
|
||||
"capIsNewExceptionPattern": "^Type\\.",
|
||||
"newIsCapExceptions": [
|
||||
"constructor"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-alert": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-caller": "error",
|
||||
"no-console": "error",
|
||||
"no-constant-condition": [
|
||||
"error",
|
||||
{
|
||||
"checkLoops": false
|
||||
}
|
||||
],
|
||||
"no-constructor-return": "error",
|
||||
"no-div-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-eq-null": "error",
|
||||
"no-eval": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implicit-globals": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-loop-func": "error",
|
||||
"no-multi-assign": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-nested-ternary": "error",
|
||||
"no-new": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-plusplus": [
|
||||
"error",
|
||||
{
|
||||
"allowForLoopAfterthoughts": true
|
||||
}
|
||||
],
|
||||
"no-proto": "error",
|
||||
"no-restricted-globals": "error",
|
||||
"no-restricted-imports": "error",
|
||||
"no-restricted-properties": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-script-url": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-shadow": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"Event",
|
||||
"Request",
|
||||
"yargs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-var": "error",
|
||||
"no-void": [
|
||||
"error",
|
||||
{
|
||||
"allowAsStatement": true
|
||||
}
|
||||
],
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-exponentiation-operator": "error",
|
||||
"prefer-numeric-literals": "error",
|
||||
"prefer-object-spread": "error",
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"prefer-regex-literals": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "error",
|
||||
"radix": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"symbol-description": "error",
|
||||
"unicode-bom": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"vars-on-top": "error",
|
||||
"yoda": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"exceptRange": true
|
||||
}
|
||||
],
|
||||
"node/no-sync": [
|
||||
"error",
|
||||
{
|
||||
"ignores": [
|
||||
"TechnologySync",
|
||||
"techsToSync"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@stylistic/array-bracket-newline": "off",
|
||||
"@stylistic/array-bracket-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/array-element-newline": "off",
|
||||
"@stylistic/arrow-parens": "off",
|
||||
"@stylistic/arrow-spacing": [
|
||||
"error",
|
||||
{
|
||||
"after": true,
|
||||
"before": true
|
||||
}
|
||||
],
|
||||
"@stylistic/block-spacing": "error",
|
||||
"@stylistic/brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
{
|
||||
"allowSingleLine": true
|
||||
}
|
||||
],
|
||||
"@stylistic/comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "always-multiline",
|
||||
"exports": "always-multiline",
|
||||
"functions": "only-multiline",
|
||||
"generics": "only-multiline",
|
||||
"enums": "always-multiline",
|
||||
"tuples": "always-multiline"
|
||||
}
|
||||
],
|
||||
"@stylistic/comma-spacing": "error",
|
||||
"@stylistic/comma-style": [
|
||||
"error",
|
||||
"last"
|
||||
],
|
||||
"@stylistic/computed-property-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/dot-location": [
|
||||
"error",
|
||||
"property"
|
||||
],
|
||||
"@stylistic/eol-last": "error",
|
||||
"@stylistic/function-call-argument-newline": "off",
|
||||
"@stylistic/function-paren-newline": "off",
|
||||
"@stylistic/generator-star-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"@stylistic/implicit-arrow-linebreak": "error",
|
||||
"@stylistic/jsx-quotes": "error",
|
||||
"@stylistic/key-spacing": "error",
|
||||
"@stylistic/keyword-spacing": "error",
|
||||
"@stylistic/line-comment-position": "off",
|
||||
"@stylistic/linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"@stylistic/lines-around-comment": "off",
|
||||
"@stylistic/lines-between-class-members": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"exceptAfterSingleLine": true
|
||||
}
|
||||
],
|
||||
"@stylistic/max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 120
|
||||
}
|
||||
],
|
||||
"@stylistic/max-statements-per-line": "off",
|
||||
"@stylistic/multiline-comment-style": [
|
||||
"error",
|
||||
"separate-lines"
|
||||
],
|
||||
"@stylistic/multiline-ternary": "off",
|
||||
"@stylistic/new-parens": "error",
|
||||
"@stylistic/newline-per-chained-call": "off",
|
||||
"@stylistic/no-confusing-arrow": "error",
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-floating-decimal": "error",
|
||||
"@stylistic/no-mixed-operators": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
[
|
||||
"&",
|
||||
"|",
|
||||
"^",
|
||||
"~",
|
||||
"<<",
|
||||
">>",
|
||||
">>>"
|
||||
],
|
||||
[
|
||||
"==",
|
||||
"!=",
|
||||
"===",
|
||||
"!==",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<="
|
||||
],
|
||||
[
|
||||
"in",
|
||||
"instanceof"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"@stylistic/no-multi-spaces": "error",
|
||||
"@stylistic/no-multiple-empty-lines": "error",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/no-trailing-spaces": "error",
|
||||
"@stylistic/no-whitespace-before-property": "error",
|
||||
"@stylistic/nonblock-statement-body-position": "error",
|
||||
"@stylistic/object-curly-newline": "error",
|
||||
"@stylistic/object-curly-spacing": "off",
|
||||
"@stylistic/one-var-declaration-per-line": "error",
|
||||
"@stylistic/operator-linebreak": [
|
||||
"error",
|
||||
"after",
|
||||
{
|
||||
"overrides": {
|
||||
"?": "before",
|
||||
":": "before",
|
||||
"||": "before",
|
||||
"&&": "before",
|
||||
"??": "before",
|
||||
"|": "before",
|
||||
"&": "before"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/padding-line-between-statements": "error",
|
||||
"@stylistic/quote-props": "off",
|
||||
"@stylistic/quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"@stylistic/rest-spread-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/semi": "error",
|
||||
"@stylistic/semi-spacing": "error",
|
||||
"@stylistic/semi-style": "off",
|
||||
"@stylistic/space-before-blocks": "error",
|
||||
"@stylistic/space-before-function-paren": "off",
|
||||
"@stylistic/space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/space-infix-ops": "off",
|
||||
"@stylistic/space-unary-ops": "error",
|
||||
"@stylistic/spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"line": {
|
||||
"markers": [
|
||||
"/"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"@stylistic/switch-colon-spacing": "error",
|
||||
"@stylistic/template-curly-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"@stylistic/template-tag-spacing": "error",
|
||||
"@stylistic/wrap-iife": "error",
|
||||
"@stylistic/wrap-regex": "off",
|
||||
"@stylistic/yield-star-spacing": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"{packages,plugins}/**/*.{ts,tsx}"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/no-duplicate-enum-values": "error",
|
||||
"typescript/no-empty-object-type": "error",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-extra-non-null-assertion": "error",
|
||||
"typescript/no-misused-new": "error",
|
||||
"typescript/no-namespace": "error",
|
||||
"typescript/no-non-null-asserted-optional-chain": "error",
|
||||
"typescript/no-require-imports": "off",
|
||||
"typescript/no-this-alias": "error",
|
||||
"typescript/no-unnecessary-type-constraint": "error",
|
||||
"typescript/no-unsafe-declaration-merging": "error",
|
||||
"typescript/no-unsafe-function-type": "error",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/prefer-namespace-keyword": "error",
|
||||
"typescript/triple-slash-reference": "off",
|
||||
"no-shadow": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"Event",
|
||||
"Request",
|
||||
"yargs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-useless-constructor": "error"
|
||||
},
|
||||
"plugins": [
|
||||
"typescript"
|
||||
]
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"test/**/*.js",
|
||||
"plugins/*/test/**/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"prefer-arrow-callback": "off"
|
||||
},
|
||||
"env": {
|
||||
"mocha": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"packages/web_ui/src/**/*.{ts,tsx}",
|
||||
"packages/lib/browser.ts",
|
||||
"packages/create/templates/*/web/**/*.{ts,js,tsx,jsx}",
|
||||
"{packages,plugins}/*/web/**/*.{ts,tsx}"
|
||||
],
|
||||
"globals": {
|
||||
"SpeechRecognitionAlternative": "readonly",
|
||||
"SpeechRecognitionResult": "readonly",
|
||||
"SpeechRecognitionResultList": "readonly"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"packages/create/templates/**/*.{js,cjs,ts,cts,jsx,tsx}"
|
||||
],
|
||||
"rules": {
|
||||
"@stylistic/indent": "off",
|
||||
"@stylistic/spaced-comment": "off",
|
||||
"@stylistic/comma-spacing": "off",
|
||||
"@stylistic/max-len": "off"
|
||||
},
|
||||
"jsPlugins": [
|
||||
"@stylistic/eslint-plugin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,7 +18,7 @@ Use the detailed guides for the affected area:
|
||||
- Read the relevant source and tests before changing behavior.
|
||||
If documentation and implementation disagree, report the discrepancy and distinguish observed behavior from intended policy.
|
||||
Update affected documentation in the same change.
|
||||
- Follow nearby code and `eslint.config.mjs`: tabs, double quotes, semicolons, and `.js` extensions in relative TypeScript imports.
|
||||
- Follow nearby code and `.oxlintrc.json`: tabs, double quotes, semicolons, and `.js` extensions in relative TypeScript imports.
|
||||
Import shared public APIs through `@clusterio/lib` and check Node/browser exports when changing them.
|
||||
- Run commands from the root using the pinned pnpm version and the [documented Node requirements](docs/contributing.md#supported-nodejs-and-factorio-versions).
|
||||
Install with `pnpm install --no-frozen-lockfile`; the lockfile is intentionally ignored.
|
||||
|
||||
@@ -49,7 +49,7 @@ async function removeBuildArtifacts() {
|
||||
tryRemove(path.join(basePath, packageName, "dist"))
|
||||
);
|
||||
tasks.push(
|
||||
tryRemove(path.join(basePath, packageName, "node_modules", ".cache"))
|
||||
tryRemove(path.join(basePath, packageName, "node_modules", ".vite"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ Defaults to null meaning assume localhost.
|
||||
|
||||
Used to override where static assets such as the web bundle is loaded from.
|
||||
This can be used if you want to put the static assets on a separate cdn url like `https://cdn.example.com/static/` while keeping the API hosted directly by clusterio.
|
||||
Due to a limitation on how the webpack assets are built the pathname component of the url have to be `/static/`, values with any other path are rejected.
|
||||
Due to a limitation on how the web assets are built the pathname component of the url have to be `/static/`, values with any other path are rejected.
|
||||
|
||||
See also the `clusteriocontroller copy-static` command for how to obtain the static asset files that need to be hosted.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Clusterio manages distributed Factorio servers through a controller, hosts, and
|
||||
| `packages/host/` | Instance lifecycle, Factorio processes, save patching, and exports. Start with `src/Host.ts`, `src/Instance.ts`, `src/server.ts`, and `src/patch.ts`. |
|
||||
| `packages/ctl/` | Command-line administration and plugin commands. |
|
||||
| `packages/lib/` | Shared messages, routing, config, permissions, plugin loading, logging, and utilities for Node and browser consumers. |
|
||||
| `packages/web_ui/` | React/Ant Design interface and browser control connection; bundled through the controller's webpack build. |
|
||||
| `packages/web_ui/` | React/Ant Design interface and browser control connection; bundled through the controller's Vite build. |
|
||||
| `packages/create/` | Installer and JavaScript/TypeScript plugin templates. |
|
||||
| `plugins/` | Bundled plugins, including their Node, web, Lua, and test code. |
|
||||
| `external_plugins/` | Plugins developed in independent repositories and included in the local workspace. |
|
||||
@@ -119,13 +119,12 @@ To have TypeScript watch for changes and rebuild immediadly you may run `pnpm wa
|
||||
|
||||
For web development on the controller there are also the following flags:
|
||||
|
||||
`--dev` - Recompile the `web_ui` package on the fly providing live reloading of it.
|
||||
`--dev` - Serve the `web_ui` package from source through Vite's development server with hot module replacement.
|
||||
|
||||
`--dev-plugin [name]` - Recompile the web module for the given plugin on the fly proving live reloading of it.
|
||||
`--dev-plugin [name]` - Serve the web module for the given plugin from source the same way.
|
||||
|
||||
These options can be combined and multiple plugins can be actived for dev mode.
|
||||
Keep in mind if you're working on external plugins the `--dev-plugin` option is likely to only work if the plugin is placed inside the `external_plugins` directory and its dependencies are installed with pnpm as part of the clusterio repository workspace.
|
||||
Note that webpack is configured to delete the web interface build when using these options, so you will have to run `pnpm -r run prepare` before you start the controller without these flags again.
|
||||
|
||||
|
||||
### Build outputs
|
||||
@@ -138,7 +137,7 @@ Note that webpack is configured to delete the web interface build when using the
|
||||
| `pnpm build-mod` | Factorio mod archives in root `dist/`. |
|
||||
| `pnpm docs` | Generated API documentation. |
|
||||
|
||||
`pnpm build` alone does not regenerate the standalone validator or webpack bundles.
|
||||
`pnpm build` alone does not regenerate the standalone validator or web bundles.
|
||||
After changing `packages/lib/src/data/messages_core.ts`, run `pnpm -r run prepare` to compile the schema and regenerate its validator.
|
||||
|
||||
|
||||
@@ -228,7 +227,7 @@ Inspect `node clean.js --help` and preview the selected scope with `--dry-run` b
|
||||
|
||||
| Command | Removal scope |
|
||||
| --- | --- |
|
||||
| `node clean.js --dry-run` | Preview workspace package/plugin build output and webpack caches; omit `--dry-run` to clean, or use `pnpm clean`. |
|
||||
| `node clean.js --dry-run` | Preview workspace package/plugin build output and Vite dependency caches; omit `--dry-run` to clean, or use `pnpm clean`. |
|
||||
| `node clean.js tests --dry-run` | Also preview root `.nyc_output/`, `coverage/`, the whole `temp/` directory, and `static/`. |
|
||||
| `node clean.js all --dry-run` | Also preview runtime databases, instances, logs, mods, local configs, `FactorioAdminToken.txt`, and `plugin-list.json`. |
|
||||
|
||||
|
||||
+14
-12
@@ -18,7 +18,7 @@ If there is a particular issue or concern you would like the team to comment on
|
||||
- [Naming Times and Durations](#naming-times-and-durations)
|
||||
- [Strings](#strings)
|
||||
3. Code Style (TS / JS / Web)
|
||||
- [ESLint](#eslint)
|
||||
- [Oxlint](#oxlint)
|
||||
- [Imports Within `lib` Package](#imports-within-lib-package)
|
||||
- [Name Style](#name-style-ts)
|
||||
4. Code Style (Lua)
|
||||
@@ -38,7 +38,7 @@ If there is a particular issue or concern you would like the team to comment on
|
||||
- [React](#react)
|
||||
- [Separation of Host and Controller](#separation-of-host-and-controller)
|
||||
- [Typescript](#typescript)
|
||||
- [Webpack](#webpack)
|
||||
- [Vite](#vite)
|
||||
8. Supportability
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Distribution](#distribution)
|
||||
@@ -157,14 +157,15 @@ Therefore, we selected double quotes as our standard.
|
||||
|
||||
## Code Style (TS / JS / Web)
|
||||
|
||||
### ESLint
|
||||
### Oxlint
|
||||
|
||||
🧾 Time Immemorial
|
||||
🧾 20th October 2026
|
||||
|
||||
> Use ESLint for style checking.
|
||||
> Use Oxlint for style checking, with formatting rules from `@stylistic/eslint-plugin`.
|
||||
|
||||
We needed to maintain a consistent style across all our files.
|
||||
Therefore, we utilise the industry-standard ESLint, as it is familiar and highly configurable.
|
||||
We needed to maintain a consistent style across all our files and used ESLint for it from the start.
|
||||
Oxlint runs the same rules about three times faster, reads ESLint disable comments, and does not depend on the TypeScript compiler API.
|
||||
The formatting rules ESLint deprecated run through `@stylistic/eslint-plugin` loaded as an Oxlint JS plugin, so the code style did not change.
|
||||
|
||||
|
||||
### Imports Within `lib` Package
|
||||
@@ -347,14 +348,15 @@ Transitioning to any other langauge would be too significant an undertaking.
|
||||
Initially, we supported multiple languages for plugins, including Rust and Go; however, this approach was quickly abandoned due to its complexity.
|
||||
|
||||
|
||||
### Webpack
|
||||
### Vite
|
||||
|
||||
🧾 Time Immemorial
|
||||
🧾 20th October 2026
|
||||
|
||||
> Our web stack uses Webpack and associated plugins.
|
||||
> Our web stack uses Vite. Plugin web bundles are ES modules that import shared dependencies through an import map.
|
||||
|
||||
We required a bundler for our web build, and at the time, Webpack was the standard.
|
||||
While we have considered alternatives since, transitioning to another solution would be a significant undertaking.
|
||||
We used Webpack with Module Federation for loading plugins from the start, as it was the standard at the time.
|
||||
Vite builds the web interface several times faster, its development server updates modules without reloading the page, and plugin bundles no longer depend on one bundler's runtime.
|
||||
See [#1127](https://github.com/clusterio/clusterio/issues/1127) for the discussion.
|
||||
|
||||
|
||||
## Supportability
|
||||
|
||||
@@ -24,3 +24,38 @@ The config errors `lib.InvalidValue`, `lib.InvalidField` and `lib.InvalidAccess`
|
||||
|
||||
Errors of classes that are not registered, like a `TypeError` from a bug, are received as `InternalServerError`.
|
||||
Plugins that define their own error classes need to list them in `errors` of the plugin declaration.
|
||||
|
||||
|
||||
## Build the web module with Vite
|
||||
|
||||
The web interface is now built with Vite instead of webpack, and plugin web modules are loaded as ES modules instead of through Module Federation.
|
||||
A plugin built with webpack fails to load with the error "Incompatible dist/web/manifest.json, the plugin must be rebuilt with Vite".
|
||||
|
||||
Delete `webpack.config.cjs` and add a `vite.config.js` next to `package.json`:
|
||||
|
||||
```js
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
In `package.json` replace `webpack-cli --mode production` in the `prepare` script with `vite build`, and replace the `webpack`, `webpack-cli` and `webpack-merge` dev dependencies with `vite`.
|
||||
Add `vite.config.js` to `.npmignore` if you had `webpack.config.js` there.
|
||||
|
||||
`clusterioPlugin` exposes `index.ts` (or `index.js`) as `"./"`, `package.json` as `"./package.json"` and `web/index.tsx` (or `.ts`, `.jsx`, `.js`) as `"./web"`.
|
||||
These are the same keys the plugin template used with webpack, so `webEntrypoint: "./web"` stays as it is.
|
||||
If your webpack config exposed other paths, move the code so it is reachable from `web/index.tsx`.
|
||||
|
||||
The web interface provides `@clusterio/lib`, `@clusterio/web_ui`, `@sinclair/typebox`, `antd`, `react`, `react-dom`, `react-router` and `react-router-dom` to plugins.
|
||||
These are left as imports in the plugin bundle and resolved by the browser, there is no need to list them as shared.
|
||||
Everything else is bundled into the plugin, including `@ant-design/icons`.
|
||||
Import `react-dom/client` and `react/jsx-runtime` by those names, other subpaths of the shared packages are bundled as a separate copy.
|
||||
|
||||
Imports of `assert`, `buffer`, `events`, `os`, `path`, `process`, `stream`, `util` and `zlib` resolve to the same browser packages as before, provided by `@clusterio/web_ui`.
|
||||
|
||||
The `container` property of `WebPluginContext` is now the plugin's ES module namespace, `await container.get("./web")` returns the module instead of a factory function.
|
||||
|
||||
`lib.PluginWebpackEnvInfo` is renamed to `lib.PluginWebEnvInfo`.
|
||||
@@ -138,8 +138,8 @@ The following properties are recognized:
|
||||
Object mapping ctl configuration fields to `FieldDefinition` objects.
|
||||
|
||||
**webEntrypoint**:
|
||||
Optional Module Federation exposure key for the web entrypoint, such as `"./web"`.
|
||||
It must match the key in the plugin's webpack `exposes` configuration; it is not a Node.js file path.
|
||||
Optional key of the web entrypoint in the plugin's web bundle, this should be `"./web"`.
|
||||
The `clusterioPlugin` Vite plugin exposes `web/index.tsx` (or `.ts`, `.jsx`, `.js`) under this key; it is not a Node.js file path.
|
||||
|
||||
**migrationEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which exports the migration entrypoint function for this plugin.
|
||||
|
||||
@@ -1,504 +0,0 @@
|
||||
import {
|
||||
defineConfig,
|
||||
globalIgnores,
|
||||
} from "eslint/config";
|
||||
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import node from "eslint-plugin-node";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import js from "@eslint/js";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
|
||||
import {
|
||||
FlatCompat,
|
||||
} from "@eslint/eslintrc";
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: import.meta.dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default defineConfig([{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
"@stylistic": stylistic,
|
||||
node,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.commonjs,
|
||||
},
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"@stylistic/indent": ["error", "tab", {
|
||||
"ignoredNodes": [
|
||||
"JSXFragment", // TODO: Linter crashes if this is removed
|
||||
],
|
||||
"ignoreComments": true,
|
||||
"SwitchCase": 1, // Indent case statements one level from switch
|
||||
}],
|
||||
"accessor-pairs": "error",
|
||||
"array-bracket-newline": "off",
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"array-callback-return": "error",
|
||||
"array-element-newline": "off",
|
||||
"arrow-body-style": "error",
|
||||
"arrow-parens": "off",
|
||||
|
||||
"arrow-spacing": ["error", {
|
||||
"after": true,
|
||||
"before": true,
|
||||
}],
|
||||
|
||||
"block-scoped-var": "error",
|
||||
"block-spacing": "error",
|
||||
|
||||
"brace-style": ["error", "1tbs", {
|
||||
"allowSingleLine": true,
|
||||
}],
|
||||
|
||||
"camelcase": "off",
|
||||
"capitalized-comments": "off",
|
||||
"class-methods-use-this": "off",
|
||||
|
||||
"comma-dangle": ["error", {
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "always-multiline",
|
||||
"exports": "always-multiline",
|
||||
"functions": "only-multiline",
|
||||
}],
|
||||
|
||||
"comma-spacing": "error",
|
||||
"comma-style": ["error", "last"],
|
||||
"complexity": ["error", { "max": 25 }],
|
||||
"computed-property-spacing": ["error", "never"],
|
||||
"consistent-return": "error",
|
||||
"consistent-this": "error",
|
||||
"curly": "error",
|
||||
"default-case": "error",
|
||||
"default-case-last": "error",
|
||||
"default-param-last": "off",
|
||||
"dot-location": ["error", "property"],
|
||||
|
||||
"dot-notation": ["error", {
|
||||
"allowPattern": "^[a-z]+(_[a-z]+)*$",
|
||||
}],
|
||||
|
||||
"eol-last": "error",
|
||||
"eqeqeq": "error",
|
||||
"func-call-spacing": "error",
|
||||
"func-name-matching": "error",
|
||||
"func-names": "off",
|
||||
|
||||
"func-style": ["error", "declaration", {
|
||||
"allowArrowFunctions": true,
|
||||
}],
|
||||
|
||||
"function-call-argument-newline": "off",
|
||||
"function-paren-newline": "off",
|
||||
|
||||
"generator-star-spacing": ["error", {
|
||||
"before": false,
|
||||
"after": true,
|
||||
}],
|
||||
|
||||
"grouped-accessor-pairs": "error",
|
||||
"guard-for-in": "error",
|
||||
"id-blacklist": "error",
|
||||
"id-length": "off",
|
||||
"id-match": "error",
|
||||
"implicit-arrow-linebreak": "error",
|
||||
|
||||
"indent-legacy": "off",
|
||||
"init-declarations": "off",
|
||||
"jsx-quotes": "error",
|
||||
"key-spacing": "error",
|
||||
"keyword-spacing": "error",
|
||||
"line-comment-position": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": "off",
|
||||
"lines-around-directive": "off",
|
||||
|
||||
"lines-between-class-members": ["error", "always", {
|
||||
"exceptAfterSingleLine": true,
|
||||
}],
|
||||
|
||||
"max-classes-per-file": "off",
|
||||
"max-depth": "error",
|
||||
|
||||
"max-len": ["error", {
|
||||
"code": 120,
|
||||
}],
|
||||
|
||||
"max-lines": "off",
|
||||
"max-lines-per-function": "off",
|
||||
"max-nested-callbacks": "error",
|
||||
"max-params": "off",
|
||||
"max-statements": "off",
|
||||
"max-statements-per-line": "off",
|
||||
"multiline-comment-style": ["error", "separate-lines"],
|
||||
"multiline-ternary": "off",
|
||||
|
||||
"new-cap": ["error", {
|
||||
"capIsNewExceptions": ["StringEnum", "StringKey"],
|
||||
"capIsNewExceptionPattern": "^Type\\.",
|
||||
"newIsCapExceptions": ["constructor"],
|
||||
}],
|
||||
|
||||
"new-parens": "error",
|
||||
"newline-after-var": "off",
|
||||
"newline-before-return": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"no-alert": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-await-in-loop": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-buffer-constructor": "error",
|
||||
"no-caller": "error",
|
||||
"no-catch-shadow": "off",
|
||||
"no-confusing-arrow": "error",
|
||||
"no-console": "error",
|
||||
|
||||
"no-constant-condition": ["error", {
|
||||
"checkLoops": false,
|
||||
}],
|
||||
|
||||
"no-constructor-return": "error",
|
||||
"no-continue": "off",
|
||||
"no-div-regex": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-empty-function": "off",
|
||||
"no-eq-null": "error",
|
||||
"no-eval": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-extra-parens": "off",
|
||||
"no-floating-decimal": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implicit-globals": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-inline-comments": "off",
|
||||
"no-invalid-this": "error",
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-loop-func": "error",
|
||||
"no-magic-numbers": "off",
|
||||
|
||||
"no-mixed-operators": ["error", {
|
||||
"groups": [
|
||||
["&", "|", "^", "~", "<<", ">>", ">>>"],
|
||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
|
||||
["in", "instanceof"],
|
||||
],
|
||||
}],
|
||||
|
||||
"no-multi-assign": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-multiple-empty-lines": "error",
|
||||
"no-native-reassign": "error",
|
||||
"no-negated-condition": "off",
|
||||
"no-negated-in-lhs": "error",
|
||||
"no-nested-ternary": "error",
|
||||
"no-new": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-object": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-param-reassign": "off",
|
||||
|
||||
"no-plusplus": ["error", {
|
||||
"allowForLoopAfterthoughts": true,
|
||||
}],
|
||||
|
||||
"no-proto": "error",
|
||||
"no-restricted-exports": "error",
|
||||
"no-restricted-globals": "error",
|
||||
"no-restricted-imports": "error",
|
||||
"no-restricted-properties": "error",
|
||||
"no-restricted-syntax": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-return-await": "off",
|
||||
"no-script-url": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
|
||||
"no-shadow": ["error", {
|
||||
"allow": ["Event", "Request", "yargs"],
|
||||
}],
|
||||
|
||||
"no-spaced-func": "error",
|
||||
|
||||
"no-tabs": "off",
|
||||
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-ternary": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-use-before-define": "off",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-constructor": "off",
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-var": "error",
|
||||
|
||||
"no-void": ["error", {
|
||||
"allowAsStatement": true,
|
||||
}],
|
||||
|
||||
"no-warning-comments": "off",
|
||||
"no-whitespace-before-property": "error",
|
||||
"nonblock-statement-body-position": "error",
|
||||
"object-curly-newline": "error",
|
||||
"object-curly-spacing": "off",
|
||||
"object-shorthand": "off",
|
||||
"one-var": "off",
|
||||
"one-var-declaration-per-line": "error",
|
||||
"operator-assignment": "off",
|
||||
|
||||
"operator-linebreak": ["error", "after", {
|
||||
"overrides": {
|
||||
"?": "before",
|
||||
":": "before",
|
||||
"||": "before",
|
||||
"&&": "before",
|
||||
"??": "before",
|
||||
},
|
||||
}],
|
||||
|
||||
"padded-blocks": "off",
|
||||
"padding-line-between-statements": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-const": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"prefer-exponentiation-operator": "error",
|
||||
"prefer-named-capture-group": "off",
|
||||
"prefer-numeric-literals": "error",
|
||||
"prefer-object-spread": "error",
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"prefer-reflect": "off",
|
||||
"prefer-regex-literals": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"prefer-template": "error",
|
||||
"quote-props": "off",
|
||||
|
||||
"quotes": ["error", "double", {
|
||||
"avoidEscape": true,
|
||||
}],
|
||||
|
||||
"radix": ["error", "always"],
|
||||
"require-atomic-updates": "off",
|
||||
"require-await": "off",
|
||||
"require-jsdoc": "off",
|
||||
"require-unicode-regexp": "off",
|
||||
"rest-spread-spacing": ["error", "never"],
|
||||
"semi": "error",
|
||||
"semi-spacing": "error",
|
||||
"semi-style": "off",
|
||||
"sort-imports": "off",
|
||||
"sort-keys": "off",
|
||||
"sort-vars": "off",
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": "off",
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": "off",
|
||||
"space-unary-ops": "error",
|
||||
|
||||
"spaced-comment": ["error", "always", {
|
||||
"line": {
|
||||
"markers": ["/"],
|
||||
},
|
||||
}],
|
||||
|
||||
"switch-colon-spacing": "error",
|
||||
"symbol-description": "error",
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"template-tag-spacing": "error",
|
||||
"unicode-bom": ["error", "never"],
|
||||
|
||||
"vars-on-top": "error",
|
||||
"wrap-iife": "error",
|
||||
"wrap-regex": "off",
|
||||
"yield-star-spacing": "error",
|
||||
|
||||
"yoda": ["error", "never", {
|
||||
"exceptRange": true,
|
||||
}],
|
||||
|
||||
"node/callback-return": "off",
|
||||
"node/exports-style": "off",
|
||||
"node/file-extension-in-import": "off",
|
||||
"node/global-require": "off",
|
||||
"node/handle-callback-err": "off",
|
||||
"node/no-callback-literal": "off",
|
||||
"node/no-deprecated-api": "off",
|
||||
"node/no-exports-assign": "off",
|
||||
"node/no-extraneous-import": "off",
|
||||
"node/no-extraneous-require": "off",
|
||||
|
||||
"node/no-missing-import": "off",
|
||||
|
||||
"node/no-missing-require": "off",
|
||||
"node/no-mixed-requires": "off",
|
||||
"node/no-new-require": "off",
|
||||
"node/no-path-concat": "off",
|
||||
"node/no-process-env": "off",
|
||||
"node/no-process-exit": "off",
|
||||
"node/no-restricted-require": "off",
|
||||
"node/no-sync": "error",
|
||||
"node/no-unpublished-bin": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
|
||||
"node/no-unpublished-require": "off",
|
||||
|
||||
"node/no-unsupported-features/es-builtins": "off",
|
||||
"node/no-unsupported-features/es-syntax": "off",
|
||||
"node/no-unsupported-features/node-builtins": "off",
|
||||
"node/prefer-global/buffer": "off",
|
||||
"node/prefer-global/console": "off",
|
||||
"node/prefer-global/process": "off",
|
||||
"node/prefer-global/text-decoder": "off",
|
||||
"node/prefer-global/text-encoder": "off",
|
||||
"node/prefer-global/url-search-params": "off",
|
||||
"node/prefer-global/url": "off",
|
||||
"node/prefer-promises/dns": "off",
|
||||
"node/prefer-promises/fs": "off",
|
||||
"node/process-exit-as-throw": "off",
|
||||
|
||||
"node/shebang": "off",
|
||||
},
|
||||
}, {
|
||||
files: ["{packages,plugins}/**/*.ts?(x)"],
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
|
||||
extends: compat.extends("plugin:@typescript-eslint/recommended"),
|
||||
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/triple-slash-reference": "off",
|
||||
|
||||
"@typescript-eslint/no-shadow": ["error", {
|
||||
"allow": ["Event", "Request", "yargs"],
|
||||
}],
|
||||
|
||||
// Allow require imports where necessary (dynamic loading, webpack configs, etc.)
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
|
||||
"prefer-const": "off",
|
||||
"no-shadow": "off",
|
||||
},
|
||||
}, {
|
||||
files: ["test/**/*.js", "plugins/*/test/**/*.js"],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.mocha,
|
||||
},
|
||||
|
||||
sourceType: "module",
|
||||
parserOptions: {},
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"prefer-arrow-callback": "off",
|
||||
"no-invalid-this": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unpublished-require": "off",
|
||||
},
|
||||
}, {
|
||||
files: [
|
||||
"packages/web_ui/src/**/*.ts?(x)",
|
||||
"packages/lib/browser.ts",
|
||||
"packages/create/templates/*/web/**/*.{t,j}s?(x)",
|
||||
"{packages,plugins}/*/web/**/*.ts?(x)",
|
||||
],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
"sourceType": "module",
|
||||
|
||||
parserOptions: {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"node/callback-return": "off",
|
||||
"node/exports-style": "off",
|
||||
"node/file-extension-in-import": "off",
|
||||
"node/global-require": "off",
|
||||
"node/handle-callback-err": "off",
|
||||
"node/no-callback-literal": "off",
|
||||
"node/no-deprecated-api": "off",
|
||||
"node/no-exports-assign": "off",
|
||||
"node/no-extraneous-import": "off",
|
||||
"node/no-extraneous-require": "off",
|
||||
"node/no-missing-import": "off",
|
||||
"node/no-missing-require": "off",
|
||||
"node/no-mixed-requires": "off",
|
||||
"node/no-new-require": "off",
|
||||
"node/no-path-concat": "off",
|
||||
"node/no-process-env": "off",
|
||||
"node/no-process-exit": "off",
|
||||
"node/no-restricted-require": "off",
|
||||
"node/no-sync": "error",
|
||||
"node/no-unpublished-bin": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unpublished-require": "off",
|
||||
"node/no-unsupported-features/es-builtins": "off",
|
||||
"node/no-unsupported-features/es-syntax": "off",
|
||||
"node/no-unsupported-features/node-builtins": "off",
|
||||
"node/prefer-global/buffer": "off",
|
||||
"node/prefer-global/console": "off",
|
||||
"node/prefer-global/process": "off",
|
||||
"node/prefer-global/text-decoder": "off",
|
||||
"node/prefer-global/text-encoder": "off",
|
||||
"node/prefer-global/url-search-params": "off",
|
||||
"node/prefer-global/url": "off",
|
||||
"node/prefer-promises/dns": "off",
|
||||
"node/prefer-promises/fs": "off",
|
||||
"node/process-exit-as-throw": "off",
|
||||
"node/shebang": "off",
|
||||
},
|
||||
}, {
|
||||
files: ["packages/create/templates/**/*.?(c){t,j}s?(x)"],
|
||||
|
||||
"rules": {
|
||||
"node/no-missing-require": "off",
|
||||
"node/no-missing-import": "off",
|
||||
"@stylistic/indent": "off",
|
||||
"spaced-comment": "off",
|
||||
"comma-spacing": "off",
|
||||
"max-len": "off",
|
||||
"indent": "off",
|
||||
},
|
||||
}, globalIgnores(["**/dist/", "dist/"])]);
|
||||
+3
-10
@@ -15,8 +15,8 @@
|
||||
"cover": "c8 pnpm test",
|
||||
"fast-cover": "FAST_TEST=y c8 pnpm test",
|
||||
"ci-cover": "c8 --reporter=lcovonly pnpm test",
|
||||
"lint": "eslint packages plugins test",
|
||||
"lint-fix": "eslint --fix packages plugins test",
|
||||
"lint": "oxlint packages plugins test",
|
||||
"lint-fix": "oxlint --fix packages plugins test",
|
||||
"clean": "node ./clean.js",
|
||||
"docs": "typedoc",
|
||||
"bundle-dependencies": "bundle-dependencies"
|
||||
@@ -43,21 +43,14 @@
|
||||
"@clusterio/host": "workspace:^",
|
||||
"@clusterio/lib": "workspace:^",
|
||||
"@clusterio/web_ui": "workspace:^",
|
||||
"@eslint/eslintrc": "^3.3.7",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@swc/core": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"c8": "^12.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.71.2",
|
||||
"@typescript-eslint/parser": "^8.71.2",
|
||||
"eslint": "^10.13.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"express": "catalog:",
|
||||
"form-data": "^4.0.6",
|
||||
"globals": "^17.13.0",
|
||||
"jsonwebtoken": "catalog:",
|
||||
"mocha": "^11.8.0",
|
||||
"oxlint": "^1.88.0",
|
||||
"typedoc": "^0.28.20",
|
||||
"typescript": "catalog:",
|
||||
"yargs": "catalog:",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -25,7 +25,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -51,7 +51,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/web_ui": "workspace:^",
|
||||
"@swc/core": "catalog:",
|
||||
"@types/busboy": "^1.5.5",
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/express": "catalog:",
|
||||
@@ -61,13 +60,12 @@
|
||||
"@types/semver": "catalog:",
|
||||
"@types/ws": "catalog:",
|
||||
"@types/yargs": "catalog:",
|
||||
"antd": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-router": "catalog:",
|
||||
"react-router-dom": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-dev-middleware": "^8.3.0",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export class ControllerHooks extends lib.AsyncHookCollection {
|
||||
* @param curr - The current value of the field.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
readonly instanceConfigFieldChanged: lib.AsyncHook<[instance: InstanceRecord, field: string, curr: unknown, prev: unknown]>;
|
||||
|
||||
/**
|
||||
@@ -165,7 +165,7 @@ export class ControllerHooks extends lib.AsyncHookCollection {
|
||||
* The connection the event occured on.
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
readonly hostConnectionEvent: lib.AsyncHook<[connection: HostConnection, event: "connect" | "drop" | "resume" | "close"]>;
|
||||
|
||||
/**
|
||||
@@ -202,7 +202,7 @@ export class ControllerHooks extends lib.AsyncHookCollection {
|
||||
* The connection the event occured on.
|
||||
* @param event - one of connect, drop, resume, and close.
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
readonly controlConnectionEvent: lib.AsyncHook<[connection: ControlConnection, event: "connect" | "drop" | "resume" | "close"]>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,7 +52,7 @@ type ControllerDebugEvents = {
|
||||
|
||||
type ControllerEvents = {
|
||||
"stop": [],
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages all controller related operations
|
||||
@@ -106,7 +106,7 @@ export default class Controller {
|
||||
_registeredEvents = new Map<lib.EventClass<unknown>, lib.EventHandler<unknown>>();
|
||||
_snoopedEvents = new Map();
|
||||
|
||||
devMiddleware: any | null = null;
|
||||
devServer: import("vite").ViteDevServer | null = null;
|
||||
|
||||
startedAtMs = Date.now();
|
||||
autosaveInterval?: ReturnType<typeof setInterval>;
|
||||
@@ -378,7 +378,7 @@ export default class Controller {
|
||||
}, 600e3);
|
||||
logSizeGauge.callback = async () => { logSizeGauge.set(await this.logSize()); };
|
||||
|
||||
// Start webpack development server if enabled
|
||||
// Start Vite development server if enabled
|
||||
if (args.dev || args.devPlugin) {
|
||||
await this._startDevServer(args);
|
||||
}
|
||||
@@ -416,15 +416,21 @@ export default class Controller {
|
||||
}
|
||||
|
||||
Controller.addAppRoutes(this.app, this.pluginInfos);
|
||||
if (this.devServer) {
|
||||
this.app.use(this.devServer.middlewares);
|
||||
}
|
||||
|
||||
if (!args.dev) {
|
||||
let manifestPath = path.join(import.meta.dirname, "..", "..", "web", "manifest.json");
|
||||
|
||||
let manifest = await Controller.loadJsonObject(manifestPath);
|
||||
if (!manifest["main.js"]) {
|
||||
logger.error("Missing main.js entry in dist/web/manifest.json");
|
||||
if (!manifest.main) {
|
||||
logger.error("Missing main entry in dist/web/manifest.json");
|
||||
}
|
||||
this.app.locals.mainBundle = manifest["main.js"] || "no_web_build";
|
||||
this.app.locals.webBundle = {
|
||||
main: manifest.main || "no_web_build",
|
||||
imports: manifest.imports || {},
|
||||
};
|
||||
}
|
||||
this.app.locals.staticRoot = this.config.get("controller.static_url");
|
||||
|
||||
@@ -492,37 +498,44 @@ export default class Controller {
|
||||
}
|
||||
|
||||
async _startDevServer(args: ControllerArgs) {
|
||||
logger.warn("Webpack development mode enabled");
|
||||
logger.warn("Vite development mode enabled");
|
||||
|
||||
const webpack = (await import("webpack")).default;
|
||||
const webpackDevMiddleware = (await import("webpack-dev-middleware")).default;
|
||||
const webpackConfigs = [];
|
||||
const vite = await import("vite");
|
||||
const webUiVite = await import("@clusterio/web_ui/vite.mjs");
|
||||
|
||||
if (args.dev) {
|
||||
// @ts-expect-error this file is run from a different folder
|
||||
webpackConfigs.push((await import("../../../webpack.config.cjs")).default({})); // Path outside of build
|
||||
}
|
||||
if (args.devPlugin) {
|
||||
let devPlugins = new Map();
|
||||
for (let name of args.devPlugin) {
|
||||
let info = this.pluginInfos.find(i => i.name === name);
|
||||
if (!info) {
|
||||
throw new lib.StartupError(`No plugin named ${name}`);
|
||||
}
|
||||
const webpackConfig = path.posix.join(info.requirePath, "webpack.config.cjs");
|
||||
let config = (await import(
|
||||
path.isAbsolute(webpackConfig) ? pathToFileURL(webpackConfig).href : webpackConfig
|
||||
)).default({});
|
||||
devPlugins.set(name, webpackConfigs.length);
|
||||
webpackConfigs.push(config);
|
||||
const devPlugins = new Map<string, string>();
|
||||
const pluginDirs = new Map<string, string>();
|
||||
for (let name of (args.devPlugin ?? []).map(String)) {
|
||||
let info = this.pluginInfos.find(i => i.name === name);
|
||||
if (!info) {
|
||||
throw new lib.StartupError(`No plugin named ${name}`);
|
||||
}
|
||||
this.app.locals.devPlugins = devPlugins;
|
||||
pluginDirs.set(name, path.dirname(info.packagePath));
|
||||
devPlugins.set(name, webUiVite.devPluginUrl(name));
|
||||
}
|
||||
this.app.locals.devPlugins = devPlugins;
|
||||
|
||||
|
||||
const compiler = webpack(webpackConfigs);
|
||||
this.devMiddleware = webpackDevMiddleware(compiler, { serverSideRender: true });
|
||||
this.app.use(this.devMiddleware);
|
||||
const controllerDir = path.join(import.meta.dirname, "..", "..", "..");
|
||||
this.devServer = await vite.createServer({
|
||||
configFile: path.join(controllerDir, "vite.config.js"),
|
||||
root: controllerDir,
|
||||
base: "/static/",
|
||||
appType: "custom",
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
preTransformRequests: false,
|
||||
},
|
||||
plugins: [webUiVite.clusterioDevPlugins(pluginDirs)],
|
||||
});
|
||||
if (args.dev) {
|
||||
this.app.locals.webBundle = {
|
||||
main: this.devServer.config.plugins.find(p => p.name === "clusterio:app")!.api.entry,
|
||||
imports: Object.fromEntries(webUiVite.sharedModules.map(
|
||||
name => [name, webUiVite.devSharedUrl(name)]
|
||||
)),
|
||||
};
|
||||
}
|
||||
this.app.locals.devServer = this.devServer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,8 +586,8 @@ export default class Controller {
|
||||
await this.clusterLogIndex.save();
|
||||
}
|
||||
|
||||
if (this.devMiddleware) {
|
||||
await new Promise((resolve, reject) => { this.devMiddleware.close(resolve); });
|
||||
if (this.devServer) {
|
||||
await this.devServer.close();
|
||||
}
|
||||
|
||||
await this.hooks.shutdown.invoke();
|
||||
@@ -1357,26 +1370,29 @@ export default class Controller {
|
||||
let depth = routeDepth + Number(req.path.slice(-1) === "/");
|
||||
let webRoot = "../".repeat(depth) || "./";
|
||||
let staticRoot = res.app.locals.staticRoot ?? `${webRoot}static/`;
|
||||
let mainBundle: string = "";
|
||||
if (res.app.locals.mainBundle) {
|
||||
mainBundle = res.app.locals.mainBundle;
|
||||
} else {
|
||||
let stats = res.locals.webpack.devMiddleware.stats.stats[0];
|
||||
mainBundle = stats.toJson().assetsByChunkName["main"][0];
|
||||
}
|
||||
mainBundle = routes.stripStaticPrefix(mainBundle);
|
||||
const { main, imports } = res.app.locals.webBundle;
|
||||
const importMap = JSON.stringify({
|
||||
imports: Object.fromEntries(Object.entries(imports).map(
|
||||
([name, file]) => [name, `${staticRoot}${routes.stripStaticPrefix(file)}`]
|
||||
)),
|
||||
}).replace(/</g, "\\u003c");
|
||||
|
||||
fs.readFile(
|
||||
path.join(import.meta.dirname, "..", "..", "..", "web", "index.html"),
|
||||
"utf8",
|
||||
).then((content) => {
|
||||
res.type("text/html");
|
||||
res.send(content
|
||||
).then(async (content) => {
|
||||
content = content
|
||||
.replace(/__CLUSTER_NAME__/g, res.app.locals.controller.config.get("controller.name"))
|
||||
.replace(/__WEB_ROOT__/g, webRoot)
|
||||
.replace(/__STATIC_ROOT__/g, staticRoot)
|
||||
.replace(/__MAIN_BUNDLE__/g, mainBundle)
|
||||
);
|
||||
.replace(/__IMPORT_MAP__/g, () => importMap)
|
||||
.replace(/__MAIN_BUNDLE__/g, () => routes.stripStaticPrefix(main))
|
||||
;
|
||||
if (res.app.locals.devServer) {
|
||||
content = await res.app.locals.devServer.transformIndexHtml(req.originalUrl, content);
|
||||
}
|
||||
res.type("text/html");
|
||||
res.send(content);
|
||||
}).catch(err => {
|
||||
next(err);
|
||||
});
|
||||
|
||||
@@ -20,9 +20,10 @@ declare global {
|
||||
namespace Express {
|
||||
export interface Locals {
|
||||
controller: Controller,
|
||||
mainBundle: string,
|
||||
webBundle: { main: string, imports: Record<string, string> },
|
||||
devServer?: import("vite").ViteDevServer,
|
||||
staticRoot: string | null,
|
||||
devPlugins: Map<string, number>,
|
||||
devPlugins?: Map<string, string>,
|
||||
streams: Map<string, ProxyStream>,
|
||||
pendingMetrics?: Promise<string>,
|
||||
}
|
||||
@@ -124,7 +125,7 @@ async function getMetrics(req: Request, res: Response) {
|
||||
|
||||
|
||||
/**
|
||||
* Removes the static/ prefix webpack adds to bundle paths so they can be
|
||||
* Removes the static/ prefix from bundle paths in manifest.json so they can be
|
||||
* resolved relative to staticRoot.
|
||||
*/
|
||||
export function stripStaticPrefix(bundlePath: string) {
|
||||
@@ -140,22 +141,18 @@ function getPlugins(req: Request, res: Response) {
|
||||
// Note: Cast through unknown is needed because load_plugin is
|
||||
// defined at runtime and no other fields in the controller config
|
||||
// currently have the boolean type.
|
||||
let web: { main:any, error:string|undefined } = { main: undefined, error: undefined};
|
||||
let web: { main: any, error: string|undefined } = { main: undefined, error: undefined};
|
||||
let devPlugins = req.app.locals.devPlugins;
|
||||
if (devPlugins && devPlugins.has(name)) {
|
||||
let stats = res.locals.webpack.devMiddleware.stats.stats[devPlugins.get(name)!];
|
||||
web.main = stats.toJson().assetsByChunkName[name][0];
|
||||
web.main = devPlugins.get(name);
|
||||
} else if (pluginInfo.manifest) {
|
||||
web.main = pluginInfo.manifest[`${pluginInfo.name}.js`];
|
||||
web.main = pluginInfo.manifest.main;
|
||||
if (!web.main) {
|
||||
web.error = `Missing ${pluginInfo.name}.js entry in manifest.json`;
|
||||
web.error = "Incompatible dist/web/manifest.json, the plugin must be rebuilt with Vite";
|
||||
}
|
||||
} else if (lib.pluginNeedsWebBuild(pluginInfo)) {
|
||||
web.error = "Missing dist/web/manifest.json";
|
||||
}
|
||||
if (web.main === "remoteEntry.js") {
|
||||
web.error = "Incompatible old remoteEntry.js entrypoint.";
|
||||
}
|
||||
if (web.main) {
|
||||
web.main = stripStaticPrefix(web.main);
|
||||
}
|
||||
@@ -308,7 +305,7 @@ type ProxyStreamEvents = {
|
||||
"close": [],
|
||||
"timeout": [],
|
||||
"source": [],
|
||||
}
|
||||
};
|
||||
|
||||
export interface ProxyStream {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioApp } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioApp({ entry: "web/index.tsx" })],
|
||||
});
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2a2a2a" />
|
||||
<title>__CLUSTER_NAME__</title>
|
||||
<script type="importmap">__IMPORT_MAP__</script>
|
||||
<style>
|
||||
html {
|
||||
color: #fff;
|
||||
@@ -26,7 +27,6 @@
|
||||
<div id="root">
|
||||
<div id="bootstrap-msg">Loading web interface...</div>
|
||||
</div>
|
||||
<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).href;
|
||||
@@ -41,9 +41,9 @@
|
||||
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>
|
||||
<script id="script-bundle" type="module" src="__STATIC_ROOT____MAIN_BUNDLE__"
|
||||
onerror="scriptError('Main bundle failed to load.')"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-implicit-globals
|
||||
declare function scriptError(err: any): void;
|
||||
import("@clusterio/web_ui")
|
||||
.then(webUi => webUi.bootstrap())
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
devServer: {
|
||||
contentBase: "./dist/web",
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "controller",
|
||||
shared: {
|
||||
"@clusterio/lib": { singleton: true },
|
||||
"@clusterio/web_ui": { singleton: true },
|
||||
"antd": { singleton: true },
|
||||
"react": { singleton: true },
|
||||
"react-dom": { singleton: true },
|
||||
"react-router": { singleton: true },
|
||||
"react-router-dom": { singleton: true },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -88,7 +88,7 @@ export async function copyPluginTemplates(pluginName, templates) {
|
||||
const config = !templates.includes("no_config") && templates.some(value => (
|
||||
["controller", "host", "instance", "ctl"].includes(value)
|
||||
));
|
||||
const webpack = config || templates.includes("controller") || templates.includes("web");
|
||||
const webBuild = config || templates.includes("controller") || templates.includes("web");
|
||||
|
||||
// A count of the number of isolated contexts the plugin runs under
|
||||
let pluginContexts = 0;
|
||||
@@ -113,15 +113,15 @@ export async function copyPluginTemplates(pluginName, templates) {
|
||||
files.set("tsconfig.json", path.join(templatePath, "tsconfig.json"));
|
||||
files.set("tsconfig.node.json", path.join(templatePath, "tsconfig.node.json"));
|
||||
files.set("tsconfig.base.json", path.join(templatePath, "tsconfig.base.json"));
|
||||
if (webpack) {
|
||||
if (webBuild) {
|
||||
files.set("tsconfig.browser.json", path.join(templatePath, "tsconfig.browser.json"));
|
||||
}
|
||||
}
|
||||
|
||||
// Files and dependences to support webpack
|
||||
if (webpack) {
|
||||
prepare.push("webpack-cli --mode production");
|
||||
files.set("webpack.config.cjs", path.join(commonPath, "webpack.config.cjs"));
|
||||
// Files and dependences to support the web build
|
||||
if (webBuild) {
|
||||
prepare.push("vite build");
|
||||
files.set("vite.config.js", path.join(commonPath, "vite.config.js"));
|
||||
if (templates.includes("web")) {
|
||||
files.set(`web/index.${ext}x`, path.join(templatePath, `web/plugin.${ext}x`));
|
||||
pluginContexts += 1;
|
||||
@@ -185,7 +185,7 @@ export async function copyPluginTemplates(pluginName, templates) {
|
||||
ctl: templates.includes("ctl"),
|
||||
web: templates.includes("web"),
|
||||
// Macro flags for context requirements
|
||||
webpack: webpack,
|
||||
web_build: webBuild,
|
||||
config: config,
|
||||
// String values for package json
|
||||
clusterio_version: _package.version,
|
||||
|
||||
@@ -67,15 +67,13 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
//%endif
|
||||
//%if webpack
|
||||
"webpack": "^5.110.3",
|
||||
"webpack-cli": "^7.2.3",
|
||||
"webpack-merge": "^5.9.0",
|
||||
//%if web_build
|
||||
"vite": "^8.3.4",
|
||||
//%endif
|
||||
//%if controller
|
||||
"@clusterio/controller": "^__clusterio_version__",
|
||||
//%endif
|
||||
//%if webpack
|
||||
//%if web_build
|
||||
"@clusterio/web_ui": "^__clusterio_version__",
|
||||
//%endif
|
||||
//%if host
|
||||
@@ -87,7 +85,7 @@
|
||||
"@clusterio/lib": "^__clusterio_version__"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.30.6"
|
||||
"@sinclair/typebox": "^0.34.52"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
dist/web
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.__ext__x",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "__plugin_name__",
|
||||
library: { type: "window", name: "plugin___plugin_name__" },
|
||||
exposes: {
|
||||
"./": "./index.__ext__",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.__ext__x",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
//%if web
|
||||
"antd": { import: false },
|
||||
"react": { import: false },
|
||||
"react-dom": { import: false },
|
||||
//%endif
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import * as lib from "@clusterio/lib";
|
||||
|
||||
export async function getEditor(argsEditor: string) {
|
||||
// eslint-disable-next-line
|
||||
return argsEditor || process.env.EDITOR || process.env.VISUAL || undefined
|
||||
return argsEditor || process.env.EDITOR || process.env.VISUAL || undefined;
|
||||
// needed for the process.env statements to not be flagged by eslint
|
||||
// priority for editors is CLI argument > env.EDITOR > env.VISUAL
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "./src/browser_polyfills";
|
||||
export * from "./src/api";
|
||||
export * from "./src/AsyncHook";
|
||||
export * from "./src/SyncHook";
|
||||
|
||||
Vendored
+1
-1
@@ -6,5 +6,5 @@ export interface BuildArgs {
|
||||
build?: boolean;
|
||||
outputDir: string;
|
||||
sourceDir: string;
|
||||
};
|
||||
}
|
||||
export function build(args: BuildArgs): Promise<void>;
|
||||
@@ -264,7 +264,9 @@ export class LockFile {
|
||||
// Event handler for synchronous process exit, readLock can't be used as it is async
|
||||
private handleExitSync = (): void => {
|
||||
try {
|
||||
// eslint-disable-next-line node/no-sync
|
||||
if (readFileSync(this.filePath, "utf8") === LockFile.content) {
|
||||
// eslint-disable-next-line node/no-sync
|
||||
unlinkSync(this.filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -14,4 +14,4 @@ export type PluginWebApi = {
|
||||
* package.
|
||||
*/
|
||||
npmPackage?: string;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// Node.js globals used by lib and its dependencies when running in a browser.
|
||||
import process from "process";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
// Vite's development server may already have created process.env
|
||||
if (typeof globalThis.process?.nextTick !== "function") {
|
||||
globalThis.process = Object.assign(process, globalThis.process);
|
||||
}
|
||||
globalThis.Buffer ??= Buffer as any;
|
||||
@@ -24,7 +24,7 @@ type OldConfigGroupSchema = Static<typeof OldConfigGroupSchema>;
|
||||
const OldConfigSchema = Type.Object({
|
||||
"groups": Type.Array(OldConfigGroupSchema),
|
||||
});
|
||||
export type OldConfigSchema = Static<typeof OldConfigSchema>
|
||||
export type OldConfigSchema = Static<typeof OldConfigSchema>;
|
||||
|
||||
export const ConfigSchema = Type.Record(Type.String(), Type.Union([
|
||||
Type.Null(),
|
||||
@@ -38,11 +38,11 @@ export type ConfigSchema = Static<typeof ConfigSchema>;
|
||||
type FieldValue = null | boolean | string | number | object;
|
||||
|
||||
type FieldType<T extends FieldValue> =
|
||||
T extends boolean ? "boolean" :
|
||||
T extends string ? "string" :
|
||||
T extends number ? "number" :
|
||||
T extends object ? "object" :
|
||||
never;
|
||||
T extends boolean ? "boolean"
|
||||
: T extends string ? "string"
|
||||
: T extends number ? "number"
|
||||
: T extends object ? "object"
|
||||
: never;
|
||||
|
||||
export interface FieldDefinition<
|
||||
Type extends FieldValue = any,
|
||||
@@ -134,7 +134,7 @@ export type ConfigDefs<
|
||||
Fields extends Record<keyof Fields, FieldValue>
|
||||
> = {
|
||||
[Prop in keyof Fields]: FieldDefinition<Fields[Prop], Fields>;
|
||||
}
|
||||
};
|
||||
|
||||
function defaultValue(def: FieldDefinition) {
|
||||
if (def.initialValue === undefined) {
|
||||
@@ -148,7 +148,7 @@ function defaultValue(def: FieldDefinition) {
|
||||
|
||||
export type ConfigEvents = {
|
||||
fieldChanged: [ name: string, curr: FieldValue, prev: FieldValue ],
|
||||
}
|
||||
};
|
||||
|
||||
export enum ConfigAccess {
|
||||
read = 0x1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Core definitions for the configuration system
|
||||
import * as classes from "./classes.js";
|
||||
import * as validators from "./validators.js";
|
||||
import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin.js";
|
||||
import type { PluginNodeEnvInfo, PluginWebEnvInfo } from "../plugin.js";
|
||||
import { Static } from "@sinclair/typebox";
|
||||
import { isTargetVersion } from "../data/version.js";
|
||||
import { SyncHook } from "../SyncHook.js";
|
||||
@@ -151,7 +151,7 @@ export class ControllerConfig extends classes.Config<ControllerConfigFields> {
|
||||
optional: true,
|
||||
validator: (value) => {
|
||||
if (value) {
|
||||
// This is unfortunately a limitiation of how the webpack build is set up right now.
|
||||
// This is unfortunately a limitiation of how the web build is set up right now.
|
||||
const pathname = new URL(value, "http://localhost/").pathname;
|
||||
if (pathname !== "/static/") {
|
||||
throw new Error(`Path component of static_url must be /static/ not ${pathname}.`);
|
||||
@@ -786,7 +786,7 @@ function validateFieldNames(
|
||||
* Add the config fields defined by the by a specific plugin infos
|
||||
*/
|
||||
export function addPluginFieldDefinitions(
|
||||
pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo,
|
||||
pluginInfo: PluginNodeEnvInfo | PluginWebEnvInfo,
|
||||
kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields",
|
||||
Config: typeof classes.Config<any>
|
||||
) {
|
||||
@@ -858,7 +858,7 @@ export function addPluginFieldDefinitions(
|
||||
*
|
||||
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
|
||||
*/
|
||||
export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) {
|
||||
export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebEnvInfo[]) {
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
addPluginFieldDefinitions(pluginInfo, "controllerConfigFields", ControllerConfig);
|
||||
if (pluginInfo.hostEntrypoint || pluginInfo.instanceEntrypoint) {
|
||||
|
||||
@@ -15,8 +15,8 @@ export abstract class DatastoreProvider<
|
||||
K extends DatastoreKey,
|
||||
V extends DatastoreValue,
|
||||
> {
|
||||
abstract save(data: Map<K, V>): Promise<void>
|
||||
abstract load(): Promise<Map<K, V>>
|
||||
abstract save(data: Map<K, V>): Promise<void>;
|
||||
abstract load(): Promise<Map<K, V>>;
|
||||
}
|
||||
|
||||
// Store all data in memory, used as a default and should be avoided
|
||||
|
||||
@@ -35,7 +35,7 @@ export type FactorioModSettingType = "startup" | "runtime-global" | "runtime-per
|
||||
* Boolean mod setting prototype.
|
||||
* @see [bool-setting](https://wiki.factorio.com/Tutorial:Mod_settings#bool-setting)
|
||||
*/
|
||||
export interface FactorioBoolSettingPrototype extends FactorioPrototypeBase{
|
||||
export interface FactorioBoolSettingPrototype extends FactorioPrototypeBase {
|
||||
type: "bool-setting",
|
||||
setting_type?: FactorioModSettingType,
|
||||
default_value: boolean,
|
||||
@@ -46,7 +46,7 @@ export interface FactorioBoolSettingPrototype extends FactorioPrototypeBase{
|
||||
* Integer mod setting prototype.
|
||||
* @see [int-setting](https://wiki.factorio.com/Tutorial:Mod_settings#int-setting)
|
||||
*/
|
||||
export interface FactorioIntSettingPrototype extends FactorioPrototypeBase{
|
||||
export interface FactorioIntSettingPrototype extends FactorioPrototypeBase {
|
||||
type: "int-setting",
|
||||
setting_type?: FactorioModSettingType,
|
||||
default_value: number,
|
||||
@@ -59,7 +59,7 @@ export interface FactorioIntSettingPrototype extends FactorioPrototypeBase{
|
||||
* Real number mod setting prototype.
|
||||
* @see [double-setting](https://wiki.factorio.com/Tutorial:Mod_settings#double-setting)
|
||||
*/
|
||||
export interface FactorioDoubleSettingPrototype extends FactorioPrototypeBase{
|
||||
export interface FactorioDoubleSettingPrototype extends FactorioPrototypeBase {
|
||||
type: "double-setting",
|
||||
setting_type?: FactorioModSettingType,
|
||||
default_value: number,
|
||||
@@ -72,7 +72,7 @@ export interface FactorioDoubleSettingPrototype extends FactorioPrototypeBase{
|
||||
* String mod setting prototype.
|
||||
* @see [string-setting](https://wiki.factorio.com/Tutorial:Mod_settings#string-setting)
|
||||
*/
|
||||
export interface FactorioStringSettingPrototype extends FactorioPrototypeBase{
|
||||
export interface FactorioStringSettingPrototype extends FactorioPrototypeBase {
|
||||
type: "string-setting",
|
||||
setting_type?: FactorioModSettingType,
|
||||
default_value: string,
|
||||
@@ -85,7 +85,7 @@ export interface FactorioStringSettingPrototype extends FactorioPrototypeBase{
|
||||
* Color mod setting prototype.
|
||||
* @see [color-setting](https://wiki.factorio.com/Tutorial:Mod_settings#color-setting)
|
||||
*/
|
||||
export interface FactorioColorSettingPrototype extends FactorioPrototypeBase{
|
||||
export interface FactorioColorSettingPrototype extends FactorioPrototypeBase {
|
||||
type: "color-setting",
|
||||
setting_type?: FactorioModSettingType,
|
||||
default_value: FactorioColor,
|
||||
@@ -104,7 +104,7 @@ export type FactorioModSettingPrototype =
|
||||
* A list of prototypes as it appears in Factorio's Lua API.
|
||||
* @see [Data.raw](https://lua-api.factorio.com/stable/types/Data.html#raw)
|
||||
*/
|
||||
export type FactorioPrototypes<Prototype = FactorioPrototypeBase> = Record<string, Record<string, Prototype>>
|
||||
export type FactorioPrototypes<Prototype = FactorioPrototypeBase> = Record<string, Record<string, Prototype>>;
|
||||
export type FactorioDefinesValue = number | string | { [index: string]: FactorioDefinesValue };
|
||||
export type FactorioDefines = {
|
||||
[index: string]: FactorioDefinesValue,
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ export const ExternalFactorioVersionSchema = Type.Object({
|
||||
headlessUrl: Type.String(),
|
||||
});
|
||||
|
||||
export type ExternalFactorioVersion = Static<typeof ExternalFactorioVersionSchema>
|
||||
export type ExternalFactorioVersion = Static<typeof ExternalFactorioVersionSchema>;
|
||||
|
||||
/**
|
||||
* Fetch all factorio versions that support headless
|
||||
@@ -47,7 +47,7 @@ function parseFactorioVersions(html: string): ExternalFactorioVersion[] {
|
||||
const versions = new Map<string, ExternalFactorioVersion>();
|
||||
|
||||
// Match <a class=" slot-button-inline version-button-stable " href="/download/archive/2.0.72">2.0.72</a>
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
const anchorRegex = /<a[^>]*class="[^"]*version-button-(?<kind>stable|experimental)[^"]*"[^>]*href="[^"]*\/download\/archive\/(?<version>\d+\.\d+.\d+)"[^>]*>/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export const LatestReleaseBuildsSchema = Type.Record(Type.String(), FullVersionS
|
||||
*/
|
||||
export const LatestReleasesSchema = Type.Record(Type.String(), LatestReleaseBuildsSchema);
|
||||
|
||||
export type LatestReleases = Static<typeof LatestReleasesSchema>
|
||||
export type LatestReleases = Static<typeof LatestReleasesSchema>;
|
||||
|
||||
/**
|
||||
* Fetch the latest stable and experimental factorio releases
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as libSchema from "../schema.js";
|
||||
import { dataClasses, errorClasses } from "./messages.js";
|
||||
import { BaseConnector, WebSocketBaseConnector } from "./connectors.js";
|
||||
import { strict as assert } from "assert";
|
||||
import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin.js";
|
||||
import type { PluginNodeEnvInfo, PluginWebEnvInfo } from "../plugin.js";
|
||||
import type {
|
||||
AddressType, JSONDeserialisable, MessageRoutable, MessageRequest, MessageEvent, IUser,
|
||||
} from "../data/index.js";
|
||||
@@ -895,7 +895,7 @@ for (let Class of errorClasses) {
|
||||
Link.registerError(Class);
|
||||
}
|
||||
|
||||
export function registerPluginMessages(pluginInfos: PluginNodeEnvInfo[]|PluginWebpackEnvInfo[]) {
|
||||
export function registerPluginMessages(pluginInfos: PluginNodeEnvInfo[]|PluginWebEnvInfo[]) {
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
for (let Class of pluginInfo.messages || []) {
|
||||
if (Class.plugin !== pluginInfo.name) {
|
||||
|
||||
@@ -177,9 +177,9 @@ export const logger = winston.createLogger({
|
||||
// Who in their right mind thought that log levels should be hard coded
|
||||
// into the type when you can define custom levels you want to support?
|
||||
export type Logger = Omit<winston.Logger,
|
||||
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "verbose" | "input" | "silly" |
|
||||
"emerg" | "alert" | "crit" | "warning" | "notice" |
|
||||
"child"
|
||||
"error" | "warn" | "help" | "data" | "info" | "debug" | "prompt" | "verbose" | "input" | "silly"
|
||||
| "emerg" | "alert" | "crit" | "warning" | "notice"
|
||||
| "child"
|
||||
> & {
|
||||
fatal: winston.LeveledLogMethod,
|
||||
error: winston.LeveledLogMethod,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @author Hornwitser
|
||||
* @module lib/users
|
||||
*/
|
||||
import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "./plugin.js";
|
||||
import type { PluginNodeEnvInfo, PluginWebEnvInfo } from "./plugin.js";
|
||||
import { Permission } from "./data/index.js";
|
||||
|
||||
export const permissions = new Map<string, Permission>();
|
||||
@@ -88,7 +88,7 @@ export function definePermission({
|
||||
*
|
||||
* @param pluginInfos - Array of plugin info objects.
|
||||
*/
|
||||
export function registerPluginPermissions(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) {
|
||||
export function registerPluginPermissions(pluginInfos: PluginNodeEnvInfo[] | PluginWebEnvInfo[]) {
|
||||
for (const pluginInfo of pluginInfos) {
|
||||
for (const permission of pluginInfo.permissions ?? []) {
|
||||
if (typeof permission.name !== "string" || !permission.name.startsWith(`${pluginInfo.name}.`)) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export type PluginDeclaration = {
|
||||
errors?: ErrorClass[];
|
||||
permissions?: PermissionDefinition[];
|
||||
routes?: string[];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a plugin is expected to ship a web build.
|
||||
@@ -91,7 +91,7 @@ export type PluginNodeEnvInfo = PluginDeclaration & {
|
||||
skippedNpmPackage?: string;
|
||||
};
|
||||
|
||||
export type PluginWebpackEnvInfo = PluginDeclaration & {
|
||||
export type PluginWebEnvInfo = PluginDeclaration & {
|
||||
container?: any;
|
||||
package?: any;
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -175,7 +175,7 @@ export async function handlePluginCommand(
|
||||
try {
|
||||
const pluginPackage = (await import(
|
||||
pathToFileURL(path.posix.join(pluginPath, "package.json")).href,
|
||||
{ with: { type: "json" }},
|
||||
{ with: { type: "json" }}
|
||||
)).default;
|
||||
pluginInfo = (await import(
|
||||
pathToFileURL(path.posix.join(pluginPath, pluginPackage.main ?? "index.js")).href
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SessionLost, ValidationError } from "./errors.js";
|
||||
import { logger } from "./logging.js";
|
||||
|
||||
export type SubscriptionRequestHandler<T> = RequestHandler<SubscriptionRequest, Event<T> | null>;
|
||||
export type EventSubscriberCallback<T> = (event: T | null, synced: boolean) => void
|
||||
export type EventSubscriberCallback<T> = (event: T | null, synced: boolean) => void;
|
||||
|
||||
/**
|
||||
* Represents a set of string subscription filters.
|
||||
@@ -241,7 +241,7 @@ type Subscriber = {
|
||||
link: Link,
|
||||
address: Address,
|
||||
filters: SubscriptionFilters,
|
||||
}
|
||||
};
|
||||
|
||||
type EventData = {
|
||||
subscriptionUpdate?: SubscriptionRequestHandler<unknown>,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"./src/config/*.ts",
|
||||
"./src/data/*.ts",
|
||||
"./src/external/*.ts",
|
||||
"./src/browser_polyfills.ts",
|
||||
"./src/check_singleton_import.ts",
|
||||
"./src/datastore.ts",
|
||||
"./src/errors.ts",
|
||||
|
||||
Vendored
-3
@@ -22,6 +22,3 @@ declare module "*.md" {
|
||||
declare const webRoot: string;
|
||||
/** Path static assets are served under, ends with a slash. */
|
||||
declare const staticRoot: string;
|
||||
|
||||
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
|
||||
declare const __webpack_share_scopes__: { default: any };
|
||||
@@ -33,19 +33,15 @@
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@ant-design/icons": "catalog:",
|
||||
"@clusterio/lib": "workspace:^",
|
||||
"@sinclair/typebox": "catalog:",
|
||||
"@swc/core": "catalog:",
|
||||
"@vitejs/plugin-react": "catalog:",
|
||||
"antd": "catalog:",
|
||||
"assert": "^2.1.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^7.1.5",
|
||||
"events": "^3.3.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"null-loader": "^4.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
@@ -53,17 +49,13 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-markdown": "catalog:",
|
||||
"react-router": "catalog:",
|
||||
"react-router-dom": "catalog:",
|
||||
"semver": "catalog:",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
"typescript": "catalog:",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-manifest-plugin": "^6.0.1"
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
|
||||
@@ -144,7 +144,7 @@ export type WebPluginContext = lib.PluginLoadContext<{
|
||||
control: Control;
|
||||
container: any;
|
||||
package: any;
|
||||
}, lib.PluginWebpackEnvInfo>;
|
||||
}, lib.PluginWebEnvInfo>;
|
||||
|
||||
/**
|
||||
* Collection of web plugin hooks
|
||||
@@ -258,7 +258,7 @@ export class BaseWebPlugin {
|
||||
constructor(
|
||||
public container: any,
|
||||
packageData: any,
|
||||
public info: lib.PluginWebpackEnvInfo,
|
||||
public info: lib.PluginWebEnvInfo,
|
||||
public control: Control,
|
||||
public logger: lib.Logger,
|
||||
) {
|
||||
|
||||
@@ -16,22 +16,7 @@ import { pages } from "./pages";
|
||||
|
||||
const { ConsoleTransport, WebConsoleFormat, logger } = lib;
|
||||
|
||||
async function loadScript(url: string) {
|
||||
let script = document.createElement("script");
|
||||
script.src = url;
|
||||
script.type = "text/javascript";
|
||||
script.async = true;
|
||||
|
||||
let result = new Promise((resolve, reject) => {
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
});
|
||||
|
||||
document.head.appendChild(script);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]> {
|
||||
async function loadPluginInfos(): Promise<[lib.PluginWebEnvInfo[], string]> {
|
||||
let response = await fetch(`${webRoot}api/plugins`);
|
||||
let pluginList: lib.PluginWebApi[];
|
||||
if (response.ok) {
|
||||
@@ -43,8 +28,7 @@ async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]>
|
||||
}
|
||||
let pluginSetKey = loadedPluginSetKey(pluginList);
|
||||
|
||||
let pluginInfos: lib.PluginWebpackEnvInfo[] = [];
|
||||
await __webpack_init_sharing__("default");
|
||||
let pluginInfos: lib.PluginWebEnvInfo[] = [];
|
||||
for (let meta of pluginList) {
|
||||
if (meta.web.error) {
|
||||
logger.error(`Failed to load plugin ${meta.name}: ${meta.web.error}`);
|
||||
@@ -54,15 +38,13 @@ async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]>
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await loadScript(`${staticRoot}${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}`);
|
||||
let container = await import(/* @vite-ignore */ `${staticRoot}${meta.web.main}`);
|
||||
if (typeof container.get !== "function") {
|
||||
throw new Error("Plugin web bundle does not export get");
|
||||
}
|
||||
await container.init(__webpack_share_scopes__.default);
|
||||
let pluginInfo = (await container.get("./"))().plugin;
|
||||
let pluginInfo = (await container.get("./")).plugin;
|
||||
pluginInfo.container = container;
|
||||
pluginInfo.package = (await container.get("./package.json"))();
|
||||
pluginInfo.package = (await container.get("./package.json")).default;
|
||||
pluginInfo.enabled = meta.enabled;
|
||||
pluginInfos.push(pluginInfo);
|
||||
|
||||
@@ -83,7 +65,7 @@ async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]>
|
||||
}
|
||||
|
||||
async function loadPlugins(
|
||||
pluginInfos: lib.PluginWebpackEnvInfo[],
|
||||
pluginInfos: lib.PluginWebEnvInfo[],
|
||||
control: Control
|
||||
) {
|
||||
const plugins = new Map<string, WebPlugin.BaseWebPlugin>();
|
||||
@@ -94,8 +76,7 @@ async function loadPlugins(
|
||||
}
|
||||
|
||||
try {
|
||||
const moduleFactory = await pluginInfo.container.get(pluginInfo.webEntrypoint);
|
||||
const webModule = moduleFactory();
|
||||
const webModule = await pluginInfo.container.get(pluginInfo.webEntrypoint);
|
||||
|
||||
const pluginContext: WebPlugin.WebPluginContext = {
|
||||
control,
|
||||
|
||||
@@ -157,7 +157,7 @@ npm init "@clusterio" -- --controller-token ${token} --mode "host" --download-he
|
||||
</>;
|
||||
}
|
||||
|
||||
function CopyButton({ text, message }: { text:string, message:string }) {
|
||||
function CopyButton({ text, message }: { text: string, message: string }) {
|
||||
let [clipboardPermision, setClipboardPermission] = useState<PermissionState>("granted");
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -264,7 +264,7 @@ type DownloadDependenciesProps = {
|
||||
mods: lib.ModRecord[];
|
||||
onChange: (change: ModChange) => void;
|
||||
builtInModNames: string[];
|
||||
}
|
||||
};
|
||||
function DownloadDependenciesButton(props: DownloadDependenciesProps) {
|
||||
const control = useContext(ControlContext);
|
||||
const [modInfos] = useMods();
|
||||
@@ -904,7 +904,7 @@ type SettingsTableFieldProps = {
|
||||
allowedValues: any;
|
||||
locale: Map<string, any>;
|
||||
changed: boolean;
|
||||
}
|
||||
};
|
||||
const SettingsTableField = memo((props: SettingsTableFieldProps) => {
|
||||
const name = props.name;
|
||||
const isBoolean = props.type === "bool-setting";
|
||||
|
||||
@@ -47,8 +47,8 @@ function PluginMachines(props: PluginMachinesProps) {
|
||||
|
||||
type ControllerPluginProps = {
|
||||
pluginMeta: PluginWebApi,
|
||||
pluginInfo?: lib.PluginWebpackEnvInfo,
|
||||
loadedPlugin?: lib.PluginWebpackEnvInfo,
|
||||
pluginInfo?: lib.PluginWebEnvInfo,
|
||||
loadedPlugin?: lib.PluginWebEnvInfo,
|
||||
title: string,
|
||||
};
|
||||
|
||||
@@ -71,8 +71,8 @@ function ControllerPlugin(props: ControllerPluginProps) {
|
||||
}}
|
||||
message={pluginError}
|
||||
description={
|
||||
"The web interface was unable to load the webpack module for this plugin. This is "+
|
||||
"usually due to an incorrect or missing webpack build for the plugin. Due to the "+
|
||||
"The web interface was unable to load the web module for this plugin. This is "+
|
||||
"usually due to an incorrect or missing web build for the plugin. Due to the "+
|
||||
"module not being loaded, the configs and web controls defined by this plugin may "+
|
||||
"not be available in the web interface."
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Space, Table, Typography } from "antd";
|
||||
import CloseCircleFilled from "@ant-design/icons/CloseCircleFilled";
|
||||
import InfoCircleFilled from "@ant-design/icons/InfoCircleFilled";
|
||||
|
||||
import type { PluginWebApi, PluginWebpackEnvInfo } from "@clusterio/lib";
|
||||
import type { PluginWebApi, PluginWebEnvInfo } from "@clusterio/lib";
|
||||
|
||||
import notify from "../util/notify";
|
||||
import { useAccount } from "../model/account";
|
||||
@@ -28,7 +28,7 @@ type PluginRow = {
|
||||
title: string;
|
||||
/** Not present for plugins the controller hasn't loaded */
|
||||
meta?: PluginWebApi;
|
||||
info?: PluginWebpackEnvInfo;
|
||||
info?: PluginWebEnvInfo;
|
||||
package?: any;
|
||||
status?: PluginStatus;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ type ModalProps = {
|
||||
disabled: boolean;
|
||||
save: lib.SaveDetails;
|
||||
instanceId: number;
|
||||
}
|
||||
};
|
||||
|
||||
function RenameModal(props: ModalProps) {
|
||||
let control = useContext(ControlContext);
|
||||
@@ -198,7 +198,7 @@ type File = {
|
||||
name: string;
|
||||
percent: number;
|
||||
status: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
let account = useAccount();
|
||||
|
||||
@@ -65,7 +65,7 @@ type UserBulkActionProps = {
|
||||
setApplyAction(func: () => Promise<void>): void,
|
||||
form: FormInstance,
|
||||
restore?: boolean,
|
||||
}
|
||||
};
|
||||
|
||||
function UserBulkActionImport({ setApplyAction, form, restore }: UserBulkActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
|
||||
@@ -54,7 +54,7 @@ export class Control extends lib.Link {
|
||||
public hooks = new WebPlugin.WebHooks(logger);
|
||||
/** @deprecated Only holds plugins using the class export, use loadedPlugins and hooks instead */
|
||||
public plugins = new Map<string, WebPlugin.BaseWebPlugin>();
|
||||
public loadedPlugins = new Map<string, lib.PluginWebpackEnvInfo>();
|
||||
public loadedPlugins = new Map<string, lib.PluginWebEnvInfo>();
|
||||
public inputComponents = new Map<string, WebPlugin.InputComponent>();
|
||||
public extensionComponents: Partial<WebPlugin.ExtensionComponents> = {};
|
||||
public loginForms: WebPlugin.PluginLoginForm[] = [];
|
||||
@@ -85,7 +85,7 @@ export class Control extends lib.Link {
|
||||
|
||||
constructor(
|
||||
connector: ControlConnector,
|
||||
public pluginInfos = new Map<string, lib.PluginWebpackEnvInfo>(),
|
||||
public pluginInfos = new Map<string, lib.PluginWebEnvInfo>(),
|
||||
/** Key of the plugin set this interface was loaded against */
|
||||
public pluginSetKey = "",
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Plugin, PluginOption } from "vite";
|
||||
|
||||
export const sharedModules: string[];
|
||||
export function devUrl(id: string): string;
|
||||
export function devSharedUrl(name: string): string;
|
||||
export function devPluginUrl(name: string): string;
|
||||
export function clusterioApp(options: { entry: string }): PluginOption[];
|
||||
export function clusterioDevPlugins(pluginDirs: Map<string, string>): Plugin;
|
||||
export function clusterioPlugin(options?: { root?: string }): PluginOption[];
|
||||
@@ -0,0 +1,387 @@
|
||||
// Vite configuration shared by the controller web interface and plugins.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import util from "node:util";
|
||||
import { createRequire } from "node:module";
|
||||
import { execFile } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { normalizePath } from "vite";
|
||||
|
||||
/**
|
||||
* Modules the web interface provides to plugins at runtime. Plugins must not
|
||||
* bundle their own copy of these, they are imported through the import map.
|
||||
*/
|
||||
export const sharedModules = [
|
||||
"@clusterio/lib",
|
||||
"@clusterio/web_ui",
|
||||
"@sinclair/typebox",
|
||||
"antd",
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"react-router",
|
||||
"react-router-dom",
|
||||
];
|
||||
|
||||
/** Our own packages, these are ESM and differ between Node.js and the browser. */
|
||||
const esmSharedModules = new Set(["@clusterio/lib", "@clusterio/web_ui"]);
|
||||
|
||||
// Only the console transport of winston works in the browser, and the colour
|
||||
// library used by it does not.
|
||||
const stubbedModules = /node_modules\/(winston\/dist\/winston\/transports\/(http|file)\.js|@colors\/colors\/)/;
|
||||
// Browser versions of Node.js modules, resolved from web_ui so lib and plugins
|
||||
// don't need them as dependencies.
|
||||
const polyfills = {
|
||||
assert: "assert/",
|
||||
buffer: "buffer/",
|
||||
events: "events/",
|
||||
os: "os-browserify/browser",
|
||||
path: "path-browserify",
|
||||
process: "process/browser.js",
|
||||
stream: "stream-browserify",
|
||||
util: "util/",
|
||||
zlib: "browserify-zlib",
|
||||
};
|
||||
const webUiPackage = fileURLToPath(new URL("./package.json", import.meta.url));
|
||||
const libPackage = createRequire(webUiPackage).resolve("@clusterio/lib/package.json");
|
||||
const sharedPrefix = "\0clusterio:shared/";
|
||||
const pluginPrefix = "\0clusterio:plugin/";
|
||||
|
||||
/** URL of a virtual module on the Vite development server relative to its base. */
|
||||
export function devUrl(id) {
|
||||
return `@id/${id.replace("\0", "__x00__")}`;
|
||||
}
|
||||
|
||||
export function devSharedUrl(name) {
|
||||
return devUrl(sharedPrefix + name);
|
||||
}
|
||||
export function devPluginUrl(name) {
|
||||
return devUrl(pluginPrefix + name);
|
||||
}
|
||||
|
||||
async function getCommitHash() {
|
||||
try {
|
||||
const { stdout } = await util.promisify(execFile)("git", ["rev-parse", "--short", "HEAD"]);
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async function findFile(dir, candidates) {
|
||||
for (const file of candidates) {
|
||||
try {
|
||||
await fs.access(path.join(dir, file));
|
||||
return file;
|
||||
} catch {
|
||||
// Try the next one
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// node_modules directories Node.js would search for packages imported from dir.
|
||||
function nodeModulesDirs(dir) {
|
||||
const dirs = [];
|
||||
for (let current = dir; ; current = path.dirname(current)) {
|
||||
if (path.basename(current) !== "node_modules") {
|
||||
dirs.push(path.join(current, "node_modules"));
|
||||
}
|
||||
if (current === path.dirname(current)) {
|
||||
return dirs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function chunkName(name) {
|
||||
return name.replace(/^@/, "").replace(/[/]/g, "_");
|
||||
}
|
||||
|
||||
// Lists the named exports of a CommonJS package so they can be re-exported
|
||||
// statically, export * from a CommonJS module only forwards default.
|
||||
function exportNames(name, root) {
|
||||
if (esmSharedModules.has(name)) {
|
||||
return null;
|
||||
}
|
||||
const mod = createRequire(path.join(root, "package.json"))(name);
|
||||
const names = Object.keys(mod).filter(key => key !== "__esModule" && /^[A-Za-z_$][\w$]*$/.test(key));
|
||||
const isModule = Object.prototype.toString.call(mod) === "[object Module]";
|
||||
if (!isModule && !mod.__esModule && !names.includes("default")) {
|
||||
names.push("default");
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function sharedModuleCode(name, root) {
|
||||
const names = exportNames(name, root);
|
||||
if (!names) {
|
||||
return `export * from ${JSON.stringify(name)};\n`;
|
||||
}
|
||||
return `export { ${names.join(", ")} } from ${JSON.stringify(name)};\n`;
|
||||
}
|
||||
|
||||
async function pluginEntryCode(pluginDir) {
|
||||
const resolve = file => JSON.stringify(normalizePath(path.join(pluginDir, file)));
|
||||
const exposes = {
|
||||
"./": await findFile(pluginDir, ["index.ts", "index.js"]),
|
||||
"./package.json": "package.json",
|
||||
"./web": await findFile(pluginDir, ["web/index.tsx", "web/index.ts", "web/index.jsx", "web/index.js"]),
|
||||
};
|
||||
if (!exposes["./"]) {
|
||||
throw new Error(`No index.ts or index.js found in ${pluginDir}`);
|
||||
}
|
||||
const entries = Object.entries(exposes)
|
||||
.filter(([, file]) => file)
|
||||
.map(([key, file]) => `\t${JSON.stringify(key)}: () => import(${resolve(file)}),`);
|
||||
return [
|
||||
"const modules = {",
|
||||
...entries,
|
||||
"};",
|
||||
"export async function get(key) {",
|
||||
"\tif (!Object.hasOwn(modules, key)) {",
|
||||
"\t\tthrow new Error(\"Module \" + key + \" is not exposed by this plugin\");",
|
||||
"\t}",
|
||||
"\treturn await modules[key]();",
|
||||
"}",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// Replaces modules that do not work in the browser with an empty module.
|
||||
// Also passed to the dependency optimizer as it does not run Vite plugins.
|
||||
const stubs = {
|
||||
name: "clusterio:stubs",
|
||||
load(id) {
|
||||
if (stubbedModules.test(normalizePath(id))) {
|
||||
return "module.exports = {};\n";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const polyfillResolver = {
|
||||
name: "clusterio:polyfills",
|
||||
resolveId(id, _importer, options) {
|
||||
const target = polyfills[id.replace(/^node:/, "")];
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
return this.resolve(target, webUiPackage, { ...options, skipSelf: true });
|
||||
},
|
||||
};
|
||||
|
||||
/** Base config needed to build code that imports @clusterio/lib for the browser. */
|
||||
function browserCompat() {
|
||||
return {
|
||||
name: "clusterio:browser-compat",
|
||||
enforce: "pre",
|
||||
async config(_config, { command }) {
|
||||
const buildEnv = process.env.NODE_ENV ?? (command === "build" ? "production" : "dev");
|
||||
return {
|
||||
base: command === "build" ? "./" : undefined,
|
||||
define: {
|
||||
"global": "globalThis",
|
||||
"process.env.APP_ENV": JSON.stringify("browser"),
|
||||
"process.env.COMMIT_HASH": JSON.stringify(await getCommitHash()),
|
||||
"process.env.BUILD_DATE": JSON.stringify(new Date().toISOString()),
|
||||
"process.env.BUILD_ENV": JSON.stringify(buildEnv),
|
||||
"process.env.BUILD_OS": JSON.stringify(os.platform()),
|
||||
},
|
||||
optimizeDeps: {
|
||||
rolldownOptions: { plugins: [polyfillResolver, stubs] },
|
||||
},
|
||||
resolve: {
|
||||
alias: [{ find: /^node:(.*)$/, replacement: "$1" }],
|
||||
},
|
||||
build: {
|
||||
target: "es2022",
|
||||
sourcemap: true,
|
||||
assetsDir: "static",
|
||||
chunkSizeWarningLimit: 2048,
|
||||
rolldownOptions: {
|
||||
// Message and permission registration relies on class names.
|
||||
output: {
|
||||
minify: {
|
||||
compress: { keepNames: { class: true, function: false } },
|
||||
mangle: { keepNames: { class: true, function: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
resolveId: polyfillResolver.resolveId,
|
||||
load: stubs.load,
|
||||
transform(code, id) {
|
||||
if (id.endsWith(".md")) {
|
||||
return { code: `export default ${JSON.stringify(code)};\n`, map: null };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function writeManifest(manifest) {
|
||||
return {
|
||||
type: "asset",
|
||||
fileName: "manifest.json",
|
||||
source: `${JSON.stringify(manifest, null, "\t")}\n`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugins for building the web interface served by the controller.
|
||||
*
|
||||
* Builds `entry` together with one entry per shared module and writes
|
||||
* manifest.json listing the main bundle and the import map for plugins.
|
||||
*/
|
||||
export function clusterioApp({ entry }) {
|
||||
let root;
|
||||
return [
|
||||
browserCompat(),
|
||||
react(),
|
||||
{
|
||||
name: "clusterio:app",
|
||||
api: { entry },
|
||||
config(config) {
|
||||
root = normalizePath(path.resolve(config.root ?? process.cwd()));
|
||||
return {
|
||||
optimizeDeps: {
|
||||
entries: [entry],
|
||||
include: sharedModules.filter(name => !esmSharedModules.has(name)),
|
||||
},
|
||||
build: {
|
||||
outDir: "dist/web",
|
||||
emptyOutDir: true,
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
main: path.posix.join(root, entry),
|
||||
...Object.fromEntries(sharedModules.map(name => [
|
||||
`shared-${chunkName(name)}`, sharedPrefix + name,
|
||||
])),
|
||||
},
|
||||
preserveEntrySignatures: "strict",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id.startsWith(sharedPrefix)) {
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
load(id) {
|
||||
if (id.startsWith(sharedPrefix)) {
|
||||
return sharedModuleCode(id.slice(sharedPrefix.length), root);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
generateBundle(_options, bundle) {
|
||||
const entries = new Map();
|
||||
for (const chunk of Object.values(bundle)) {
|
||||
if (chunk.type === "chunk" && chunk.isEntry) {
|
||||
entries.set(chunk.facadeModuleId, chunk.fileName);
|
||||
}
|
||||
}
|
||||
this.emitFile(writeManifest({
|
||||
main: entries.get(path.posix.join(root, entry)),
|
||||
imports: Object.fromEntries(sharedModules.map(
|
||||
name => [name, entries.get(sharedPrefix + name)]
|
||||
)),
|
||||
}));
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin serving plugins from source on the controller's development
|
||||
* server, given as a map of plugin name to plugin directory.
|
||||
*/
|
||||
export function clusterioDevPlugins(pluginDirs) {
|
||||
return {
|
||||
name: "clusterio:dev-plugins",
|
||||
config(config) {
|
||||
const root = path.resolve(config.root ?? process.cwd());
|
||||
const sources = [root, path.dirname(webUiPackage), path.dirname(libPackage), ...pluginDirs.values()];
|
||||
return {
|
||||
server: {
|
||||
// Vite's default is the workspace root, which may be the data directory.
|
||||
fs: { allow: [...sources, ...sources.flatMap(nodeModulesDirs)] },
|
||||
},
|
||||
optimizeDeps: {
|
||||
entries: [...pluginDirs.values()].flatMap(
|
||||
dir => [`${dir}/index.{ts,js}`, `${dir}/web/**/*.{ts,tsx,js,jsx}`]
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id.startsWith(pluginPrefix)) {
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
load(id) {
|
||||
if (id.startsWith(pluginPrefix)) {
|
||||
const dir = pluginDirs.get(id.slice(pluginPrefix.length));
|
||||
return dir ? pluginEntryCode(dir) : null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugins for building the web bundle of a Clusterio plugin.
|
||||
*
|
||||
* The bundle exposes index, package.json and web/index from the plugin
|
||||
* directory. Modules in sharedModules are left as bare imports and resolved
|
||||
* by the web interface's import map at runtime.
|
||||
*/
|
||||
export function clusterioPlugin({ root = process.cwd() } = {}) {
|
||||
const entryId = "\0clusterio:plugin-entry";
|
||||
return [
|
||||
browserCompat(),
|
||||
react(),
|
||||
{
|
||||
name: "clusterio:plugin",
|
||||
async config() {
|
||||
const pkg = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
||||
const name = pkg.name.replace(/^@[^/]+\/(plugin-)?/, "");
|
||||
return {
|
||||
build: {
|
||||
outDir: "dist/web",
|
||||
emptyOutDir: true,
|
||||
rolldownOptions: {
|
||||
input: { [name]: entryId },
|
||||
external: sharedModules,
|
||||
preserveEntrySignatures: "strict",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === entryId) {
|
||||
return id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
load(id) {
|
||||
if (id === entryId) {
|
||||
return pluginEntryCode(root);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
generateBundle(_options, bundle) {
|
||||
const chunk = Object.values(bundle).find(c => c.type === "chunk" && c.facadeModuleId === entryId);
|
||||
this.emitFile(writeManifest({ main: chunk.fileName }));
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
"use strict";
|
||||
const os = require("os");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const webpack = require("webpack");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
function getCommitHash() {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (env = {}, argv = {}) => ({
|
||||
mode: argv.mode ?? "development",
|
||||
devtool: argv.mode === "production" ? "source-map" : "eval-source-map",
|
||||
cache: { type: "filesystem" },
|
||||
performance: {
|
||||
maxAssetSize: 2**21,
|
||||
maxEntrypointSize: 2**21,
|
||||
},
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
assetModuleFilename: "static/[hash][ext][query]",
|
||||
filename: "static/[name].[contenthash].js",
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new WebpackManifestPlugin({ publicPath: "" }),
|
||||
|
||||
// required for winston
|
||||
new webpack.ProvidePlugin({
|
||||
process: require.resolve("process/browser.js"),
|
||||
Buffer: [require.resolve("buffer/"), "Buffer"],
|
||||
}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.APP_ENV": JSON.stringify("browser"),
|
||||
"process.env.COMMIT_HASH": JSON.stringify(getCommitHash()),
|
||||
"process.env.BUILD_DATE": JSON.stringify(new Date().toISOString()),
|
||||
"process.env.BUILD_ENV": JSON.stringify(process.env.NODE_ENV ?? "dev"),
|
||||
"process.env.BUILD_OS": JSON.stringify(os.platform()),
|
||||
}),
|
||||
|
||||
// Make sure ant-design icons use the ES variant
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/@ant-design\/icons\/[A-Z]/,
|
||||
resource => {
|
||||
resource.request = resource.request.replace("@ant-design\/icons", "$&/es/icons");
|
||||
}
|
||||
),
|
||||
// Strip node: prefix in request paths.
|
||||
new webpack.NormalModuleReplacementPlugin(/^node:/, resource => {
|
||||
resource.request = resource.request.replace(/^node:/, "");
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /node_modules.winston.dist.winston.transports.(http|file)/,
|
||||
use: require.resolve("null-loader"),
|
||||
},
|
||||
// Colour library used by winston which doesn't work in browser.
|
||||
{
|
||||
test: /node_modules.colors/,
|
||||
use: require.resolve("null-loader"),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
require.resolve("style-loader"),
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
sourceMap: argv.mode !== "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "typescript",
|
||||
tsx: true,
|
||||
},
|
||||
target: "es2022",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
sourceMaps: true,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "ecmascript",
|
||||
jsx: true,
|
||||
},
|
||||
target: "es2022",
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.png$/,
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.md$/,
|
||||
type: "asset/source",
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".json", ".jsx", ".tsx"],
|
||||
extensionAlias: {
|
||||
".js": [".ts", ".js"],
|
||||
},
|
||||
fallback: {
|
||||
"crypto": false,
|
||||
"fs": false,
|
||||
"fs/promises": false,
|
||||
|
||||
// Required for winston
|
||||
"util": require.resolve("util/"),
|
||||
"os": require.resolve("os-browserify/browser"),
|
||||
"buffer": require.resolve("buffer/"),
|
||||
|
||||
// Required for zlib
|
||||
"assert": require.resolve("assert/"),
|
||||
"stream": require.resolve("stream-browserify"),
|
||||
|
||||
"events": require.resolve("events/"),
|
||||
"path": require.resolve("path-browserify"),
|
||||
"zlib": require.resolve("browserify-zlib"),
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
moduleIds: "deterministic",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
keep_classnames: true,
|
||||
},
|
||||
mangle: {
|
||||
keep_classnames: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -26,7 +26,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -57,8 +57,6 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "global_chat",
|
||||
library: { type: "var", name: "plugin_global_chat" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -9,7 +9,7 @@ type IpcPlayerAuth = {
|
||||
type: "set_verify_code",
|
||||
player: string,
|
||||
verify_code: string,
|
||||
}
|
||||
};
|
||||
|
||||
export default async function loadInstancePlugin(context: InstancePluginContext) {
|
||||
const { instance, host, logger, plugin } = context;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -64,8 +64,6 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "player_auth",
|
||||
library: { type: "var", name: "plugin_player_auth" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.tsx",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
"antd": { import: false },
|
||||
"react": { import: false },
|
||||
"react-dom": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -10,12 +10,12 @@ import { type Component, enabledComponents, stripPlayerData } from "./components
|
||||
|
||||
type IpcPlayerName = {
|
||||
player_name: string
|
||||
}
|
||||
};
|
||||
|
||||
type IpcDownloadRequest = {
|
||||
player_name: string,
|
||||
recipe_notifications?: string,
|
||||
}
|
||||
};
|
||||
|
||||
/** Components as an escaped JSON object for the scenario. */
|
||||
function componentsJson(enabled: Set<Component>) {
|
||||
@@ -30,7 +30,7 @@ type IpcAcquireResponse = {
|
||||
message?: string,
|
||||
epoch?: number,
|
||||
token?: string,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits string into array of strings with max of a certain length
|
||||
@@ -64,7 +64,7 @@ export type RecipeNotificationDelta = {
|
||||
remove?: string[],
|
||||
/** add is the whole stored list, sent without the instance's: the save works out what to remove */
|
||||
full?: true,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the changes needed to turn the current cleared recipe list into the stored one
|
||||
|
||||
@@ -30,7 +30,7 @@ export type IpcPlayerData = {
|
||||
// Components the upload was serialized with, all when missing
|
||||
components?: Record<string, boolean> | unknown[],
|
||||
destination_instance_id?: number, // set_destination's instance, on the leave upload only; not player data
|
||||
}
|
||||
};
|
||||
|
||||
// Marks tombstones in player_sync.json
|
||||
const jsonReserved = { deleted: Type.Optional(Type.Never()) };
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -51,7 +51,7 @@
|
||||
"@sinclair/typebox": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@ant-design/icons": "catalog:",
|
||||
"@clusterio/controller": "workspace:^",
|
||||
"@clusterio/ctl": "workspace:^",
|
||||
"@clusterio/host": "workspace:^",
|
||||
@@ -63,8 +63,6 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "player_sync",
|
||||
library: { type: "var", name: "plugin_player_sync" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.tsx",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
"antd": { import: false },
|
||||
"react": { import: false },
|
||||
"react-dom": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -22,7 +22,7 @@ type Technology = {
|
||||
level: number,
|
||||
progress: number | null,
|
||||
researched: boolean,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async function loadTechnologies(
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -57,8 +57,6 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -145,21 +145,21 @@ describe("research_sync controller", function() {
|
||||
});
|
||||
|
||||
describe("technology sync unlocking several levels", function() {
|
||||
const techSync = (name, level, researched) => mock.getHandler(controller, SyncTechnologiesRequest)(
|
||||
const syncTech = (name, level, researched) => mock.getHandler(controller, SyncTechnologiesRequest)(
|
||||
new SyncTechnologiesRequest([new TechnologySync(name, level, null, researched)]), src
|
||||
);
|
||||
it("should drop one entry per level unlocked on a known technology", async function() {
|
||||
await techSync("m", 4, true);
|
||||
await syncTech("m", 4, true);
|
||||
await update(["m", "m", "m", "x"]);
|
||||
sent.length = 0;
|
||||
await techSync("m", 7, true);
|
||||
await syncTech("m", 7, true);
|
||||
assert.deepEqual(await sync(), ["x"]);
|
||||
assert.equal(queueEvents().length, 1);
|
||||
assert.deepEqual(queueEvents()[0][1], new QueueEvent(["x"], null, null));
|
||||
});
|
||||
it("should drop one entry per level unlocked on a new technology", async function() {
|
||||
await update(["m", "m", "x"]);
|
||||
await techSync("m", 2, true);
|
||||
await syncTech("m", 2, true);
|
||||
assert.deepEqual(await sync(), ["x"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "research_sync",
|
||||
library: { type: "var", name: "plugin_research_sync" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -26,12 +26,12 @@ type IpcStats = {
|
||||
weight: number,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type IpcFlowType = {
|
||||
"input": Record<string, number>,
|
||||
"output": Record<string, number>,
|
||||
}
|
||||
};
|
||||
|
||||
const instancePlayerCount = new Gauge(
|
||||
"clusterio_statistics_exporter_instance_player_count",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -52,8 +52,6 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "statistics_exporter",
|
||||
library: { type: "var", name: "plugin_statistics_exporter" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
webpack.config.js
|
||||
vite.config.js
|
||||
dist/browser
|
||||
@@ -78,7 +78,9 @@ export class NeuralDole {
|
||||
}
|
||||
}
|
||||
|
||||
divider(object: { name:string, quality:string, count:number, instanceId:number, instanceName:string }): number {
|
||||
divider(
|
||||
object: { name: string, quality: string, count: number, instanceId: number, instanceName: string }
|
||||
): number {
|
||||
let magicData = doleNN.dose(
|
||||
object.count, // numReq
|
||||
this.items.getItemCount(object.name, object.quality) || 0,
|
||||
@@ -118,7 +120,7 @@ export function doleDivider(
|
||||
logItemTransfers,
|
||||
logger,
|
||||
} :{
|
||||
object: { name:string, quality:string, count:number, instanceId:number, instanceName:string },
|
||||
object: { name: string, quality: string, count: number, instanceId: number, instanceName: string },
|
||||
items: lib.ItemDatabase,
|
||||
logItemTransfers: boolean,
|
||||
logger: lib.Logger,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/node/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --mode production",
|
||||
"prepare": "tsc --build && vite build",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -61,8 +61,6 @@
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"webpack": "catalog:",
|
||||
"webpack-cli": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { clusterioPlugin } from "@clusterio/web_ui/vite.mjs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [clusterioPlugin()],
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const common = require("@clusterio/web_ui/webpack.common");
|
||||
|
||||
module.exports = (env = {}, argv = {}) => merge(common(env, argv), {
|
||||
context: __dirname,
|
||||
entry: "./web/index.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist", "web"),
|
||||
},
|
||||
plugins: [
|
||||
new webpack.container.ModuleFederationPlugin({
|
||||
name: "subspace_storage",
|
||||
library: { type: "var", name: "plugin_subspace_storage" },
|
||||
exposes: {
|
||||
"./": "./index.ts",
|
||||
"./package.json": "./package.json",
|
||||
"./web": "./web/index.tsx",
|
||||
},
|
||||
shared: {
|
||||
"@clusterio/lib": { import: false },
|
||||
"@clusterio/web_ui": { import: false },
|
||||
"antd": { import: false },
|
||||
"react": { import: false },
|
||||
"react-dom": { import: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
+4
-7
@@ -6,8 +6,8 @@ packages:
|
||||
- test/
|
||||
|
||||
catalog:
|
||||
'@ant-design/icons': ^5.6.1
|
||||
'@sinclair/typebox': ^0.34.52
|
||||
'@swc/core': ^1.16.18
|
||||
'@types/express': ^5.0.6
|
||||
'@types/finalhandler': ^1.2.4
|
||||
'@types/jsonwebtoken': ^9.0.10
|
||||
@@ -19,6 +19,7 @@ catalog:
|
||||
'@types/yargs': ^17.0.35
|
||||
'@types/yauzl': ^3.4.0
|
||||
'@types/yazl': ^3.3.1
|
||||
'@vitejs/plugin-react': ^6.1.2
|
||||
antd: ^5.29.3
|
||||
chalk: ^6.0.1
|
||||
express: ^5.3.0
|
||||
@@ -26,13 +27,12 @@ catalog:
|
||||
react: ^18.3.1
|
||||
react-dom: ^18.3.1
|
||||
react-markdown: ^10.1.0
|
||||
react-router: ^7.18.4
|
||||
react-router-dom: ^7.18.4
|
||||
semver: ^7.8.5
|
||||
set-blocking: ^2.0.0
|
||||
typescript: ^5.9.3
|
||||
webpack: ^5.112.0
|
||||
webpack-cli: ^7.2.3
|
||||
webpack-merge: 5.9.0
|
||||
vite: ^8.3.4
|
||||
winston: ^3.19.0
|
||||
winston-daily-rotate-file: ^5.0.0
|
||||
winston-transport: ^4.9.0
|
||||
@@ -47,7 +47,4 @@ autoInstallPeers: false
|
||||
|
||||
linkWorkspacePackages: true
|
||||
|
||||
allowBuilds:
|
||||
'@swc/core': true
|
||||
|
||||
shellEmulator: true
|
||||
@@ -181,7 +181,10 @@ describe("controller/src/Controller", function() {
|
||||
before(async function() {
|
||||
const app = express();
|
||||
app.locals.controller = { config: { get: () => "Test Cluster" } };
|
||||
app.locals.mainBundle = "static/main.abc.js";
|
||||
app.locals.webBundle = {
|
||||
main: "static/main.abc.js",
|
||||
imports: { "react": "static/shared-react.abc.js" },
|
||||
};
|
||||
for (let route of ["/", "/instances/:id/view"]) {
|
||||
app.get(route, Controller.serveWeb(route));
|
||||
}
|
||||
@@ -199,12 +202,14 @@ describe("controller/src/Controller", function() {
|
||||
assert(html.includes("src=\"./static/main.abc.js\""));
|
||||
assert(html.includes("new URL(\"./\", document.location)"));
|
||||
assert(html.includes("new URL(\"./static/\", document.location)"));
|
||||
assert(html.includes('{"imports":{"react":"./static/shared-react.abc.js"}}'));
|
||||
});
|
||||
it("should use relative roots for nested routes", async function() {
|
||||
let html = await (await fetch(`http://localhost:${port}/instances/1/view`)).text();
|
||||
assert(html.includes("src=\"../../static/main.abc.js\""));
|
||||
assert(html.includes("new URL(\"../../\", document.location)"));
|
||||
assert(html.includes("new URL(\"../../static/\", document.location)"));
|
||||
assert(html.includes('{"imports":{"react":"../../static/shared-react.abc.js"}}'));
|
||||
html = await (await fetch(`http://localhost:${port}/instances/1/view/`)).text();
|
||||
assert(html.includes("src=\"../../../static/main.abc.js\""));
|
||||
assert(html.includes("new URL(\"../../../static/\", document.location)"));
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("controller/src/routes", function() {
|
||||
it("should strip static/ from the plugin bundle path", async function() {
|
||||
controller.pluginInfos = [{
|
||||
name: "foo", version: "1.0.0", npmPackage: "foo",
|
||||
manifest: { "foo.js": "static/foo.abc.js" },
|
||||
manifest: { main: "static/foo.abc.js" },
|
||||
}];
|
||||
controller.loadedPlugins = new Set(controller.pluginInfos);
|
||||
let response = await fetch(endpoint);
|
||||
@@ -171,15 +171,16 @@ describe("controller/src/routes", function() {
|
||||
controller.pluginInfos = [
|
||||
{ name: "foo", version: "1.0.0", npmPackage: "foo", webEntrypoint: "dist/web/index.js" },
|
||||
{ name: "bar", version: "1.0.0", npmPackage: "bar", manifest: {} },
|
||||
{ name: "baz", version: "1.0.0", npmPackage: "baz", manifest: { "baz.js": "remoteEntry.js" } },
|
||||
{ name: "baz", version: "1.0.0", npmPackage: "baz", manifest: { "baz.js": "static/baz.abc.js" } },
|
||||
];
|
||||
controller.loadedPlugins = new Set([controller.pluginInfos[0]]);
|
||||
let response = await fetch(endpoint);
|
||||
assert.equal(response.status, 200);
|
||||
let data = await response.json();
|
||||
assert.equal(data[0].web.error, "Missing dist/web/manifest.json");
|
||||
assert.equal(data[1].web.error, "Missing bar.js entry in manifest.json");
|
||||
assert.equal(data[2].web.error, "Incompatible old remoteEntry.js entrypoint.");
|
||||
const incompatible = "Incompatible dist/web/manifest.json, the plugin must be rebuilt with Vite";
|
||||
assert.equal(data[1].web.error, incompatible);
|
||||
assert.equal(data[2].web.error, incompatible);
|
||||
assert.equal(data[1].loaded, false);
|
||||
assert.equal(data[1].enabled, false);
|
||||
});
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ describe("host/server", function() {
|
||||
let port = hostServer._randomDynamicPort();
|
||||
assert.equal(typeof port, "number");
|
||||
assert(Number.isInteger(port));
|
||||
assert(0 <= port && port < 2**16);
|
||||
assert(port >= 0 && port < 2**16);
|
||||
});
|
||||
|
||||
it("should return a port number in the dynamic range", function() {
|
||||
|
||||
@@ -1313,7 +1313,7 @@ describe("Integration of Clusterio", function() {
|
||||
} else if (isV2) {
|
||||
exchangeString = testStrings.modified_v2;
|
||||
}
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @stylistic/max-len
|
||||
const args = `base/freeplay --seed 1234 --map-exchange-string "${exchangeString.replace(/[\n\r]+/g, "")}"`;
|
||||
await execCtl("instance stop 44");
|
||||
|
||||
|
||||
Reference in new issue
Block a user