From c0643fadca2865e242f5b34a9de59d8b2c8ce267 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 3 Oct 2023 09:19:19 +0100 Subject: [PATCH] Build a web-based emulator for the documentation site (#1597) Historically we've used copy-cat to provide a web-based emulator for running example code on our documentation site. However, copy-cat is often out-of-date with CC:T, which means example snippets fail when you try to run them! This commit vendors in copy-cat (or rather an updated version of it) into CC:T itself, allowing us to ensure the emulator is always in sync with the mod. While the ARCHITECTURE.md documentation goes into a little bit more detail here, the general implementation is as follows - In project/src/main we implement the core of the emulator. This includes a basic reimplementation of some of CC's classes to work on the web (mostly the HTTP API and ComputerThread), and some additional code to expose the computers to Javascript. - This is all then compiled to Javascript using [TeaVM][1] (we actually use a [personal fork of it][2] as there's a couple of changes I've not upstreamed yet). - The Javascript side then pulls in the these compiled classes (and the CC ROM) and hooks them up to [cc-web-term][3] to display the actual computer. - As we're no longer pulling in copy-cat, we can simplify our bundling system a little - we now just compile to ESM modules directly. [1]: https://github.com/konsoletyper/teavm [2]: https://github.com/SquidDev/teavm/tree/squid-patches [3]: https://github.com/squiddev-cc/cc-web-term --- .../cc-tweaked.java-convention.gradle.kts | 6 +- .../kotlin/cc/tweaked/gradle/Extensions.kt | 5 +- config/checkstyle/suppressions.xml | 6 + doc/events/file_transfer.md | 2 +- gradle/libs.versions.toml | 19 +- illuaminate.sexp | 2 +- package-lock.json | 1598 +++++++++++++++-- package.json | 7 +- projects/core-api/build.gradle.kts | 4 +- .../core/asm/PeripheralMethodSupplier.java | 5 +- projects/web/ARCHITECTURE.md | 43 + projects/web/build.gradle.kts | 61 +- projects/web/rollup.config.js | 46 +- .../java/cc/tweaked/web/builder/Builder.java | 150 ++ .../cc/tweaked/web/builder/CloseScope.java | 50 + .../cc/tweaked/web/builder/PatchCobalt.java | 133 ++ .../web/builder/TransformingClassLoader.java | 164 ++ .../cc/tweaked/web/builder/package-info.java | 14 + projects/web/src/frontend/emu/computer.ts | 141 ++ projects/web/src/frontend/emu/index.tsx | 51 + projects/web/src/frontend/emu/java.ts | 38 + projects/web/src/frontend/index.tsx | 56 +- projects/web/src/frontend/typings.ts | 151 +- .../src/htmlTransform/components/Recipe.tsx | 8 +- .../htmlTransform/components/WithExport.tsx | 4 +- .../src/htmlTransform/components/support.tsx | 2 +- projects/web/src/htmlTransform/index.tsx | 14 +- .../java/cc/tweaked/web/EmulatedComputer.java | 209 +++ .../cc/tweaked/web/EmulatorEnvironment.java | 57 + .../src/main/java/cc/tweaked/web/Main.java | 44 + .../java/cc/tweaked/web/ResourceMount.java | 52 + .../java/cc/tweaked/web/js/Callbacks.java | 71 + .../cc/tweaked/web/js/ComputerDisplay.java | 61 + .../cc/tweaked/web/js/ComputerHandle.java | 79 + .../main/java/cc/tweaked/web/js/Console.java | 28 + .../cc/tweaked/web/js/JavascriptConv.java | 82 + .../java/cc/tweaked/web/package-info.java | 14 + .../cc/tweaked/web/peripheral/AudioState.java | 65 + .../web/peripheral/SpeakerPeripheral.java | 88 + .../web/peripheral/TickablePeripheral.java | 14 + .../java/cc/tweaked/web/stub/FileChannel.java | 24 + .../cc/tweaked/web/stub/ReentrantLock.java | 20 + .../computercraft/core/TCoreConfig.java | 26 + .../core/apis/http/TCheckUrl.java | 30 + .../core/apis/http/request/THttpRequest.java | 156 ++ .../core/apis/http/websocket/TWebsocket.java | 107 ++ .../core/asm/MethodReflection.java | 137 ++ .../core/asm/StaticGenerator.java | 313 ++++ .../core/asm/TLuaMethodSupplier.java | 43 + .../core/asm/TPeripheralMethodSupplier.java | 41 + .../core/computer/TComputerThread.java | 61 + .../codec/http/TDefaultHttpHeaders.java | 154 ++ .../main/java/io/netty/util/TAsciiString.java | 55 + .../main/java/org/slf4j/TLoggerFactory.java | 99 + .../main/java/org/slf4j/TMarkerFactory.java | 69 + 55 files changed, 4762 insertions(+), 217 deletions(-) create mode 100644 projects/web/ARCHITECTURE.md create mode 100644 projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java create mode 100644 projects/web/src/builder/java/cc/tweaked/web/builder/CloseScope.java create mode 100644 projects/web/src/builder/java/cc/tweaked/web/builder/PatchCobalt.java create mode 100644 projects/web/src/builder/java/cc/tweaked/web/builder/TransformingClassLoader.java create mode 100644 projects/web/src/builder/java/cc/tweaked/web/builder/package-info.java create mode 100644 projects/web/src/frontend/emu/computer.ts create mode 100644 projects/web/src/frontend/emu/index.tsx create mode 100644 projects/web/src/frontend/emu/java.ts create mode 100644 projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/EmulatorEnvironment.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/Main.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/ResourceMount.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/js/ComputerDisplay.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/js/ComputerHandle.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/js/Console.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/package-info.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/peripheral/AudioState.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/peripheral/TickablePeripheral.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/stub/FileChannel.java create mode 100644 projects/web/src/main/java/cc/tweaked/web/stub/ReentrantLock.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/TCoreConfig.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/apis/http/TCheckUrl.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/asm/MethodReflection.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/asm/TLuaMethodSupplier.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/asm/TPeripheralMethodSupplier.java create mode 100644 projects/web/src/main/java/dan200/computercraft/core/computer/TComputerThread.java create mode 100644 projects/web/src/main/java/io/netty/handler/codec/http/TDefaultHttpHeaders.java create mode 100644 projects/web/src/main/java/io/netty/util/TAsciiString.java create mode 100644 projects/web/src/main/java/org/slf4j/TLoggerFactory.java create mode 100644 projects/web/src/main/java/org/slf4j/TMarkerFactory.java diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index f7497f8d0..e61b43776 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -66,6 +66,7 @@ repositories { includeGroup("me.shedaniel.cloth") includeGroup("me.shedaniel") includeGroup("mezz.jei") + includeGroup("org.teavm") includeModule("com.terraformersmc", "modmenu") includeModule("me.lucko", "fabric-permissions-api") } @@ -99,7 +100,10 @@ sourceSets.all { check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty check("NullAway", CheckSeverity.ERROR) - option("NullAway:AnnotatedPackages", listOf("dan200.computercraft", "net.fabricmc.fabric.api").joinToString(",")) + option( + "NullAway:AnnotatedPackages", + listOf("dan200.computercraft", "cc.tweaked", "net.fabricmc.fabric.api").joinToString(","), + ) option("NullAway:ExcludedFieldAnnotations", listOf("org.spongepowered.asm.mixin.Shadow").joinToString(",")) option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull") option("NullAway:CheckOptionalEmptiness") diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt index 27cf9dcc0..9c520eae8 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt @@ -5,6 +5,7 @@ package cc.tweaked.gradle import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.file.FileSystemLocation import org.gradle.api.file.FileSystemLocationProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider @@ -126,5 +127,5 @@ inline fun use(block: (CloseScope) -> R): R { /** Proxy method to avoid overload ambiguity. */ fun Property.setProvider(provider: Provider) = set(provider) -/** Short-cut method to get the absolute path of a [FileSystemLocationProperty]. */ -fun FileSystemLocationProperty<*>.getAbsolutePath(): String = get().asFile.absolutePath +/** Short-cut method to get the absolute path of a [FileSystemLocation] provider. */ +fun Provider.getAbsolutePath(): String = get().asFile.absolutePath diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 5fbc64125..568411c49 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -16,4 +16,10 @@ SPDX-License-Identifier: MPL-2.0 + + + + + + diff --git a/doc/events/file_transfer.md b/doc/events/file_transfer.md index 560037472..dc73238da 100644 --- a/doc/events/file_transfer.md +++ b/doc/events/file_transfer.md @@ -29,7 +29,7 @@ ## Example local size = file.seek("end") file.seek("set", 0) - print(file.getName() .. " " .. file.getSize()) + print(file.getName() .. " " .. size) end ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11a3c3113..db44bf33b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ kotlin = "1.8.10" kotlin-coroutines = "1.6.4" netty = "4.1.82.Final" nightConfig = "3.6.7" -slf4j = "1.7.36" +slf4j = "2.0.1" # Minecraft mods emi = "1.0.8+1.20.1" @@ -60,7 +60,7 @@ fabric-loom = "1.3.7" forgeGradle = "6.0.8" githubRelease = "2.4.1" ideaExt = "1.1.7" -illuaminate = "0.1.0-40-g975cbc3" +illuaminate = "0.1.0-44-g9ee0055" librarian = "1.+" minotaur = "2.+" mixinGradle = "0.7.+" @@ -69,10 +69,12 @@ spotless = "6.21.0" taskTree = "2.1.1" vanillaGradle = "0.2.1-SNAPSHOT" vineflower = "1.11.0" +teavm = "0.9.0-SQUID.1" [libraries] # Normal dependencies asm = { module = "org.ow2.asm:asm", version.ref = "asm" } +asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" } checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" } cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" } @@ -138,6 +140,14 @@ librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" } minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" } nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } +teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" } +teavm-jso = { module = "org.teavm:teavm-jso", version.ref = "teavm" } +teavm-jso-apis = { module = "org.teavm:teavm-jso-apis", version.ref = "teavm" } +teavm-jso-impl = { module = "org.teavm:teavm-jso-impl", version.ref = "teavm" } +teavm-metaprogramming-api = { module = "org.teavm:teavm-metaprogramming-api", version.ref = "teavm" } +teavm-metaprogramming-impl = { module = "org.teavm:teavm-metaprogramming-impl", version.ref = "teavm" } +teavm-platform = { module = "org.teavm:teavm-platform", version.ref = "teavm" } +teavm-tooling = { module = "org.teavm:teavm-tooling", version.ref = "teavm" } vanillaGradle = { module = "org.spongepowered:vanillagradle", version.ref = "vanillaGradle" } vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" } @@ -151,6 +161,7 @@ mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" } taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" } [bundles] +annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"] kotlin = ["kotlin-stdlib", "kotlin-coroutines"] # Minecraft @@ -164,3 +175,7 @@ externalMods-fabric-runtime = ["jei-fabric", "modmenu"] # Testing test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"] testRuntime = ["junit-jupiter-engine", "jqwik-engine"] + +# Build tools +teavm-api = [ "teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api" ] +teavm-tooling = [ "teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl" ] diff --git a/illuaminate.sexp b/illuaminate.sexp index b72f0c497..582df0ae3 100644 --- a/illuaminate.sexp +++ b/illuaminate.sexp @@ -23,7 +23,7 @@ (url https://tweaked.cc/) (source-link https://github.com/cc-tweaked/CC-Tweaked/blob/${commit}/${path}#L${line}) - (styles /projects/web/src/frontend/styles.css) + (styles /projects/web/build/rollup/index.css) (scripts /projects/web/build/rollup/index.js) (head doc/head.html)) diff --git a/package-lock.json b/package-lock.json index df02b8bb7..844108d71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "1.0.0", "license": "BSD-3-Clause", "dependencies": { + "@squid-dev/cc-web-term": "^2.0.0", "preact": "^10.5.5", + "setimmediate": "^1.0.5", "tslib": "^2.0.3" }, "devDependencies": { - "@rollup/plugin-terser": "^0.4.0", + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-typescript": "^11.0.0", "@rollup/plugin-url": "^8.0.1", "@types/glob": "^8.1.0", @@ -22,42 +25,12 @@ "rehype": "^13.0.0", "rehype-highlight": "^7.0.0", "rehype-react": "^8.0.0", - "requirejs": "^2.3.6", "rollup": "^3.19.1", + "rollup-plugin-postcss": "^4.0.2", "tsx": "^3.12.10", "typescript": "^5.2.2" } }, - "node_modules/@esbuild-kit/cjs-loader": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@esbuild-kit/cjs-loader/-/cjs-loader-2.4.4.tgz", - "integrity": "sha512-NfsJX4PdzhwSkfJukczyUiZGc7zNNWZcEAyqeISpDnn0PTfzMJR1aR8xAIPskBejIxBJbIgCCMzbaYa9SXepIg==", - "dev": true, - "dependencies": { - "@esbuild-kit/core-utils": "^3.2.3", - "get-tsconfig": "^4.7.0" - } - }, - "node_modules/@esbuild-kit/core-utils": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", - "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", - "dev": true, - "dependencies": { - "esbuild": "~0.18.20", - "source-map-support": "^0.5.21" - } - }, - "node_modules/@esbuild-kit/esm-loader": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", - "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", - "dev": true, - "dependencies": { - "@esbuild-kit/core-utils": "^3.3.2", - "get-tsconfig": "^4.7.0" - } - }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -495,6 +468,31 @@ "node": ">=14" } }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", + "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-terser": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", @@ -518,9 +516,9 @@ } }, "node_modules/@rollup/plugin-typescript": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.3.tgz", - "integrity": "sha512-8o6cNgN44kQBcpsUJTbTXMTtb87oR1O0zgP3Dxm71hrNgparap3VujgofEilTYJo+ivf2ke6uy3/E5QEaiRlDA==", + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.4.tgz", + "integrity": "sha512-WZRh5LBVLQXdKFICUId5J3eIpmjGURaBqntfg3GSZACgeOAFS+lOSMGTwfzDkELTaZVp/lWdMVNU3UkwCUBg/Q==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -587,10 +585,28 @@ } } }, + "node_modules/@squid-dev/cc-web-term": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@squid-dev/cc-web-term/-/cc-web-term-2.0.0.tgz", + "integrity": "sha512-pFa3bOFG7vTwVmh+aBvQFIma+I8vaib1JXNaepYFsifF+HvFFvjlje8Qh2Aix1EjUlKJH0XjONO5c9NdvBMVfg==", + "dependencies": { + "gif.js": "^0.2.0", + "preact": "^10.5.5" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", + "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", "dev": true }, "node_modules/@types/glob": { @@ -613,9 +629,9 @@ } }, "node_modules/@types/mdast": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.0.tgz", - "integrity": "sha512-YLeG8CujC9adtj/kuDzq1N4tCDYKoZ5l/bnjq8d74+t/3q/tHquJOJKUQXJrLCflOHpKjXgcI/a929gpmLOEng==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.1.tgz", + "integrity": "sha512-IlKct1rUTJ1T81d8OHzyop15kGv9A/ff7Gz7IJgrk6jDb4Udw77pCJ+vq8oxZf4Ghpm+616+i1s/LNg/Vh7d+g==", "dev": true, "dependencies": { "@types/unist": "*" @@ -628,9 +644,15 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.0.tgz", + "integrity": "sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, "node_modules/@types/unist": { @@ -670,12 +692,15 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -697,6 +722,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -706,12 +737,88 @@ "balanced-match": "^1.0.0" } }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001541", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz", + "integrity": "sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -722,6 +829,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", @@ -760,6 +883,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -771,10 +900,22 @@ } }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -790,6 +931,168 @@ "node": ">= 8" } }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -812,12 +1115,82 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/electron-to-chromium": { + "version": "1.4.537", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.537.tgz", + "integrity": "sha512-W1+g9qs9hviII0HAwOdehGYkr+zt7KKdmCcJcjH0mYg6oL8+ioT3Skjmt7BLoAQqXhjf40AXd+HlR4oAWMlXjA==", + "dev": true + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -873,12 +1246,27 @@ "@esbuild/win32-x64": "0.18.20" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -921,10 +1309,19 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "dev": true, + "dependencies": { + "loader-utils": "^3.2.0" + } + }, "node_modules/get-tsconfig": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.0.tgz", - "integrity": "sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -933,20 +1330,25 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gif.js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/gif.js/-/gif.js-0.2.0.tgz", + "integrity": "sha512-bYxCoT8OZKmbxY8RN4qDiYuj4nrQDTzgLRcFVovyona1PTWNePzI4nzOmotnlOFIzTk/ZxAHtv+TfVLiBWj/hw==" + }, "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", + "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -967,6 +1369,15 @@ "node": ">= 0.4.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hast-util-from-html": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", @@ -1081,9 +1492,9 @@ } }, "node_modules/hast-util-to-jsx-runtime": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.1.1.tgz", - "integrity": "sha512-YDAf9FaxFzjs/tGXBoLQTB/EGxBGjiGD6Xd5NdkMWw3B4gfKGo0sEO65/E+ihDSsWV+AF61e2Ro2e2J8rdzsZg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.2.0.tgz", + "integrity": "sha512-wSlp23N45CMjDg/BPW8zvhEi3R+8eRE1qFbjEyAUzMCzu2l1Wzwakq+Tlia9nkCtEl5mDxa7nKHsvYJ6Gfn21A==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -1185,12 +1596,69 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", + "dev": true + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", "dev": true }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -1212,6 +1680,12 @@ "node": ">=8" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -1231,9 +1705,9 @@ "dev": true }, "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -1248,6 +1722,42 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, "node_modules/lowlight": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.0.0.tgz", @@ -1307,6 +1817,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, "node_modules/micromark-util-character": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz", @@ -1424,14 +1940,100 @@ } }, "node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -1475,6 +2077,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1487,10 +2095,582 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz", + "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==", + "dev": true, + "dependencies": { + "generic-names": "^4.0.0", + "icss-replace-symbols": "^1.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.1" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/preact": { - "version": "10.17.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.17.1.tgz", - "integrity": "sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA==", + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.0.tgz", + "integrity": "sha512-O4dGFmErPd3RNVDvXmCbOW6hetnve6vYtjx5qf51mCUmBS96s66MrNQkEII5UThDGoNF7953ptA+aNupiDxVeg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -1514,6 +2694,15 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "dev": true }, + "node_modules/promise.series": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", + "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, "node_modules/property-information": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.3.0.tgz", @@ -1611,19 +2800,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/requirejs": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", - "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", - "dev": true, - "bin": { - "r_js": "bin/r.js", - "r.js": "bin/r.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -1641,6 +2817,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1651,9 +2836,9 @@ } }, "node_modules/rollup": { - "version": "3.29.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz", - "integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1666,6 +2851,48 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-postcss": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz", + "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "concat-with-sourcemaps": "^1.1.0", + "cssnano": "^5.0.1", + "import-cwd": "^3.0.0", + "p-queue": "^6.6.2", + "pify": "^5.0.0", + "postcss-load-config": "^3.0.0", + "postcss-modules": "^4.0.0", + "promise.series": "^0.2.0", + "resolve": "^1.19.0", + "rollup-pluginutils": "^2.8.2", + "safe-identifier": "^0.4.2", + "style-inject": "^0.3.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "8.x" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1686,6 +2913,12 @@ } ] }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "dev": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1704,6 +2937,11 @@ "randombytes": "^2.1.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1738,9 +2976,9 @@ } }, "node_modules/smob": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz", - "integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", + "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", "dev": true }, "node_modules/source-map": { @@ -1752,6 +2990,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -1772,6 +3020,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -1882,6 +3143,12 @@ "node": ">=8" } }, + "node_modules/style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, "node_modules/style-to-object": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.2.tgz", @@ -1891,6 +3158,34 @@ "inline-style-parser": "0.1.1" } }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -1903,10 +3198,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/terser": { - "version": "5.19.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", - "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", + "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -1921,6 +3237,12 @@ "node": ">=10" } }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -1947,20 +3269,20 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsx": { - "version": "3.12.10", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.12.10.tgz", - "integrity": "sha512-2+46h4xvUt1aLDNvk5YBT8Uzw+b7BolGbn7iSMucYqCXZiDc+1IMghLVdw8kKjING32JFOeO+Am9posvjkeclA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.13.0.tgz", + "integrity": "sha512-rjmRpTu3as/5fjNq/kOkOtihgLxuIz6pbKdj9xwP4J5jOLkBxw/rjN5ANw+KyrrOXV5uB7HC8+SrrSJxT65y+A==", "dev": true, "dependencies": { - "@esbuild-kit/cjs-loader": "^2.4.2", - "@esbuild-kit/core-utils": "^3.3.0", - "@esbuild-kit/esm-loader": "^2.6.3" + "esbuild": "~0.18.20", + "get-tsconfig": "^4.7.2", + "source-map-support": "^0.5.21" }, "bin": { - "tsx": "dist/cli.js" + "tsx": "dist/cli.mjs" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" } }, "node_modules/typescript": { @@ -2077,6 +3399,42 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/vfile": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", @@ -2189,21 +3547,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2236,6 +3579,27 @@ "node": ">=8" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index b3eb6d7a9..b0266b102 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,14 @@ "license": "BSD-3-Clause", "type": "module", "dependencies": { + "@squid-dev/cc-web-term": "^2.0.0", "preact": "^10.5.5", + "setimmediate": "^1.0.5", "tslib": "^2.0.3" }, "devDependencies": { - "@rollup/plugin-terser": "^0.4.0", + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-typescript": "^11.0.0", "@rollup/plugin-url": "^8.0.1", "@types/glob": "^8.1.0", @@ -19,8 +22,8 @@ "rehype": "^13.0.0", "rehype-highlight": "^7.0.0", "rehype-react": "^8.0.0", - "requirejs": "^2.3.6", "rollup": "^3.19.1", + "rollup-plugin-postcss": "^4.0.2", "tsx": "^3.12.10", "typescript": "^5.2.2" } diff --git a/projects/core-api/build.gradle.kts b/projects/core-api/build.gradle.kts index ccc61de99..4341fba36 100644 --- a/projects/core-api/build.gradle.kts +++ b/projects/core-api/build.gradle.kts @@ -18,9 +18,7 @@ val docApi by configurations.registering { } dependencies { - compileOnlyApi(libs.jsr305) - compileOnlyApi(libs.checkerFramework) - compileOnlyApi(libs.jetbrainsAnnotations) + compileOnlyApi(libs.bundles.annotations) "docApi"(project(":common-api")) } diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java index ca7fa8ff3..f03a5ac77 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java +++ b/projects/core/src/main/java/dan200/computercraft/core/asm/PeripheralMethodSupplier.java @@ -20,7 +20,7 @@ * This is used by {@link ComputerContext} to construct {@linkplain ComputerContext#peripheralMethods() the context-wide * method supplier}. It should not be used directly. */ -public class PeripheralMethodSupplier { +public final class PeripheralMethodSupplier { private static final Generator GENERATOR = new Generator<>(PeripheralMethod.class, List.of(ILuaContext.class, IComputerAccess.class), m -> (target, context, computer, args) -> { var escArgs = args.escapes(); @@ -31,6 +31,9 @@ public class PeripheralMethodSupplier { method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args) ); + private PeripheralMethodSupplier() { + } + public static MethodSupplier create(List genericMethods) { return new MethodSupplierImpl<>(genericMethods, GENERATOR, DYNAMIC, x -> x instanceof IDynamicPeripheral dynamic ? Objects.requireNonNull(dynamic.getMethodNames(), "Dynamic methods cannot be null") diff --git a/projects/web/ARCHITECTURE.md b/projects/web/ARCHITECTURE.md new file mode 100644 index 000000000..949735ea8 --- /dev/null +++ b/projects/web/ARCHITECTURE.md @@ -0,0 +1,43 @@ + + +# Architecture +As mentioned in the main architecture guide, the web subproject is responsible for building CC: Tweaked's documentation +website. This is surprisingly more complex than one might initially assume, hence the need for this document at all! + +## Web-based emulator +Most of the complexity comes from the web-based emulator we embed in our documentation. This uses [TeaVM] to compile +CC: Tweaked's core to Javascript, and then call out to it in the main site. + +The code for this is split into three separate components: + - `src/main`: This holds the emulator itself: this is a basic Java project which depends on CC:T's core, and exposes an + interface for Javascript code. + + Some of our code (or dependencies) cannot be compiled to Javascript, for instance most of our HTTP implementation. In + theses cases we provide a replacement class. These classes start with `T` (for instance `THttpRequest`), which are + specially handled in the next step. + + - `src/builder`: This module consumes the above code and compiles everything to Javascript using TeaVM. There's a + couple of places where we need to patch the bytecode before compiling it, so this also includes a basic ASM + rewriting system. + + - `src/frontend`: This consumes the interface exposed by the main emulator, and actually embeds the emulator in the + website. + +## Static content +Rendering the static portion of the website is fortunately much simpler. + + - Doc generation: This is mostly handled in various Gradle files. The Forge Gradle script uses [cct-javadoc] to convert + Javadoc on our peripherals and APIs to LDoc/[illuaminate] compatible documentation. This is then fed into illuaminate + which spits out HTML. + + - `src/htmlTransform`: We do a small amount of post-processing on the HTML in order. This project does syntax + highlighting of non-Lua code blocks, and replaces special `` tags with a rendered view of a given + Minecraft recipe. + +[TeaVM]: https://github.com/konsoletyper/teavm "TeaVM - Compiler of Java bytecode to JavaScript" +[cct-javadoc]: https://github.com/cc-tweaked/cct-javadoc: "cct-javadoc - A Javadoc doclet to extract documentation from @LuaFunction methods." +[illuaminate]: https://github.com/Squiddev/illuaminate: "illuaminate - Very WIP static analysis for Lua" diff --git a/projects/web/build.gradle.kts b/projects/web/build.gradle.kts index 3fbb42c01..58a4d0189 100644 --- a/projects/web/build.gradle.kts +++ b/projects/web/build.gradle.kts @@ -5,11 +5,13 @@ import cc.tweaked.gradle.getAbsolutePath plugins { - `lifecycle-base` + id("cc-tweaked.java-convention") id("cc-tweaked.node") id("cc-tweaked.illuaminate") } +val modVersion: String by extra + node { projectRoot.set(rootProject.projectDir) } @@ -18,12 +20,61 @@ illuaminate { version.set(libs.versions.illuaminate) } +sourceSets.register("builder") + +dependencies { + implementation(project(":core")) + implementation(libs.bundles.teavm.api) + implementation(libs.asm) + implementation(libs.guava) + implementation(libs.netty.http) + implementation(libs.slf4j) + + "builderCompileOnly"(libs.bundles.annotations) + "builderImplementation"(libs.bundles.teavm.tooling) + "builderImplementation"(libs.asm) + "builderImplementation"(libs.asm.commons) +} + +val compileTeaVM by tasks.registering(JavaExec::class) { + group = LifecycleBasePlugin.BUILD_GROUP + description = "Generate our classes and resources files" + + val output = layout.buildDirectory.dir("teaVM") + val minify = !project.hasProperty("noMinify") + + inputs.property("version", modVersion) + inputs.property("minify", minify) + inputs.files(sourceSets.main.get().runtimeClasspath).withPropertyName("inputClasspath") + outputs.dir(output).withPropertyName("output") + + classpath = sourceSets["builder"].runtimeClasspath + jvmArguments.addAll( + provider { + val main = sourceSets.main.get() + listOf( + "-Dcct.input=${main.output.classesDirs.asPath}", + "-Dcct.version=$modVersion", + "-Dcct.classpath=${main.runtimeClasspath.asPath}", + "-Dcct.output=${output.getAbsolutePath()}", + "-Dcct.minify=$minify", + ) + }, + ) + mainClass.set("cc.tweaked.web.builder.Builder") + javaLauncher.set(project.javaToolchains.launcherFor { languageVersion.set(java.toolchain.languageVersion) }) +} + val rollup by tasks.registering(cc.tweaked.gradle.NpxExecToDir::class) { group = LifecycleBasePlugin.BUILD_GROUP description = "Bundles JS into rollup" + val minify = !project.hasProperty("noMinify") + inputs.property("minify", minify) + // Sources inputs.files(fileTree("src/frontend")).withPropertyName("sources") + inputs.files(compileTeaVM) // Config files inputs.file("tsconfig.json").withPropertyName("Typescript config") inputs.file("rollup.config.js").withPropertyName("Rollup config") @@ -31,7 +82,7 @@ val rollup by tasks.registering(cc.tweaked.gradle.NpxExecToDir::class) { // Output directory. Also defined in illuaminate.sexp and rollup.config.js output.set(layout.buildDirectory.dir("rollup")) - args = listOf("rollup", "--config", "rollup.config.js") + args = listOf("rollup", "--config", "rollup.config.js") + if (minify) emptyList() else listOf("--configDebug") } val illuaminateDocs by tasks.registering(cc.tweaked.gradle.IlluaminateExecToDir::class) { @@ -44,9 +95,8 @@ val illuaminateDocs by tasks.registering(cc.tweaked.gradle.IlluaminateExecToDir: inputs.files(rootProject.fileTree("doc")).withPropertyName("docs") inputs.files(project(":core").fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom") inputs.files(project(":forge").tasks.named("luaJavadoc")) - // Additional assets + // Assets inputs.files(rollup) - inputs.file("src/frontend/styles.css").withPropertyName("styles") // Output directory. Also defined in illuaminate.sexp. output.set(layout.buildDirectory.dir("illuaminate")) @@ -72,7 +122,8 @@ val htmlTransform by tasks.registering(cc.tweaked.gradle.NpxExecToDir::class) { argumentProviders.add { listOf( - "tsx", sources.dir.resolve("index.tsx").absolutePath, + "tsx", + sources.dir.resolve("index.tsx").absolutePath, illuaminateDocs.get().output.getAbsolutePath(), sources.dir.resolve("export/index.json").absolutePath, output.getAbsolutePath(), diff --git a/projects/web/rollup.config.js b/projects/web/rollup.config.js index 5390894ed..3ad79e072 100644 --- a/projects/web/rollup.config.js +++ b/projects/web/rollup.config.js @@ -2,52 +2,48 @@ // // SPDX-License-Identifier: MPL-2.0 -import { readFileSync } from "fs"; import path from "path"; import terser from "@rollup/plugin-terser"; +import resolve from "@rollup/plugin-node-resolve"; import typescript from "@rollup/plugin-typescript"; import url from "@rollup/plugin-url"; +import postcss from "rollup-plugin-postcss"; const input = "src/frontend"; -const requirejs = readFileSync("../../node_modules/requirejs/require.js"); -/** @type import("rollup").RollupOptions */ -export default { +const minify = args => !args.configDebug; + +/** @type import("rollup").RollupOptionsFunction */ +export default args => ({ input: [`${input}/index.tsx`], output: { // Also defined in build.gradle.kts dir: "build/rollup/", - // We bundle requirejs (and config) into the header. It's rather gross - // but also works reasonably well. - // Also suffix a ?v=${date} onto the end in the event we need to require a specific copy-cat version. - banner: ` - ${requirejs} - require.config({ - paths: { copycat: "https://copy-cat.squiddev.cc" }, - urlArgs: function(id) { return id == "copycat/embed" ? "?v=20211221" : ""; } - }); - `, - format: "amd", + format: "esm", generatedCode: { preset: "es2015", constBindings: true, }, - amd: { - define: "require", - } }, context: "window", - external: ["copycat/embed"], plugins: [ typescript(), + resolve({ browser: true }), url({ - include: "**/*.dfpwm", + include: ["**/*.dfpwm", "**/*.worker.js", "**/*.png"], fileName: "[name]-[hash][extname]", publicPath: "/", + limit: 0, + }), + + postcss({ + namedExports: true, + minimize: minify(args), + extract: true, }), { @@ -59,8 +55,14 @@ export default { ? `export default ${JSON.stringify(code)};\n` : null; }, + + async resolveId(source) { + if (source === "cct/classes") return path.resolve("build/teaVM/classes.js"); + if (source === "cct/resources") return path.resolve("build/teaVM/resources.js"); + return null; + }, }, - terser(), + minify(args) && terser(), ], -}; +}); diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java new file mode 100644 index 000000000..37687ffc3 --- /dev/null +++ b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.builder; + +import org.teavm.common.JsonUtil; +import org.teavm.tooling.ConsoleTeaVMToolLog; +import org.teavm.tooling.TeaVMProblemRenderer; +import org.teavm.tooling.TeaVMTargetType; +import org.teavm.tooling.TeaVMTool; +import org.teavm.vm.TeaVMOptimizationLevel; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +/** + * The main entrypoint to our Javascript builder. + *

+ * This generates both our classes and resources JS files. + */ +public class Builder { + public static void main(String[] args) throws Exception { + try (var scope = new CloseScope()) { + var input = getPath(scope, "cct.input"); + var classpath = getPath(scope, "cct.classpath"); + var output = getFile("cct.output"); + var version = System.getProperty("cct.version"); + var minify = Boolean.parseBoolean(System.getProperty("cct.minify", "true")); + + buildClasses(input, classpath, output, minify); + buildResources(version, input, classpath, output); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + + private static void buildClasses(List input, List classpath, Path output, boolean minify) throws Exception { + var remapper = new TransformingClassLoader(classpath); + // Remap several classes to our stubs. Really we should add all of these to TeaVM, but our current + // implementations are a bit of a hack. + remapper.remapClass("java/nio/channels/FileChannel", "cc/tweaked/web/stub/FileChannel"); + remapper.remapClass("java/util/concurrent/locks/ReentrantLock", "cc/tweaked/web/stub/ReentrantLock"); + // Add some additional transformers. + remapper.addTransformer(PatchCobalt::patch); + + // Scans the main input folders for classes starting with "T", and uses them as an overlay, replacing the + // original class with this redefinition. + for (var file : input) { + traverseClasses(file, (fullName, path) -> { + var lastPart = fullName.lastIndexOf('/'); + var className = fullName.substring(lastPart + 1); + if (className.startsWith("T") && Character.isUpperCase(className.charAt(1))) { + var originalName = fullName.substring(0, lastPart + 1) + className.substring(1); + System.out.printf("Replacing %s with %s\n", originalName, fullName); + remapper.remapClass(fullName, originalName, path); + } + }); + } + + // Then finally start the compiler! + var tool = new TeaVMTool(); + tool.setTargetType(TeaVMTargetType.JAVASCRIPT); + tool.setTargetDirectory(output.toFile()); + tool.setClassLoader(remapper); + tool.setMainClass("cc.tweaked.web.Main"); + + tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); + tool.setObfuscated(minify); + + tool.generate(); + TeaVMProblemRenderer.describeProblems(tool.getDependencyInfo().getCallGraph(), tool.getProblemProvider(), new ConsoleTeaVMToolLog(false)); + if (!tool.getProblemProvider().getSevereProblems().isEmpty()) System.exit(1); + } + + private static void buildResources(String version, List input, List classpath, Path output) throws IOException { + try (var out = Files.newBufferedWriter(output.resolve("resources.js"))) { + out.write("export const version = \""); + JsonUtil.writeEscapedString(out, version); + out.write("\";\n"); + out.write("export const resources = {\n"); + + Stream.of(input, classpath).flatMap(Collection::stream).forEach(root -> { + var start = root.resolve("data/computercraft/lua"); + if (!Files.exists(start)) return; + + try (var walker = Files.find(start, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())) { + walker.forEach(x -> { + try { + out.write(" \""); + JsonUtil.writeEscapedString(out, start.relativize(x).toString()); + out.write("\": \""); + JsonUtil.writeEscapedString(out, Files.readString(x, StandardCharsets.UTF_8)); + out.write("\",\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + + out.write("};\n"); + } + } + + private static void traverseClasses(Path root, BiConsumer child) throws IOException { + try (var walker = Files.walk(root)) { + walker.forEach(entry -> { + if (Files.isDirectory(entry)) return; + + var name = root.relativize(entry).toString(); + if (!name.endsWith(".class")) return; + + var className = name.substring(0, name.length() - 6).replace(File.separatorChar, '/'); + + child.accept(className, entry); + }); + } + } + + private static List getPath(CloseScope scope, String name) { + return Arrays.stream(System.getProperty(name).split(File.pathSeparator)) + .map(Path::of) + .filter(Files::exists) + .map(file -> { + try { + return Files.isDirectory(file) ? file : scope.add(FileSystems.newFileSystem(file)).getPath("/"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .toList(); + } + + private static Path getFile(String name) { + return Path.of(System.getProperty(name)); + } +} diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/CloseScope.java b/projects/web/src/builder/java/cc/tweaked/web/builder/CloseScope.java new file mode 100644 index 000000000..9e45074f5 --- /dev/null +++ b/projects/web/src/builder/java/cc/tweaked/web/builder/CloseScope.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.builder; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * A {@link Closeable} implementation which can be used to combine other {@link Closeable} instances. + *

+ * This is mostly identical to the version in {@link dan200.computercraft.test.core.CloseScope}. Really they should be + * merged, but not sure where/how to do that! + */ +public final class CloseScope implements Closeable { + private final Deque toClose = new ArrayDeque<>(); + + public T add(T value) { + toClose.addLast(value); + return value; + } + + @Override + public void close() throws IOException { + Throwable error = null; + + AutoCloseable next; + while ((next = toClose.pollLast()) != null) { + try { + next.close(); + } catch (Throwable e) { + if (error == null) { + error = e; + } else { + error.addSuppressed(e); + } + } + } + + if (error != null) CloseScope.throwUnchecked0(error); + } + + @SuppressWarnings("unchecked") + private static void throwUnchecked0(Throwable t) throws T { + throw (T) t; + } +} diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/PatchCobalt.java b/projects/web/src/builder/java/cc/tweaked/web/builder/PatchCobalt.java new file mode 100644 index 000000000..6b623f4a5 --- /dev/null +++ b/projects/web/src/builder/java/cc/tweaked/web/builder/PatchCobalt.java @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.builder; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import javax.annotation.Nullable; + +import static org.objectweb.asm.Opcodes.*; + +/** + * Patch Cobalt's {@code LuaState} and CC's {@code CobaltLuaMachine} to self-interrupt. + *

+ * In normal CC:T, computers are paused/interrupted asynchronously from the {@code ComputerThread}. However, as + * Javascript doesn't (easily) support multi-threaded code, we must find another option. + *

+ * Instead, we patch {@code LuaState.isInterrupted()} to periodically return true (every 1024 instructions), and then + * patch the interruption callback to refresh the timeout state. This means that the machine runs in very small time + * slices which, while quite slow, ensures we never block the UI thread for very long. + */ +public final class PatchCobalt { + private static final String LUA_STATE = "org/squiddev/cobalt/LuaState"; + private static final String COBALT_MACHINE = "dan200/computercraft/core/lua/CobaltLuaMachine"; + + private PatchCobalt() { + } + + public static ClassVisitor patch(String name, ClassVisitor visitor) { + return switch (name) { + case LUA_STATE -> patchLuaState(visitor); + case COBALT_MACHINE -> patchCobaltMachine(visitor); + default -> visitor; + }; + } + + /** + * Patch Cobalt's {@code LuaState.isInterrupted()} to periodically return true. + * + * @param cv The original class visitor. + * @return The transforming class visitor. + */ + private static ClassVisitor patchLuaState(ClassVisitor cv) { + return new ClassVisitor(Opcodes.ASM9, cv) { + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + super.visitField(ACC_PRIVATE, "$count", "I", null, null).visitEnd(); + } + + @Override + public @Nullable MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + var mv = super.visitMethod(access, name, descriptor, signature, exceptions); + if (mv == null) return null; + + if (name.equals("isInterrupted")) { + mv.visitCode(); + + // int x = $count + 1; + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, LUA_STATE, "$count", "I"); + mv.visitInsn(ICONST_1); + mv.visitInsn(IADD); + mv.visitLdcInsn(1023); + mv.visitInsn(IAND); + mv.visitVarInsn(ISTORE, 1); + // $count = x; + mv.visitVarInsn(ALOAD, 0); + mv.visitVarInsn(ILOAD, 1); + mv.visitFieldInsn(PUTFIELD, LUA_STATE, "$count", "I"); + // return x == 0; + var label = new Label(); + mv.visitVarInsn(ILOAD, 1); + mv.visitJumpInsn(IFNE, label); + mv.visitInsn(ICONST_1); + mv.visitInsn(IRETURN); + mv.visitLabel(label); + mv.visitInsn(ICONST_0); + mv.visitInsn(IRETURN); + + mv.visitMaxs(2, 2); + mv.visitEnd(); + + return null; + } + return mv; + } + }; + } + + /** + * Patch {@code CobaltLuaMachine} to call {@code TimeoutState.refresh()} at the head of the function. + * + * @param cv The original class visitor. + * @return The wrapped visitor. + */ + private static ClassVisitor patchCobaltMachine(ClassVisitor cv) { + return new ClassVisitor(ASM9, cv) { + private boolean visited = false; + + @Override + public @Nullable MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + var mv = super.visitMethod(access, name, descriptor, signature, exceptions); + if (mv == null) return null; + + if (name.startsWith("lambda$") && descriptor.equals("()Lorg/squiddev/cobalt/interrupt/InterruptAction;")) { + visited = true; + return new MethodVisitor(api, mv) { + @Override + public void visitCode() { + super.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, COBALT_MACHINE, "timeout", "Ldan200/computercraft/core/computer/TimeoutState;"); + mv.visitMethodInsn(INVOKEVIRTUAL, "dan200/computercraft/core/computer/TimeoutState", "refresh", "()V", false); + } + }; + } + + return mv; + } + + @Override + public void visitEnd() { + if (!visited) throw new IllegalStateException("Did not inject .refresh() into CobaltLuaMachine"); + super.visitEnd(); + } + }; + } +} diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/TransformingClassLoader.java b/projects/web/src/builder/java/cc/tweaked/web/builder/TransformingClassLoader.java new file mode 100644 index 000000000..fe2a0a00f --- /dev/null +++ b/projects/web/src/builder/java/cc/tweaked/web/builder/TransformingClassLoader.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.builder; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +/** + * A class loader which can {@linkplain #addTransformer(BiFunction) transform} and {@linkplain #remapClass(String, String) + * remap/rename} classes. + *

+ * When loading classes, this behaves much like {@link java.net.URLClassLoader}, loading files from a list of paths. + * However, when the TeaVM compiler requests the class bytes for compilation, we run the class through a list of + * transformers first, patching the classes to function in a Javascript environment. + */ +public class TransformingClassLoader extends ClassLoader { + private final Map remappedClasses = new HashMap<>(); + private final Map remappedResources = new HashMap<>(); + private final List> transformers = new ArrayList<>(); + + private final Remapper remapper = new Remapper() { + @Override + public String map(String internalName) { + return remappedClasses.getOrDefault(internalName, internalName); + } + }; + + private final List classpath; + + // Cache of the last transformed file - TeaVM tends to call getResourceAsStream multiple times. + private @Nullable TransformedClass lastFile; + + public TransformingClassLoader(List classpath) { + this.classpath = classpath; + + addTransformer((name, cv) -> new ClassRemapper(cv, remapper)); + } + + public void addTransformer(BiFunction transform) { + transformers.add(transform); + } + + public void remapClass(String from, String to, Path fromLocation) { + remappedClasses.put(from, to); + remappedResources.put(to + ".class", fromLocation); + } + + public void remapClass(String from, String to) { + remappedClasses.put(from, to); + } + + private @Nullable Path findUnmappedFile(String name) { + return classpath.stream().map(x -> x.resolve(name)).filter(Files::exists).findFirst().orElse(null); + } + + private @Nullable Path findFile(String name) { + var path = remappedResources.get(name); + return path != null ? path : findUnmappedFile(name); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + // findClass is only called at compile time, and so we load the original class, not the remapped one. + // Yes, this is super cursed. + var path = findUnmappedFile(name.replace('.', '/') + ".class"); + if (path == null) throw new ClassNotFoundException(); + + byte[] bytes; + try { + bytes = Files.readAllBytes(path); + } catch (IOException e) { + throw new ClassNotFoundException("Failed reading " + name, e); + } + + return defineClass(name, bytes, 0, bytes.length); + } + + @Override + public @Nullable InputStream getResourceAsStream(String name) { + if (!name.endsWith(".class")) return super.getResourceAsStream(name); + + var lastFile = this.lastFile; + if (lastFile != null && lastFile.name().equals(name)) return new ByteArrayInputStream(lastFile.contents()); + + var path = findFile(name); + if (path == null) return null; + + ClassReader reader; + try (var stream = Files.newInputStream(path)) { + reader = new ClassReader(stream); + } catch (IOException e) { + throw new UncheckedIOException("Failed reading " + name, e); + } + + var writer = new ClassWriter(reader, 0); + var className = reader.getClassName(); + ClassVisitor sink = writer; + for (var transformer : transformers) sink = transformer.apply(className, sink); + reader.accept(sink, 0); + + var bytes = writer.toByteArray(); + this.lastFile = new TransformedClass(name, bytes); + return new ByteArrayInputStream(bytes); + } + + @Override + protected @Nullable URL findResource(String name) { + var path = findFile(name); + return path == null ? null : toURL(path); + } + + @Override + protected Enumeration findResources(String name) { + var path = remappedResources.get(name); + return new IteratorEnumeration<>( + (path == null ? classpath.stream().map(x -> x.resolve(name)) : Stream.of(path)) + .filter(Files::exists) + .map(TransformingClassLoader::toURL) + .iterator() + ); + } + + @SuppressWarnings("JdkObsolete") + private record IteratorEnumeration(Iterator iterator) implements Enumeration { + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public T nextElement() { + return iterator.next(); + } + } + + private static URL toURL(Path path) { + try { + return path.toUri().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException("Cannot convert " + path + " to a URL", e); + } + } + + private record TransformedClass(String name, byte[] contents) { + } +} diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/package-info.java b/projects/web/src/builder/java/cc/tweaked/web/builder/package-info.java new file mode 100644 index 000000000..26eb421e6 --- /dev/null +++ b/projects/web/src/builder/java/cc/tweaked/web/builder/package-info.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +@DefaultQualifier(value = NonNull.class, locations = { + TypeUseLocation.RETURN, + TypeUseLocation.PARAMETER, + TypeUseLocation.FIELD, +}) +package cc.tweaked.web.builder; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; diff --git a/projects/web/src/frontend/emu/computer.ts b/projects/web/src/frontend/emu/computer.ts new file mode 100644 index 000000000..2e93fa362 --- /dev/null +++ b/projects/web/src/frontend/emu/computer.ts @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +import { type ComputerActionable, type KeyCode, type LuaValue, Semaphore, TerminalData, lwjgl3Code } from "@squid-dev/cc-web-term"; +import { type ComputerDisplay, type ComputerHandle, type PeripheralKind, type Side, start } from "./java"; + +const colours = "0123456789abcdef"; + +/** + * A reference to an emulated computer. + * + * This acts as a bridge between the Java-side computer and our Javascript code, + * including the terminal renderer and the main computer component. + */ +class EmulatedComputer implements ComputerDisplay, ComputerActionable { + public readonly terminal: TerminalData = new TerminalData(); + public readonly semaphore: Semaphore = new Semaphore(); + private readonly stateChanged: (label: string | null, on: boolean) => void; + + private label: string | null = null; + + private computer?: ComputerHandle; + private callbacks: Array<(cb: ComputerHandle) => void> = []; + private removed: boolean = false; + + public constructor(stateChange: (label: string | null, on: boolean) => void) { + this.stateChanged = stateChange; + this.label = null; + } + + public getLabel(): string | null { + return this.label; + } + + public setState(label: string | null, on: boolean): void { + if (this.label !== label) this.label = label; + this.stateChanged(label, on); + } + + public updateTerminal( + width: number, height: number, + x: number, y: number, blink: boolean, cursorColour: number, + ): void { + this.terminal.resize(width, height); + this.terminal.cursorX = x; + this.terminal.cursorY = y; + this.terminal.cursorBlink = blink; + this.terminal.currentFore = colours.charAt(cursorColour); + } + + public setTerminalLine(line: number, text: string, fore: string, back: string): void { + this.terminal.text[line] = text; + this.terminal.fore[line] = fore; + this.terminal.back[line] = back; + } + + public setPaletteColour(colour: number, r: number, g: number, b: number): void { + this.terminal.palette[colours.charAt(colour)] = + `rgb(${(r * 0xFF) & 0xFF},${(g * 0xFF) & 0xFF},${(b * 0xFF) & 0xFF})`; + } + + public flushTerminal(): void { + this.semaphore.signal(); + } + + public start(): void { + start(this) + .then(computer => { + this.computer = computer; + if (this.removed) computer.dispose(); + for (const callback of this.callbacks) callback(computer); + }) + .catch(e => { + const width = this.terminal.sizeX; + const fg = "0".repeat(width); + const bg = "e".repeat(width); + + const message = `${e}`.replace(/(?![^\n]{1,51}$)([^\n]{1,51})\s/, "$1\n").split("\n"); + for (let y = 0; y < this.terminal.sizeY; y++) { + const text = message[y] ?? ""; + this.terminal.text[y] = text.length > width ? text.substring(0, width) : text + " ".repeat(width - text.length); + this.terminal.fore[y] = fg; + this.terminal.back[y] = bg; + } + }); + } + + public queueEvent(event: string, args: Array): void { + if (this.computer !== undefined) this.computer.event(event, args); + } + + public keyDown(key: KeyCode, repeat: boolean): void { + const code = lwjgl3Code(key); + if (code !== undefined) this.queueEvent("key", [code, repeat]); + } + + public keyUp(key: KeyCode): void { + const code = lwjgl3Code(key); + if (code !== undefined) this.queueEvent("key_up", [code]); + } + + public turnOn(): void { + this.computer?.turnOn(); + } + + public shutdown(): void { + this.computer?.shutdown(); + } + + public reboot(): void { + this.computer?.reboot(); + } + + public dispose(): void { + this.removed = true; + this.computer?.dispose(); + } + + public transferFiles(files: Array<{ name: string, contents: ArrayBuffer }>): void { + this.computer?.transferFiles(files); + } + + public setPeripheral(side: Side, kind: PeripheralKind | null): void { + if (this.computer) { + this.computer.setPeripheral(side, kind); + } else { + this.callbacks.push(handler => handler.setPeripheral(side, kind)); + } + } + + public addFile(path: string, contents: string | ArrayBuffer): void { + if (this.computer) { + this.computer.addFile(path, contents); + } else { + this.callbacks.push(handler => handler.addFile(path, contents)); + } + } +} + +export default EmulatedComputer; diff --git a/projects/web/src/frontend/emu/index.tsx b/projects/web/src/frontend/emu/index.tsx new file mode 100644 index 000000000..c5c430b92 --- /dev/null +++ b/projects/web/src/frontend/emu/index.tsx @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +import { type FunctionalComponent, h } from "preact"; +import type { PeripheralKind, Side } from "./java"; +import { useEffect, useMemo, useState } from "preact/hooks"; +import { Terminal } from "@squid-dev/cc-web-term"; +import EmulatedComputer from "./computer"; +import termFont from "@squid-dev/cc-web-term/assets/term_font.png"; + +export type ComputerProps = { + files?: Record, + peripherals?: Partial>, +} + +/** + * Renders a computer in the world. + * + * @param props The properties for this component + * @returns The resulting JSX element. + */ +const Computer: FunctionalComponent = ({ files, peripherals }) => { + const [label, setLabel] = useState(null); + const [isOn, setOn] = useState(false); + const computer = useMemo(() => { + const computer = new EmulatedComputer((label, on) => { + setLabel(label); + setOn(on); + }); + for (const [side, peripheral] of Object.entries(peripherals ?? {})) { + computer.setPeripheral(side as Side, peripheral); + } + for (const [path, contents] of Object.entries(files ?? {})) { + computer.addFile(path, contents); + } + return computer; + }, [setLabel, setOn, files, peripherals]); + + useEffect(() => { + computer.start(); + return () => computer.dispose(); + }, [computer]); + + return ; +}; + +export default Computer; diff --git a/projects/web/src/frontend/emu/java.ts b/projects/web/src/frontend/emu/java.ts new file mode 100644 index 000000000..7ced944bb --- /dev/null +++ b/projects/web/src/frontend/emu/java.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +import "setimmediate"; + +import type { ComputerDisplay, ComputerHandle } from "cct/classes"; +export type { ComputerDisplay, ComputerHandle, PeripheralKind, Side } from "cct/classes"; + +const load = async (): Promise<(computer: ComputerDisplay) => ComputerHandle> => { + const [classes, { version, resources }] = await Promise.all([import("cct/classes"), import("cct/resources")]); + + let addComputer: ((computer: ComputerDisplay) => ComputerHandle) | null = null; + const encoder = new TextEncoder(); + window.$javaCallbacks = { + setup: add => addComputer = add, + modVersion: version, + listResources: () => Object.keys(resources), + getResource: path => new Int8Array(encoder.encode(resources[path])) + }; + classes.main(); + + if (!addComputer) throw new Error("Callbacks.setup was never called"); + return addComputer; +}; + +let addComputer: Promise<(computer: ComputerDisplay) => ComputerHandle> | null = null; + +/** + * Load our emulator and start a new computer. + * + * @param computer The display the computer's terminal should be drawn to. + * @returns The {@link ComputerHandle} for this computer. + */ +export const start = (computer: ComputerDisplay): Promise => { + if (addComputer == null) addComputer = load(); + return addComputer.then(f => f(computer)); +}; diff --git a/projects/web/src/frontend/index.tsx b/projects/web/src/frontend/index.tsx index ca1c1f446..71172b917 100644 --- a/projects/web/src/frontend/index.tsx +++ b/projects/web/src/frontend/index.tsx @@ -2,8 +2,7 @@ // // SPDX-License-Identifier: MPL-2.0 -import { Component, Computer, h, render, type PeripheralKind } from "copycat/embed"; -import type { ComponentChild, FunctionalComponent } from "preact"; +import { Component, type ComponentChild, type FunctionalComponent, h, render } from "preact"; import settingsFile from "./mount/.settings"; import exampleAudioUrl from "./mount/example.dfpwm"; @@ -12,6 +11,9 @@ import exampleNfp from "./mount/example.nfp"; import exampleNft from "./mount/example.nft"; import exprTemplate from "./mount/expr_template.lua"; import startupFile from "./mount/startup.lua"; +import type { PeripheralKind } from "./emu/java"; +import Computer from "./emu"; +import "./styles.css"; const defaultFiles: Record = { ".settings": settingsFile, @@ -25,24 +27,24 @@ const clamp = (value: number, min: number, max: number): number => { if (value < min) return min; if (value > max) return max; return value; -} +}; -const download = async (url: string): Promise => { +const download = async (url: string): Promise => { const result = await fetch(url); if (result.status != 200) throw new Error(`${url} responded with ${result.status} ${result.statusText}`); - return new Uint8Array(await result.arrayBuffer()); + return await result.arrayBuffer(); }; -let dfpwmAudio: Promise | null = null; +let dfpwmAudio: Promise | null = null; const Click: FunctionalComponent<{ run: () => void }> = ({ run }) => - + ; type WindowProps = {}; type Example = { - files: Record, + files: Record, peripheral: PeripheralKind | null, } @@ -58,7 +60,7 @@ class Window extends Component { private top: number = 0; private dragging?: { downX: number, downY: number, initialX: number, initialY: number }; - private snippets: { [file: string]: string } = {}; + private snippets: Record = {}; constructor(props: WindowProps, context: unknown) { super(props, context); @@ -67,10 +69,10 @@ class Window extends Component { visible: false, example: null, exampleIdx: 0, - } + }; } - componentDidMount() { + componentDidMount(): void { const elements = document.querySelectorAll("pre[data-lua-kind]"); for (let i = 0; i < elements.length; i++) { const element = elements[i] as HTMLElement; @@ -88,11 +90,11 @@ class Window extends Component { const mount = element.getAttribute("data-mount"); const peripheral = element.getAttribute("data-peripheral"); - render(, element.parentElement!!); + render(, element.parentElement!); } } - componentDidUpdate(_: WindowProps, { visible }: WindowState) { + componentDidUpdate(_: WindowProps, { visible }: WindowState): void { if (!visible && this.state.visible) this.setPosition(this.left, this.top); } @@ -120,7 +122,7 @@ class Window extends Component { this.top = 20; } - const files: Record = { "example.lua": example }; + const files: Record = { "example.lua": example }; if (mount !== null) { for (const toMount of mount.split(",")) { const [name, path] = toMount.split(":", 2); @@ -147,14 +149,14 @@ class Window extends Component { }, exampleIdx: exampleIdx + 1, })); - } + }; } - private readonly close = () => this.setState({ visible: false }); + private readonly close = (): void => this.setState({ visible: false }); // All the dragging code is terrible. However, I've had massive performance // issues doing it other ways, so this'll have to do. - private onDown(e: Event, touch: Touch) { + private onDown(e: Event, touch: Touch): void { e.stopPropagation(); e.preventDefault(); @@ -168,10 +170,10 @@ class Window extends Component { window.addEventListener("mouseup", this.onUp, true); window.addEventListener("touchend", this.onUp, true); } - private readonly onMouseDown = (e: MouseEvent) => this.onDown(e, e); - private readonly onTouchDown = (e: TouchEvent) => this.onDown(e, e.touches[0]); + private readonly onMouseDown = (e: MouseEvent): void => this.onDown(e, e); + private readonly onTouchDown = (e: TouchEvent): void => this.onDown(e, e.touches[0]); - private onDrag(e: Event, touch: Touch) { + private onDrag(e: Event, touch: Touch): void { e.stopPropagation(); e.preventDefault(); @@ -182,11 +184,11 @@ class Window extends Component { dragging.initialX + (touch.clientX - dragging.downX), dragging.initialY + (touch.clientY - dragging.downY), ); - }; - private readonly onMouseDrag = (e: MouseEvent) => this.onDrag(e, e); - private readonly onTouchDrag = (e: TouchEvent) => this.onDrag(e, e.touches[0]); + } + private readonly onMouseDrag = (e: MouseEvent): void => this.onDrag(e, e); + private readonly onTouchDrag = (e: TouchEvent): void => this.onDrag(e, e.touches[0]); - private readonly onUp = (e: Event) => { + private readonly onUp = (e: Event): void => { e.stopPropagation(); this.dragging = undefined; @@ -195,7 +197,7 @@ class Window extends Component { window.removeEventListener("touchmove", this.onTouchDrag, true); window.removeEventListener("mouseup", this.onUp, true); window.removeEventListener("touchend", this.onUp, true); - } + }; private readonly setPosition = (left: number, top: number): void => { const root = this.base as HTMLElement; @@ -203,10 +205,10 @@ class Window extends Component { left = this.left = clamp(left, 0, window.innerWidth - root.offsetWidth); top = this.top = clamp(top, 0, window.innerHeight - root.offsetHeight); root.style.transform = `translate(${left}px, ${top}px)`; - } + }; } const root = document.createElement("div"); document.body.appendChild(root); -render(, document.body, root); +render(, root); diff --git a/projects/web/src/frontend/typings.ts b/projects/web/src/frontend/typings.ts index 067e53211..eecd86f69 100644 --- a/projects/web/src/frontend/typings.ts +++ b/projects/web/src/frontend/typings.ts @@ -17,7 +17,6 @@ declare module "*.nft" { export default contents; } - declare module "*.settings" { const contents: string; export default contents; @@ -33,33 +32,147 @@ declare module "*.dfpwm" { export default contents; } +declare module "cct/resources" { + export const version: string; + export const resources: Record; +} -declare module "copycat/embed" { - import { h, Component, render, ComponentChild } from "preact"; +declare module "cct/classes" { + export const main: () => void; export type Side = "up" | "down" | "left" | "right" | "front" | "back"; export type PeripheralKind = "speaker"; - export { h, Component, render }; + /** + * Controls a specific computer on the Javascript side. See {@code js/ComputerAccess.java}. + */ + export interface ComputerDisplay { + /** + * Set this computer's current state + * + * @param label This computer's label + * @param on If this computer is on right now + */ + setState(label: string | null, on: boolean): void; - export type ComputerAccess = unknown; + /** + * Update the terminal's properties + * + * @param width The terminal width + * @param height The terminal height + * @param x The X cursor + * @param y The Y cursor + * @param blink Whether the cursor is blinking + * @param cursorColour The cursor's colour + */ + updateTerminal(width: number, height: number, x: number, y: number, blink: boolean, cursorColour: number): void; - export type MainProps = { - hdFont?: boolean | string, - persistId?: number, - files?: { [filename: string]: string | ArrayBuffer }, - label?: string, - width?: number, - height?: number, - resolve?: (computer: ComputerAccess) => void, - peripherals?: { - [side in Side]?: PeripheralKind | null - }, + /** + * Set a line on the terminal + * + * @param line The line index to set + * @param text The line's text + * @param fore The line's foreground + * @param back The line's background + */ + setTerminalLine(line: number, text: string, fore: string, back: string): void; + + /** + * Set the palette colour for a specific index + * + * @param colour The colour index to set + * @param r The red value, between 0 and 1 + * @param g The green value, between 0 and 1 + * @param b The blue value, between 0 and 1 + */ + setPaletteColour(colour: number, r: number, g: number, b: number): void; + + /** + * Mark the terminal as having changed. Should be called after all other terminal methods. + */ + flushTerminal(): void; } - class Computer extends Component { - public render(props: MainProps, state: unknown): ComponentChild; + export interface ComputerHandle { + /** + * Queue an event on the computer. + */ + event(event: string, args: Array | null): void; + + /** + * Shut the computer down. + */ + shutdown(): void; + + /** + * Turn the computer on. + */ + turnOn(): void; + + /** + * Reboot the computer. + */ + reboot(): void; + + /** + * Dispose of this computer, marking it as no longer running. + */ + dispose(): void; + + /** + * Transfer some files to this computer. + * + * @param files A list of files and their contents. + */ + transferFiles(files: Array<{ name: string, contents: ArrayBuffer }>): void; + + /** + * Set a peripheral on a particular side + * + * @param side The side to set the peripheral on. + * @param kind The kind of peripheral. For now, can only be "speaker". + */ + setPeripheral(side: Side, kind: PeripheralKind | null): void; + + /** + * Add a file to this computer's filesystem. + * @param path The path of the file. + * @param contents The path to the file. + */ + addFile(path: string, contents: string | ArrayBuffer): void; } - export { Computer }; + export interface Callbacks { + /** + * Get the current callback instance + * + * @param addComputer A computer to add a new computer. + */ + setup(addComputer: (computer: ComputerDisplay) => ComputerHandle): void; + + /** + * The version of CC: Tweaked currently loaded. + */ + modVersion: string; + + /** + * List all resources available in the ROM. + */ + listResources(): Array; + + /** + * Load a resource from the ROM. + * + * @param path The path to the resource to load. + */ + getResource(path: string): Int8Array; + } } + +declare namespace JSX { + export type Element = import("preact").JSX.Element; + export type IntrinsicElements = import("preact").JSX.IntrinsicElements; + export type ElementClass = import("preact").JSX.ElementClass; +} + +declare var $javaCallbacks: import("cct/classes").Callbacks; // eslint-disable-line no-var diff --git a/projects/web/src/htmlTransform/components/Recipe.tsx b/projects/web/src/htmlTransform/components/Recipe.tsx index 78c261013..ac36037f4 100644 --- a/projects/web/src/htmlTransform/components/Recipe.tsx +++ b/projects/web/src/htmlTransform/components/Recipe.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MPL-2.0 -import { h, type FunctionComponent, type JSX } from "preact"; +import { type FunctionComponent, type JSX, h } from "preact"; import useExport from "./WithExport"; const Item: FunctionComponent<{ item: string }> = ({ item }) => { @@ -14,12 +14,12 @@ const Item: FunctionComponent<{ item: string }> = ({ item }) => { alt={itemName} title={itemName} className="recipe-icon" - /> + />; }; const EmptyItem: FunctionComponent = () => ; -const Arrow: FunctionComponent = (props) => +const Arrow: FunctionComponent = props => {recipeInfo.count}} - + ; }; export default Recipe; diff --git a/projects/web/src/htmlTransform/components/WithExport.tsx b/projects/web/src/htmlTransform/components/WithExport.tsx index 937bd9799..6bd17cc36 100644 --- a/projects/web/src/htmlTransform/components/WithExport.tsx +++ b/projects/web/src/htmlTransform/components/WithExport.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MPL-2.0 -import { h, createContext, type FunctionComponent, type VNode } from "preact"; +import { type FunctionComponent, type VNode, createContext, h } from "preact"; import { useContext } from "preact/hooks"; export type DataExport = { @@ -21,7 +21,7 @@ const DataExport = createContext({ recipes: {}, }); -export const useExport = () => useContext(DataExport); +export const useExport = (): DataExport => useContext(DataExport); export default useExport; export const WithExport: FunctionComponent<{ data: DataExport, children: VNode }> = diff --git a/projects/web/src/htmlTransform/components/support.tsx b/projects/web/src/htmlTransform/components/support.tsx index a1197f6ee..9e41b67b0 100644 --- a/projects/web/src/htmlTransform/components/support.tsx +++ b/projects/web/src/htmlTransform/components/support.tsx @@ -26,4 +26,4 @@ export const noChildren = function (component: FunctionComponent): Functio }; wrapped.displayName = name; return wrapped; -} +}; diff --git a/projects/web/src/htmlTransform/index.tsx b/projects/web/src/htmlTransform/index.tsx index 0839207d9..5fec2cb44 100644 --- a/projects/web/src/htmlTransform/index.tsx +++ b/projects/web/src/htmlTransform/index.tsx @@ -13,17 +13,17 @@ import fs from "fs/promises"; import { glob } from "glob"; import path from "path"; -import { h, type JSX } from "preact"; +import { type JSX, h } from "preact"; import renderToStaticMarkup from "preact-render-to-string"; import * as runtime from "preact/jsx-runtime"; -import rehypeHighlight, { type Options as HighlightOptions } from "rehype-highlight"; +import rehypeHighlight from "rehype-highlight"; import rehypeParse from "rehype-parse"; import rehypeReact, { type Options as ReactOptions } from "rehype-react"; -import { unified, type Plugin } from "unified"; +import { unified } from "unified"; // Our components import Recipe from "./components/Recipe"; import { noChildren } from "./components/support"; -import { WithExport, type DataExport } from "./components/WithExport"; +import { type DataExport, WithExport } from "./components/WithExport"; (async () => { if (process.argv.length !== 5) { @@ -43,13 +43,13 @@ import { WithExport, type DataExport } from "./components/WithExport"; // Run button into them. ["pre"]: (args: JSX.IntrinsicElements["pre"] & { "data-lua-kind"?: undefined }) => { const element =

;
-                return args["data-lua-kind"] ? 
{element}
: element + return args["data-lua-kind"] ?
{element}
: element; } - } + } as any, }; const processor = unified() .use(rehypeParse, { emitParseErrors: true }) - .use(rehypeHighlight as unknown as Plugin<[HighlightOptions], import("hast").Root>, { prefix: "" }) + .use(rehypeHighlight, { prefix: "" }) .use(rehypeReact, reactOptions); const dataExport = JSON.parse(await fs.readFile(dataFile, "utf-8")) as DataExport; diff --git a/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java b/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java new file mode 100644 index 000000000..effde1d37 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/EmulatedComputer.java @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web; + +import cc.tweaked.web.js.ComputerDisplay; +import cc.tweaked.web.js.ComputerHandle; +import cc.tweaked.web.js.JavascriptConv; +import cc.tweaked.web.peripheral.SpeakerPeripheral; +import cc.tweaked.web.peripheral.TickablePeripheral; +import dan200.computercraft.api.filesystem.WritableMount; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.ComputerContext; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.core.apis.transfer.TransferredFile; +import dan200.computercraft.core.apis.transfer.TransferredFiles; +import dan200.computercraft.core.computer.Computer; +import dan200.computercraft.core.computer.ComputerEnvironment; +import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.filesystem.MemoryMount; +import dan200.computercraft.core.metrics.Metric; +import dan200.computercraft.core.metrics.MetricsObserver; +import dan200.computercraft.core.terminal.Terminal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSString; +import org.teavm.jso.typedarrays.ArrayBuffer; + +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Manages the core lifecycle of an emulated {@link Computer}. + *

+ * This is exposed to Javascript via the {@link ComputerHandle} interface. + */ +class EmulatedComputer implements ComputerEnvironment, ComputerHandle, MetricsObserver { + private static final Logger LOG = LoggerFactory.getLogger(EmulatedComputer.class); + + private static final ComputerSide[] SIDES = ComputerSide.values(); + private boolean terminalChanged = false; + private final Terminal terminal = new Terminal(51, 19, true, () -> terminalChanged = true); + private final Computer computer; + private final ComputerDisplay computerAccess; + private boolean disposed = false; + private final MemoryMount mount = new MemoryMount(); + + EmulatedComputer(ComputerContext context, ComputerDisplay computerAccess) { + this.computerAccess = computerAccess; + this.computer = new Computer(context, this, terminal, 0); + + if (!disposed) computer.turnOn(); + } + + /** + * Tick this computer. + * + * @return If this computer has been disposed of. + */ + public boolean tick() { + if (disposed && computer.isOn()) computer.unload(); + + try { + computer.tick(); + } catch (RuntimeException e) { + LOG.error("Error when ticking computer", e); + } + + if (computer.pollAndResetChanged()) { + computerAccess.setState(computer.getLabel(), computer.isOn()); + } + + for (var side : SIDES) { + var peripheral = computer.getEnvironment().getPeripheral(side); + if (peripheral instanceof TickablePeripheral toTick) toTick.tick(); + } + + if (terminalChanged) { + terminalChanged = false; + computerAccess.updateTerminal( + terminal.getWidth(), terminal.getHeight(), + terminal.getCursorX(), terminal.getCursorY(), + terminal.getCursorBlink(), terminal.getTextColour() + ); + + for (var i = 0; i < terminal.getHeight(); i++) { + computerAccess.setTerminalLine(i, + terminal.getLine(i).toString(), + terminal.getTextColourLine(i).toString(), + terminal.getBackgroundColourLine(i).toString() + ); + } + + var palette = terminal.getPalette(); + for (var i = 0; i < 16; i++) { + var colours = palette.getColour(i); + computerAccess.setPaletteColour(15 - i, colours[0], colours[1], colours[2]); + } + + computerAccess.flushTerminal(); + } + + return disposed && !computer.isOn(); + } + + @Override + public int getDay() { + return (int) ((Main.getTicks() + 6000) / 24000) + 1; + } + + @Override + public double getTimeOfDay() { + return ((Main.getTicks() + 6000) % 24000) / 1000.0; + } + + @Nullable + @Override + public WritableMount createRootMount() { + return mount; + } + + @Override + public MetricsObserver getMetrics() { + return this; + } + + @Override + public void observe(Metric.Counter counter) { + } + + @Override + public void observe(Metric.Event event, long value) { + } + + @Override + public void event(String event, @Nullable JSObject[] args) { + computer.queueEvent(event, JavascriptConv.toJava(args)); + } + + @Override + public void shutdown() { + computer.shutdown(); + } + + @Override + public void turnOn() { + computer.turnOn(); + } + + @Override + public void reboot() { + computer.reboot(); + } + + @Override + public void dispose() { + disposed = true; + } + + @Override + public void transferFiles(FileContents[] files) { + computer.queueEvent(TransferredFiles.EVENT, new Object[]{ + new TransferredFiles( + Arrays.stream(files) + .map(x -> new TransferredFile(x.getName(), new ArrayByteChannel(bytesOfBuffer(x.getContents())))) + .toList(), + () -> { + }), + }); + } + + @Override + public void setPeripheral(String sideName, @Nullable String kind) { + var side = ComputerSide.valueOfInsensitive(sideName); + if (side == null) throw new IllegalArgumentException("Unknown sideName"); + + IPeripheral peripheral; + if (kind == null) { + peripheral = null; + } else if (kind.equals("speaker")) { + peripheral = new SpeakerPeripheral(); + } else { + throw new IllegalArgumentException("Unknown peripheral kind"); + } + + computer.getEnvironment().setPeripheral(side, peripheral); + } + + @Override + public void addFile(String path, JSObject contents) { + byte[] bytes; + if (JavascriptConv.isArrayBuffer(contents)) { + bytes = bytesOfBuffer(contents.cast()); + } else { + JSString string = contents.cast(); + bytes = string.stringValue().getBytes(StandardCharsets.UTF_8); + } + + mount.addFile(path, bytes); + } + + private byte[] bytesOfBuffer(ArrayBuffer buffer) { + var oldBytes = JavascriptConv.asByteArray(buffer); + return Arrays.copyOf(oldBytes, oldBytes.length); + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/EmulatorEnvironment.java b/projects/web/src/main/java/cc/tweaked/web/EmulatorEnvironment.java new file mode 100644 index 000000000..7230cfa23 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/EmulatorEnvironment.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web; + +import cc.tweaked.web.js.Callbacks; +import dan200.computercraft.api.filesystem.Mount; +import dan200.computercraft.core.computer.GlobalEnvironment; + +import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * The {@link GlobalEnvironment} for all {@linkplain EmulatedComputer emulated computers}. This reads resources and + * version information from {@linkplain Callbacks the resources module}. + */ +final class EmulatorEnvironment implements GlobalEnvironment { + public static final EmulatorEnvironment INSTANCE = new EmulatorEnvironment(); + + private final String version = Callbacks.getModVersion(); + private @Nullable ResourceMount romMount; + private @Nullable byte[] bios; + + private EmulatorEnvironment() { + } + + @Override + public String getHostString() { + return "ComputerCraft " + version + " (tweaked.cc)"; + } + + @Override + public String getUserAgent() { + return "computercraft/" + version; + } + + @Override + public Mount createResourceMount(String domain, String subPath) { + if (domain.equals("computercraft") && subPath.equals("lua/rom")) { + return romMount != null ? romMount : (romMount = new ResourceMount()); + } else { + throw new IllegalArgumentException("Unknown domain or subpath"); + } + } + + @Override + public InputStream createResourceFile(String domain, String subPath) { + if (domain.equals("computercraft") && subPath.equals("lua/bios.lua")) { + var biosContents = bios != null ? bios : (bios = Callbacks.getResource("bios.lua")); + return new ByteArrayInputStream(biosContents); + } else { + throw new IllegalArgumentException("Unknown domain or subpath"); + } + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/Main.java b/projects/web/src/main/java/cc/tweaked/web/Main.java new file mode 100644 index 000000000..914ac2ab6 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/Main.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web; + +import cc.tweaked.web.js.Callbacks; +import dan200.computercraft.core.ComputerContext; +import org.teavm.jso.browser.Window; + +import java.util.ArrayList; +import java.util.List; + +/** + * The main entrypoint to the emulator. + */ +public class Main { + public static final String CORS_PROXY = "https://copy-cat-cors.vercel.app/?{}"; + + private static long ticks; + + public static void main(String[] args) { + var context = ComputerContext.builder(EmulatorEnvironment.INSTANCE).build(); + List computers = new ArrayList<>(); + + Callbacks.setup(access -> { + var wrapper = new EmulatedComputer(context, access); + computers.add(wrapper); + return wrapper; + }); + + Window.setInterval(() -> { + ticks++; + var iterator = computers.iterator(); + while (iterator.hasNext()) { + if (iterator.next().tick()) iterator.remove(); + } + }, 50); + } + + public static long getTicks() { + return ticks; + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/ResourceMount.java b/projects/web/src/main/java/cc/tweaked/web/ResourceMount.java new file mode 100644 index 000000000..b6877dd24 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/ResourceMount.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web; + +import cc.tweaked.web.js.Callbacks; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.core.filesystem.AbstractInMemoryMount; + +import javax.annotation.Nullable; +import java.nio.channels.SeekableByteChannel; + +/** + * Mounts in files from JavaScript-supplied resources. + * + * @see Callbacks#listResources() + * @see Callbacks#getResource(String) + */ +final class ResourceMount extends AbstractInMemoryMount { + private static final String PREFIX = "rom/"; + + ResourceMount() { + root = new FileEntry(""); + for (var file : Callbacks.listResources()) { + if (file.startsWith(PREFIX)) getOrCreateChild(root, file.substring(PREFIX.length()), FileEntry::new); + } + } + + @Override + protected long getSize(String path, FileEntry file) { + return file.isDirectory() ? 0 : getContents(file).length; + } + + @Override + protected SeekableByteChannel openForRead(String path, FileEntry file) { + return new ArrayByteChannel(getContents(file)); + } + + private byte[] getContents(FileEntry file) { + return file.contents != null ? file.contents : (file.contents = Callbacks.getResource(PREFIX + file.path)); + } + + protected static final class FileEntry extends AbstractInMemoryMount.FileEntry { + private final String path; + private @Nullable byte[] contents; + + FileEntry(String path) { + this.path = path; + } + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java b/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java new file mode 100644 index 000000000..5eb09c490 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/js/Callbacks.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.js; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSByRef; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.browser.TimerHandler; + +/** + * Invoke functions in the {@code $javaCallbacks} object. This global is set up by the Javascript code before the + * Java code is started. + *

+ * This module is a bit of a hack - we should be able to do most of this with module imports/exports. However, handling + * those within TeaVM is a bit awkward, so this ends up being much easier. + */ +public class Callbacks { + @JSFunctor + @FunctionalInterface + public interface AddComputer extends JSObject { + ComputerHandle addComputer(ComputerDisplay computer); + } + + /** + * Export the {@link AddComputer} function to the Javascript code. + * + * @param addComputer The function to add a computer. + */ + @JSBody(params = "setup", script = "return $javaCallbacks.setup(setup);") + public static native void setup(AddComputer addComputer); + + /** + * Get the version of CC: Tweaked. + * + * @return The mod's version. + */ + @JSBody(script = "return $javaCallbacks.modVersion;") + public static native String getModVersion(); + + /** + * List all resources available in the ROM. + * + * @return All available resources. + */ + @JSBody(script = "return $javaCallbacks.listResources();") + public static native String[] listResources(); + + /** + * Load a resource from the ROM. + * + * @param resource The path to the resource to load. + * @return The loaded resource. + */ + @JSByRef + @JSBody(params = "name", script = "return $javaCallbacks.getResource(name);") + public static native byte[] getResource(String resource); + + /** + * Call {@code setImmediate} (or rather a polyfill) to run an asynchronous task. + *

+ * While it would be nicer to use something built-in like {@code queueMicrotask}, our computer execution definitely + * doesn't count as a microtask, and doing so will stall the UI thread. + * + * @param task The task to run. + */ + @JSBody(params = "task", script = "return setImmediate(task);") + public static native void setImmediate(TimerHandler task); +} diff --git a/projects/web/src/main/java/cc/tweaked/web/js/ComputerDisplay.java b/projects/web/src/main/java/cc/tweaked/web/js/ComputerDisplay.java new file mode 100644 index 000000000..ecd5b9a2f --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/js/ComputerDisplay.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.js; + +import org.teavm.jso.JSObject; + +import javax.annotation.Nullable; + +/** + * The Javascript-side terminal which displays this computer. + * + * @see Callbacks.AddComputer#addComputer(ComputerDisplay) + */ +public interface ComputerDisplay extends JSObject { + /** + * Set this computer's current state. + * + * @param label This computer's label + * @param on If this computer is on right now + */ + void setState(@Nullable String label, boolean on); + + /** + * Update the terminal's properties. + * + * @param width The terminal width + * @param height The terminal height + * @param x The X cursor + * @param y The Y cursor + * @param blink Whether the cursor is blinking + * @param cursorColour The cursor's colour + */ + void updateTerminal(int width, int height, int x, int y, boolean blink, int cursorColour); + + /** + * Set a line on the terminal. + * + * @param line The line index to set + * @param text The line's text + * @param fore The line's foreground + * @param back The line's background + */ + void setTerminalLine(int line, String text, String fore, String back); + + /** + * Set the palette colour for a specific index. + * + * @param colour The colour index to set + * @param r The red value, between 0 and 1 + * @param g The green value, between 0 and 1 + * @param b The blue value, between 0 and 1 + */ + void setPaletteColour(int colour, double r, double g, double b); + + /** + * Mark the terminal as having changed. Should be called after all other terminal methods. + */ + void flushTerminal(); +} diff --git a/projects/web/src/main/java/cc/tweaked/web/js/ComputerHandle.java b/projects/web/src/main/java/cc/tweaked/web/js/ComputerHandle.java new file mode 100644 index 000000000..87fd462f7 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/js/ComputerHandle.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.js; + +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.core.JSString; +import org.teavm.jso.typedarrays.ArrayBuffer; + +import javax.annotation.Nullable; + +/** + * A Javascript-facing interface for controlling computers. + */ +public interface ComputerHandle extends JSObject { + /** + * Queue an event on the computer. + * + * @param event The name of the event. + * @param args The arguments for this event. + */ + void event(String event, @Nullable JSObject[] args); + + /** + * Shut the computer down. + */ + void shutdown(); + + /** + * Turn the computer on. + */ + void turnOn(); + + /** + * Reboot the computer. + */ + void reboot(); + + /** + * Dispose of this computer, marking it as no longer running. + */ + void dispose(); + + /** + * Transfer some files to this computer. + * + * @param files A list of files and their contents. + */ + void transferFiles(FileContents[] files); + + /** + * Set a peripheral on a particular side. + * + * @param side The side to set the peripheral on. + * @param kind The kind of peripheral. For now, can only be "speaker". + */ + void setPeripheral(String side, @Nullable String kind); + + /** + * Add a file to this computer's filesystem. + * + * @param path The path of the file. + * @param contents The contents of the file, either a {@link JSString} or {@link ArrayBuffer}. + */ + void addFile(String path, JSObject contents); + + /** + * A file to transfer to the computer. + */ + interface FileContents extends JSObject { + @JSProperty + String getName(); + + @JSProperty + ArrayBuffer getContents(); + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/js/Console.java b/projects/web/src/main/java/cc/tweaked/web/js/Console.java new file mode 100644 index 000000000..26fa945b1 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/js/Console.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.js; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; + +/** + * Wraps Javascript's {@code console} class. + */ +public final class Console { + private Console() { + } + + @JSBody(params = "x", script = "console.log(x);") + public static native void log(String message); + + @JSBody(params = "x", script = "console.warn(x);") + public static native void warn(String message); + + @JSBody(params = "x", script = "console.error(x);") + public static native void error(String message); + + @JSBody(params = "x", script = "console.error(x);") + public static native void error(JSObject object); +} diff --git a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java new file mode 100644 index 000000000..b1123bd6b --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.js; + +import org.jetbrains.annotations.Contract; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSByRef; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSBoolean; +import org.teavm.jso.core.JSNumber; +import org.teavm.jso.core.JSObjects; +import org.teavm.jso.core.JSString; +import org.teavm.jso.typedarrays.ArrayBuffer; +import org.teavm.jso.typedarrays.Int8Array; + +import javax.annotation.Nullable; + +/** + * Utility methods for converting between Java and Javascript representations. + */ +public class JavascriptConv { + /** + * Convert an array of Javascript values to an equivalent array of Java values. + * + * @param value The value to convert. + * @return The converted value. + */ + @Contract("null -> null; !null -> !null") + public static @Nullable Object[] toJava(@Nullable JSObject[] value) { + if (value == null) return null; + var out = new Object[value.length]; + for (var i = 0; i < value.length; i++) out[i] = toJava(value[i]); + return out; + } + + /** + * Convert a primitive Javascript value to a boxed Java object. + * + * @param value The value to convert. + * @return The converted value. + */ + public static @Nullable Object toJava(@Nullable JSObject value) { + if (value == null) return null; + return switch (JSObjects.typeOf(value)) { + case "string" -> ((JSString) value).stringValue(); + case "number" -> ((JSNumber) value).doubleValue(); + case "boolean" -> ((JSBoolean) value).booleanValue(); + default -> null; + }; + } + + /** + * Check if an arbitrary object is a {@link ArrayBuffer}. + * + * @param object The object ot check + * @return Whether this is an {@link ArrayBuffer}. + */ + @JSBody(params = "data", script = "return data instanceof ArrayBuffer;") + public static native boolean isArrayBuffer(JSObject object); + + /** + * Wrap a JS {@link Int8Array} into a {@code byte[]}. + * + * @param view The array to wrap. + * @return The wrapped array. + */ + @JSByRef + @JSBody(params = "x", script = "return x;") + public static native byte[] asByteArray(Int8Array view); + + /** + * Wrap a JS {@link ArrayBuffer} into a {@code byte[]}. + * + * @param view The array to wrap. + * @return The wrapped array. + */ + public static byte[] asByteArray(ArrayBuffer view) { + return asByteArray(Int8Array.create(view)); + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/package-info.java b/projects/web/src/main/java/cc/tweaked/web/package-info.java new file mode 100644 index 000000000..20cad057a --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/package-info.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +@DefaultQualifier(value = NonNull.class, locations = { + TypeUseLocation.RETURN, + TypeUseLocation.PARAMETER, + TypeUseLocation.FIELD, +}) +package cc.tweaked.web; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; diff --git a/projects/web/src/main/java/cc/tweaked/web/peripheral/AudioState.java b/projects/web/src/main/java/cc/tweaked/web/peripheral/AudioState.java new file mode 100644 index 000000000..9e3f5838c --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/peripheral/AudioState.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.peripheral; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaTable; +import org.teavm.jso.webaudio.AudioBuffer; +import org.teavm.jso.webaudio.AudioContext; + +import javax.annotation.Nullable; +import java.util.Optional; + +import static cc.tweaked.web.peripheral.SpeakerPeripheral.SAMPLE_RATE; + +final class AudioState { + /** + * The minimum size of the client's audio buffer. Once we have less than this on the client, we should send another + * batch of audio. + */ + private static final double CLIENT_BUFFER = 0.5; + + private final AudioContext audioContext; + private @Nullable AudioBuffer nextBuffer; + private double nextTime; + + AudioState(AudioContext audioContext) { + this.audioContext = audioContext; + nextTime = audioContext.getCurrentTime(); + } + + boolean pushBuffer(LuaTable table, int size, Optional volume) throws LuaException { + if (nextBuffer != null) return false; + + var buffer = nextBuffer = audioContext.createBuffer(1, size, SAMPLE_RATE); + var contents = buffer.getChannelData(0); + + for (var i = 0; i < size; i++) contents.set(i, table.getInt(i + 1) / 128.0f); + + // So we really should go via DFPWM here, but I do not have enough faith in our performance to do this properly. + + if (shouldSendPending()) playNext(); + return true; + } + + boolean isPlaying() { + return nextTime >= audioContext.getCurrentTime(); + } + + boolean shouldSendPending() { + return nextBuffer != null && audioContext.getCurrentTime() >= nextTime - CLIENT_BUFFER; + } + + void playNext() { + if (nextBuffer == null) throw new NullPointerException("Buffer is null"); + var source = audioContext.createBufferSource(); + source.setBuffer(nextBuffer); + source.connect(audioContext.getDestination()); + source.start(nextTime); + + nextTime += nextBuffer.getDuration(); + nextBuffer = null; + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java b/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java new file mode 100644 index 000000000..5faff07ee --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/peripheral/SpeakerPeripheral.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.peripheral; + +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaTable; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.teavm.jso.webaudio.AudioContext; + +import javax.annotation.Nullable; +import java.util.Optional; + +import static dan200.computercraft.api.lua.LuaValues.checkFinite; + +/** + * A minimal speaker peripheral, which implements {@code playAudio} and nothing else. + */ +public class SpeakerPeripheral implements TickablePeripheral { + public static final int SAMPLE_RATE = 48000; + + private static @MonotonicNonNull AudioContext audioContext; + + private @Nullable IComputerAccess computer; + + private @Nullable AudioState state; + + @Override + public String getType() { + return "speaker"; + } + + @Override + public void attach(IComputerAccess computer) { + this.computer = computer; + } + + @Override + public void detach(IComputerAccess computer) { + this.computer = null; + } + + @Override + public void tick() { + if (state != null && state.shouldSendPending()) { + state.playNext(); + if (computer != null) computer.queueEvent("speaker_audio_empty", computer.getAttachmentName()); + } + } + + @LuaFunction + public final boolean playNote(String instrumentA, Optional volumeA, Optional pitchA) throws LuaException { + throw new LuaException("Cannot play notes outside of Minecraft"); + } + + @LuaFunction + public final boolean playSound(String name, Optional volumeA, Optional pitchA) throws LuaException { + throw new LuaException("Cannot play sounds outside of Minecraft"); + } + + @LuaFunction(unsafe = true) + public final boolean playAudio(LuaTable audio, Optional volume) throws LuaException { + checkFinite(1, volume.orElse(0.0)); + + var length = audio.length(); + if (length <= 0) throw new LuaException("Cannot play empty audio"); + if (length > 128 * 1024) throw new LuaException("Audio data is too large"); + + if (audioContext == null) audioContext = AudioContext.create(); + if (state == null || !state.isPlaying()) state = new AudioState(audioContext); + + return state.pushBuffer(audio, length, volume); + } + + @LuaFunction + public final void stop() { + // TODO: Not sure how to do this. + } + + @Override + public boolean equals(@Nullable IPeripheral other) { + return other instanceof SpeakerPeripheral; + } +} diff --git a/projects/web/src/main/java/cc/tweaked/web/peripheral/TickablePeripheral.java b/projects/web/src/main/java/cc/tweaked/web/peripheral/TickablePeripheral.java new file mode 100644 index 000000000..7776d79a3 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/peripheral/TickablePeripheral.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.peripheral; + +import dan200.computercraft.api.peripheral.IPeripheral; + +/** + * A peripheral which will be updated every time the computer ticks. + */ +public interface TickablePeripheral extends IPeripheral { + void tick(); +} diff --git a/projects/web/src/main/java/cc/tweaked/web/stub/FileChannel.java b/projects/web/src/main/java/cc/tweaked/web/stub/FileChannel.java new file mode 100644 index 000000000..416125800 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/stub/FileChannel.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.stub; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; + +/** + * A stub for {@link java.nio.channels.FileChannel}. This is never constructed, only used in {@code instanceof} checks. + */ +public abstract class FileChannel implements SeekableByteChannel { + private FileChannel() { + } + + @Override + public abstract FileChannel position(long newPosition) throws IOException; + + public abstract void force(boolean metadata) throws IOException; + + public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException; +} diff --git a/projects/web/src/main/java/cc/tweaked/web/stub/ReentrantLock.java b/projects/web/src/main/java/cc/tweaked/web/stub/ReentrantLock.java new file mode 100644 index 000000000..ebe15a2f7 --- /dev/null +++ b/projects/web/src/main/java/cc/tweaked/web/stub/ReentrantLock.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.web.stub; + +/** + * A no-op stub for {@link java.util.concurrent.locks.ReentrantLock}. + */ +public class ReentrantLock { + public boolean tryLock() { + return true; + } + + public void unlock() { + } + + public void lockInterruptibly() throws InterruptedException { + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/TCoreConfig.java b/projects/web/src/main/java/dan200/computercraft/core/TCoreConfig.java new file mode 100644 index 000000000..28e8ad635 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/TCoreConfig.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core; + +import dan200.computercraft.core.apis.http.options.AddressRule; + +/** + * Replaces {@link CoreConfig} with a slightly cut-down version. + *

+ * This is mostly required to avoid pulling in {@link AddressRule}. + */ +public final class TCoreConfig { + private TCoreConfig() { + } + + public static int maximumFilesOpen = 128; + public static boolean disableLua51Features = false; + public static String defaultComputerSettings = ""; + + public static boolean httpEnabled = true; + public static boolean httpWebsocketEnabled = true; + public static int httpMaxRequests = 16; + public static int httpMaxWebsockets = 4; +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/TCheckUrl.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/TCheckUrl.java new file mode 100644 index 000000000..0d3fc1486 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/TCheckUrl.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.http; + +import dan200.computercraft.core.apis.IAPIEnvironment; + +import java.net.URI; + +/** + * Replaces {@link CheckUrl} with an implementation which unconditionally returns true. + */ +public class TCheckUrl extends Resource { + private static final String EVENT = "http_check"; + + private final IAPIEnvironment environment; + private final String address; + + public TCheckUrl(ResourceGroup limiter, IAPIEnvironment environment, String address, URI uri) { + super(limiter); + this.environment = environment; + this.address = address; + } + + public void run() { + if (isClosed()) return; + environment.queueEvent(EVENT, address, true); + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java new file mode 100644 index 000000000..27917c9f8 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/request/THttpRequest.java @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.http.request; + +import cc.tweaked.web.Main; +import cc.tweaked.web.js.JavascriptConv; +import dan200.computercraft.core.Logging; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.apis.handles.ArrayByteChannel; +import dan200.computercraft.core.apis.handles.BinaryReadableHandle; +import dan200.computercraft.core.apis.handles.EncodedReadableHandle; +import dan200.computercraft.core.apis.handles.HandleGeneric; +import dan200.computercraft.core.apis.http.HTTPRequestException; +import dan200.computercraft.core.apis.http.Resource; +import dan200.computercraft.core.apis.http.ResourceGroup; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.teavm.jso.ajax.XMLHttpRequest; +import org.teavm.jso.typedarrays.ArrayBuffer; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.StringReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.SeekableByteChannel; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Replaces {@link HttpRequest} with a version which uses AJAX/{@link XMLHttpRequest}. + */ +public class THttpRequest extends Resource { + private static final Logger LOG = LoggerFactory.getLogger(THttpRequest.class); + private static final String SUCCESS_EVENT = "http_success"; + private static final String FAILURE_EVENT = "http_failure"; + + private final IAPIEnvironment environment; + + private final String address; + private final @Nullable String postBuffer; + private final HttpHeaders headers; + private final boolean binary; + + public THttpRequest( + ResourceGroup limiter, IAPIEnvironment environment, String address, @Nullable String postText, + HttpHeaders headers, boolean binary, boolean followRedirects, int timeout + ) { + super(limiter); + this.environment = environment; + this.address = address; + postBuffer = postText; + this.headers = headers; + this.binary = binary; + + if (postText != null) { + if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { + headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8"); + } + } + } + + public static URI checkUri(String address) throws HTTPRequestException { + URI url; + try { + url = new URI(address); + } catch (URISyntaxException e) { + throw new HTTPRequestException("URL malformed"); + } + + checkUri(url); + return url; + } + + public static void checkUri(URI url) throws HTTPRequestException { + // Validate the URL + if (url.getScheme() == null) throw new HTTPRequestException("Must specify http or https"); + if (url.getHost() == null) throw new HTTPRequestException("URL malformed"); + + var scheme = url.getScheme().toLowerCase(Locale.ROOT); + if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { + throw new HTTPRequestException("Invalid protocol '" + scheme + "'"); + } + } + + public void request(URI uri, HttpMethod method) { + if (isClosed()) return; + + try { + var request = XMLHttpRequest.create(); + request.setOnReadyStateChange(() -> onResponseStateChange(request)); + request.setResponseType(binary ? "arraybuffer" : "text"); + var address = uri.toASCIIString(); + request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address)); + for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) { + var header = iterator.next(); + request.setRequestHeader(header.getKey(), header.getValue()); + } + request.send(postBuffer); + checkClosed(); + } catch (Exception e) { + failure("Could not connect"); + LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e); + } + } + + private void onResponseStateChange(XMLHttpRequest request) { + if (request.getReadyState() != XMLHttpRequest.DONE) return; + if (request.getStatus() == 0) { + this.failure("Could not connect"); + return; + } + + HandleGeneric reader; + if (binary) { + ArrayBuffer buffer = request.getResponse().cast(); + SeekableByteChannel contents = new ArrayByteChannel(JavascriptConv.asByteArray(buffer)); + reader = BinaryReadableHandle.of(contents); + } else { + reader = new EncodedReadableHandle(new BufferedReader(new StringReader(request.getResponseText()))); + } + + Map responseHeaders = new HashMap<>(); + for (var header : request.getAllResponseHeaders().split("\r\n")) { + var index = header.indexOf(':'); + if (index < 0) continue; + + // Normalise the header (so "content-type" becomes "Content-Type") + var upcase = true; + var headerBuilder = new StringBuilder(index); + for (var i = 0; i < index; i++) { + var c = header.charAt(i); + headerBuilder.append(upcase ? Character.toUpperCase(c) : c); + upcase = c == '-'; + } + responseHeaders.put(headerBuilder.toString(), header.substring(index + 1).trim()); + } + var stream = new HttpResponseHandle(reader, request.getStatus(), request.getStatusText(), responseHeaders); + + if (request.getStatus() >= 200 && request.getStatus() < 400) { + if (tryClose()) environment.queueEvent(SUCCESS_EVENT, address, stream); + } else { + if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, request.getStatusText(), stream); + } + } + + void failure(String message) { + if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message); + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java new file mode 100644 index 000000000..6e8c42123 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/apis/http/websocket/TWebsocket.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.apis.http.websocket; + +import cc.tweaked.web.js.Console; +import cc.tweaked.web.js.JavascriptConv; +import com.google.common.base.Strings; +import dan200.computercraft.core.apis.IAPIEnvironment; +import dan200.computercraft.core.apis.http.Resource; +import dan200.computercraft.core.apis.http.ResourceGroup; +import dan200.computercraft.core.apis.http.options.Action; +import dan200.computercraft.core.apis.http.options.Options; +import io.netty.handler.codec.http.HttpHeaders; +import org.teavm.jso.typedarrays.Int8Array; +import org.teavm.jso.websocket.WebSocket; + +import javax.annotation.Nullable; +import java.net.URI; +import java.nio.ByteBuffer; + +/** + * Replaces {@link Websocket} with a version which uses Javascript's built-in {@link WebSocket} client. + */ +public class TWebsocket extends Resource implements WebsocketClient { + private final IAPIEnvironment environment; + private final URI uri; + private final String address; + + private @Nullable WebSocket websocket; + + public TWebsocket(ResourceGroup limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) { + super(limiter); + this.environment = environment; + this.uri = uri; + this.address = address; + } + + public void connect() { + if (isClosed()) return; + + var client = this.websocket = WebSocket.create(uri.toASCIIString()); + client.setBinaryType("arraybuffer"); + client.onOpen(e -> success(Action.ALLOW.toPartial().toOptions())); + client.onError(e -> { + Console.error(e); + failure("Could not connect"); + }); + client.onMessage(e -> { + if (isClosed()) return; + if (JavascriptConv.isArrayBuffer(e.getData())) { + var array = Int8Array.create(e.getDataAsArray()); + var contents = new byte[array.getLength()]; + for (var i = 0; i < contents.length; i++) contents[i] = array.get(i); + environment.queueEvent("websocket_message", address, contents, true); + } else { + environment.queueEvent("websocket_message", address, e.getDataAsString(), false); + } + }); + client.onClose(e -> close(e.getCode(), e.getReason())); + } + + @Override + public void sendText(String message) { + if (websocket == null) return; + websocket.send(message); + } + + @Override + public void sendBinary(ByteBuffer message) { + if (websocket == null) return; + + var array = Int8Array.create(message.remaining()); + for (var i = 0; i < array.getLength(); i++) array.set(i, message.get(i)); + websocket.send(array); + } + + @Override + protected void dispose() { + super.dispose(); + if (websocket != null) { + websocket.close(); + websocket = null; + } + } + + private void success(Options options) { + if (isClosed()) return; + + var handle = new WebsocketHandle(environment, address, this, options); + environment.queueEvent(SUCCESS_EVENT, address, handle); + createOwnerReference(handle); + + checkClosed(); + } + + void failure(String message) { + if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message); + } + + void close(int status, String reason) { + if (!tryClose()) return; + + environment.queueEvent(CLOSE_EVENT, address, Strings.isNullOrEmpty(reason) ? null : reason, status < 0 ? null : status); + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/asm/MethodReflection.java b/projects/web/src/main/java/dan200/computercraft/core/asm/MethodReflection.java new file mode 100644 index 000000000..11203e3d8 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/asm/MethodReflection.java @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.PeripheralType; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.NamedMethod; +import org.teavm.metaprogramming.*; + +import javax.annotation.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +/** + * Compile-time generation of {@link LuaMethod} methods. + * + * @see TLuaMethodSupplier + * @see StaticGenerator + */ +@CompileTime +public class MethodReflection { + @Meta + public static native boolean getMethods(Class type, Consumer> make); + + private static void getMethods(ReflectClass klass, Value>> make) { + var result = getMethodsImpl(klass, make); + // Using "unsupportedCase" here causes us to skip generating any code and just return null. While null isn't + // a boolean, it's still false-y and thus has the same effect in the generated JS! + if (!result) Metaprogramming.unsupportedCase(); + Metaprogramming.exit(() -> result); + } + + private static boolean getMethodsImpl(ReflectClass klass, Value>> make) { + if (!klass.getName().startsWith("dan200.computercraft.") && !klass.getName().startsWith("cc.tweaked.web.peripheral")) { + return false; + } + if (klass.getName().contains("lambda")) return false; + + Class actualClass; + try { + actualClass = Metaprogramming.getClassLoader().loadClass(klass.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + var methods = Internal.getMethods(actualClass); + for (var method : methods) { + var name = method.name(); + var nonYielding = method.nonYielding(); + var actualField = method.method().getField("INSTANCE"); + + Metaprogramming.emit(() -> make.get().accept(new NamedMethod<>(name, (LuaMethod) actualField.get(null), nonYielding, null))); + } + + return !methods.isEmpty(); + } + + private static final class Internal { + private static final LoadingCache, List>>> CLASS_CACHE = CacheBuilder + .newBuilder() + .build(CacheLoader.from(Internal::getMethodsImpl)); + + private static final StaticGenerator GENERATOR = new StaticGenerator<>( + LuaMethod.class, Collections.singletonList(ILuaContext.class), Internal::createClass + ); + + static List>> getMethods(Class klass) { + try { + return CLASS_CACHE.get(klass); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static ReflectClass createClass(byte[] bytes) { + /* + StaticGenerator is not declared to be @CompileTime, to ensure it loads in the same module/classloader as + other files in this package. This means it can't call Metaprogramming.createClass directly, as that's + only available to @CompileTime classes. + + We need to use an explicit call (rather than a MethodReference), as TeaVM doesn't correctly rewrite the + latter. + */ + return Metaprogramming.createClass(bytes); + } + + private static List>> getMethodsImpl(Class klass) { + ArrayList>> methods = null; + + // Find all methods on the current class + for (var method : klass.getMethods()) { + var annotation = method.getAnnotation(LuaFunction.class); + if (annotation == null) continue; + + if (Modifier.isStatic(method.getModifiers())) { + System.err.printf("LuaFunction method %s.%s should be an instance method.\n", method.getDeclaringClass(), method.getName()); + continue; + } + + var instance = GENERATOR.getMethod(method).orElse(null); + if (instance == null) continue; + + if (methods == null) methods = new ArrayList<>(); + addMethod(methods, method, annotation, null, instance); + } + + if (methods == null) return List.of(); + methods.trimToSize(); + return Collections.unmodifiableList(methods); + } + + private static void addMethod(List>> methods, Method method, LuaFunction annotation, @Nullable PeripheralType genericType, ReflectClass instance) { + var names = annotation.value(); + var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); + if (names.length == 0) { + methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType)); + } else { + for (var name : names) { + methods.add(new NamedMethod<>(name, instance, isSimple, genericType)); + } + } + } + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java b/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java new file mode 100644 index 000000000..aa95f2d27 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/asm/StaticGenerator.java @@ -0,0 +1,313 @@ +// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.methods.LuaMethod; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.teavm.metaprogramming.ReflectClass; + +import javax.annotation.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import static org.objectweb.asm.Opcodes.*; + +/** + * The underlying generator for {@link LuaFunction}-annotated methods. + *

+ * The constructor {@link StaticGenerator#StaticGenerator(Class, List, Function)} takes in the type of interface to generate (i.e. + * {@link LuaMethod}) and the context arguments for this function (in the case of {@link LuaMethod}, this will just be + * {@link ILuaContext}). + *

+ * The generated class then implements this interface - the {@code apply} method calls the appropriate methods on + * {@link IArguments} to extract the arguments, and then calls the original method. + * + * @param The type of the interface the generated classes implement. + */ +public final class StaticGenerator { + private static final String METHOD_NAME = "apply"; + private static final String[] EXCEPTIONS = new String[]{Type.getInternalName(LuaException.class)}; + + private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(MethodResult.class); + private static final String DESC_METHOD_RESULT = Type.getDescriptor(MethodResult.class); + + private static final String INTERNAL_ARGUMENTS = Type.getInternalName(IArguments.class); + private static final String DESC_ARGUMENTS = Type.getDescriptor(IArguments.class); + + private static final String INTERNAL_COERCED = Type.getInternalName(Coerced.class); + + private final Class base; + private final List> context; + + private final String[] interfaces; + private final String methodDesc; + private final String classPrefix; + + private final Function> createClass; + + private final LoadingCache>> methodCache = CacheBuilder + .newBuilder() + .build(CacheLoader.from(catching(this::build, Optional.empty()))); + + public StaticGenerator(Class base, List> context, Function> createClass) { + this.base = base; + this.context = context; + this.createClass = createClass; + + interfaces = new String[]{Type.getInternalName(base)}; + + var methodDesc = new StringBuilder().append("(Ljava/lang/Object;"); + for (var klass : context) methodDesc.append(Type.getDescriptor(klass)); + methodDesc.append(DESC_ARGUMENTS).append(")").append(DESC_METHOD_RESULT); + this.methodDesc = methodDesc.toString(); + + classPrefix = StaticGenerator.class.getPackageName() + "." + base.getSimpleName() + "$"; + } + + public Optional> getMethod(Method method) { + return methodCache.getUnchecked(method); + } + + private Optional> build(Method method) { + var name = method.getDeclaringClass().getName() + "." + method.getName(); + var modifiers = method.getModifiers(); + + // Instance methods must be final - this prevents them being overridden and potentially exposed twice. + if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) { + System.err.printf("Lua Method %s should be final.\n", name); + } + + if (!Modifier.isPublic(modifiers)) { + System.err.printf("Lua Method %s should be a public method.\n", name); + return Optional.empty(); + } + + if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + System.err.printf("Lua Method %s should be on a public class.\n", name); + return Optional.empty(); + } + + var exceptions = method.getExceptionTypes(); + for (var exception : exceptions) { + if (exception != LuaException.class) { + System.err.printf("Lua Method %s cannot throw %s.\n", name, exception.getName()); + return Optional.empty(); + } + } + + var annotation = method.getAnnotation(LuaFunction.class); + if (annotation.unsafe() && annotation.mainThread()) { + System.err.printf("Lua Method %s cannot use unsafe and mainThread.\n", name); + return Optional.empty(); + } + + // We have some rather ugly handling of static methods in both here and the main generate function. Static methods + // only come from generic sources, so this should be safe. + var target = Modifier.isStatic(modifiers) ? method.getParameterTypes()[0] : method.getDeclaringClass(); + + try { + var bytes = generate(classPrefix + method.getDeclaringClass().getSimpleName() + "$" + method.getName(), target, method, annotation.unsafe()); + if (bytes == null) return Optional.empty(); + + return Optional.of(createClass.apply(bytes).asSubclass(base)); + } catch (ClassFormatError | RuntimeException e) { + System.err.printf("Error generating %s\n", name); + e.printStackTrace(); + return Optional.empty(); + } + } + + @Nullable + private byte[] generate(String className, Class target, Method targetMethod, boolean unsafe) { + var internalName = className.replace(".", "/"); + + // Construct a public final class which extends Object and implements MethodInstance.Delegate + var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + cw.visit(V17, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces); + cw.visitSource("CC generated method", null); + + cw.visitField(ACC_PUBLIC | ACC_STATIC | ACC_FINAL, "INSTANCE", "L" + internalName + ";", null, null).visitEnd(); + + { // Constructor just invokes super. + var mw = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mw.visitCode(); + mw.visitVarInsn(ALOAD, 0); + mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mw.visitInsn(RETURN); + mw.visitMaxs(0, 0); + mw.visitEnd(); + } + + // Static initialiser sets the INSTANCE field. + { + var mw = cw.visitMethod(ACC_STATIC, "", "()V", null, null); + mw.visitCode(); + mw.visitTypeInsn(NEW, internalName); + mw.visitInsn(DUP); + mw.visitMethodInsn(INVOKESPECIAL, internalName, "", "()V", false); + mw.visitFieldInsn(PUTSTATIC, internalName, "INSTANCE", "L" + internalName + ";"); + mw.visitInsn(RETURN); + mw.visitMaxs(0, 0); + mw.visitEnd(); + } + + { + var mw = cw.visitMethod(ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS); + mw.visitCode(); + + // If we're an instance method, load the target as the first argument. + if (!Modifier.isStatic(targetMethod.getModifiers())) { + mw.visitVarInsn(ALOAD, 1); + mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target)); + } + + var argIndex = 0; + for (var genericArg : targetMethod.getGenericParameterTypes()) { + var loadedArg = loadArg(mw, target, targetMethod, unsafe, genericArg, argIndex); + if (loadedArg == null) return null; + if (loadedArg) argIndex++; + } + + mw.visitMethodInsn( + Modifier.isStatic(targetMethod.getModifiers()) ? INVOKESTATIC : INVOKEVIRTUAL, + Type.getInternalName(targetMethod.getDeclaringClass()), targetMethod.getName(), + Type.getMethodDescriptor(targetMethod), false + ); + + // We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult, + // we convert basic types into an immediate result. + var ret = targetMethod.getReturnType(); + if (ret != MethodResult.class) { + if (ret == void.class) { + mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false); + } else if (ret.isPrimitive()) { + var boxed = Primitives.wrap(ret); + mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(boxed), "valueOf", "(" + Type.getDescriptor(ret) + ")" + Type.getDescriptor(boxed), false); + mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false); + } else if (ret == Object[].class) { + mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "([Ljava/lang/Object;)" + DESC_METHOD_RESULT, false); + } else { + mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false); + } + } + + mw.visitInsn(ARETURN); + + mw.visitMaxs(0, 0); + mw.visitEnd(); + } + + cw.visitEnd(); + + return cw.toByteArray(); + } + + @Nullable + private Boolean loadArg(MethodVisitor mw, Class target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex) { + if (genericArg == target) { + mw.visitVarInsn(ALOAD, 1); + mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target)); + return false; + } + + var arg = Reflect.getRawType(method, genericArg, true); + if (arg == null) return null; + + if (arg == IArguments.class) { + mw.visitVarInsn(ALOAD, 2 + context.size()); + return false; + } + + var idx = context.indexOf(arg); + if (idx >= 0) { + mw.visitVarInsn(ALOAD, 2 + idx); + return false; + } + + if (arg == Coerced.class) { + var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.COERCED_IN).getType(), false); + if (klass == null) return null; + + if (klass == String.class) { + mw.visitTypeInsn(NEW, INTERNAL_COERCED); + mw.visitInsn(DUP); + mw.visitVarInsn(ALOAD, 2 + context.size()); + Reflect.loadInt(mw, argIndex); + mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;", true); + mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "", "(Ljava/lang/Object;)V", false); + return true; + } + } + + if (arg == Optional.class) { + var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false); + if (klass == null) return null; + + if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) { + mw.visitVarInsn(ALOAD, 2 + context.size()); + Reflect.loadInt(mw, argIndex); + mw.visitLdcInsn(Type.getType(klass)); + mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true); + return true; + } + + var name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe); + if (name != null) { + mw.visitVarInsn(ALOAD, 2 + context.size()); + Reflect.loadInt(mw, argIndex); + mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true); + return true; + } + } + + if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) { + mw.visitVarInsn(ALOAD, 2 + context.size()); + Reflect.loadInt(mw, argIndex); + mw.visitLdcInsn(Type.getType(arg)); + mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true); + mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg)); + return true; + } + + var name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe); + if (name != null) { + if (Reflect.getRawType(method, genericArg, false) == null) return null; + + mw.visitVarInsn(ALOAD, 2 + context.size()); + Reflect.loadInt(mw, argIndex); + mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg), true); + return true; + } + + System.err.printf("Unknown parameter type %s for method %s.%s.\n", + arg.getName(), method.getDeclaringClass().getName(), method.getName()); + return null; + } + + @SuppressWarnings("Guava") + static com.google.common.base.Function catching(Function function, U def) { + return x -> { + try { + return function.apply(x); + } catch (Exception | LinkageError e) { + // LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching + // methods on a class which references non-existent (i.e. client-only) types. + e.printStackTrace(); + return def; + } + }; + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/asm/TLuaMethodSupplier.java b/projects/web/src/main/java/dan200/computercraft/core/asm/TLuaMethodSupplier.java new file mode 100644 index 000000000..cf1ac1157 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/asm/TLuaMethodSupplier.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.ObjectSource; + +import java.util.List; + +/** + * Replaces {@link LuaMethodSupplier} with a version which uses {@link MethodReflection} to fabricate the classes. + */ +public final class TLuaMethodSupplier implements MethodSupplier { + static final TLuaMethodSupplier INSTANCE = new TLuaMethodSupplier(); + + private TLuaMethodSupplier() { + } + + @Override + public boolean forEachSelfMethod(Object object, UntargetedConsumer consumer) { + return MethodReflection.getMethods(object.getClass(), method -> consumer.accept(method.name(), method.method(), method)); + } + + @Override + public boolean forEachMethod(Object object, TargetedConsumer consumer) { + var hasMethods = MethodReflection.getMethods(object.getClass(), method -> consumer.accept(object, method.name(), method.method(), method)); + + if (object instanceof ObjectSource source) { + for (var extra : source.getExtra()) { + hasMethods |= MethodReflection.getMethods(extra.getClass(), method -> consumer.accept(extra, method.name(), method.method(), method)); + } + } + + return hasMethods; + } + + public static MethodSupplier create(List genericMethods) { + return INSTANCE; + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/asm/TPeripheralMethodSupplier.java b/projects/web/src/main/java/dan200/computercraft/core/asm/TPeripheralMethodSupplier.java new file mode 100644 index 000000000..e15bddc50 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/asm/TPeripheralMethodSupplier.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.core.methods.LuaMethod; +import dan200.computercraft.core.methods.MethodSupplier; +import dan200.computercraft.core.methods.PeripheralMethod; + +import java.util.List; + +/** + * Replaces {@link PeripheralMethodSupplier} with a version which lifts {@link LuaMethod}s to {@link PeripheralMethod}. + * As none of our peripherals need {@link IComputerAccess}, this is entirely safe. + */ +public final class TPeripheralMethodSupplier implements MethodSupplier { + static final TPeripheralMethodSupplier INSTANCE = new TPeripheralMethodSupplier(); + + private TPeripheralMethodSupplier() { + } + + @Override + public boolean forEachSelfMethod(Object object, UntargetedConsumer consumer) { + return TLuaMethodSupplier.INSTANCE.forEachSelfMethod(object, (name, method, info) -> consumer.accept(name, cast(method), null)); + } + + @Override + public boolean forEachMethod(Object object, TargetedConsumer consumer) { + return TLuaMethodSupplier.INSTANCE.forEachMethod(object, (target, name, method, info) -> consumer.accept(target, name, cast(method), null)); + } + + private static PeripheralMethod cast(LuaMethod method) { + return (target, context, computer, args) -> method.apply(target, context, args); + } + + public static MethodSupplier create(List genericMethods) { + return INSTANCE; + } +} diff --git a/projects/web/src/main/java/dan200/computercraft/core/computer/TComputerThread.java b/projects/web/src/main/java/dan200/computercraft/core/computer/TComputerThread.java new file mode 100644 index 000000000..5c6875dd0 --- /dev/null +++ b/projects/web/src/main/java/dan200/computercraft/core/computer/TComputerThread.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.computer; + +import cc.tweaked.web.js.Callbacks; +import org.teavm.jso.browser.TimerHandler; + +import java.util.ArrayDeque; +import java.util.concurrent.TimeUnit; + +/** + * A reimplementation of {@link ComputerThread} which, well, avoids any threading! + *

+ * This instead just exucutes work as soon as possible via {@link Callbacks#setImmediate(TimerHandler)}. Timeouts are + * instead handled via polling, see {@link cc.tweaked.web.builder.PatchCobalt}. + */ +public class TComputerThread { + private static final ArrayDeque executors = new ArrayDeque<>(); + private final TimerHandler callback = this::workOnce; + + public TComputerThread(int threads) { + } + + public void queue(ComputerExecutor executor) { + if (executor.onComputerQueue) throw new IllegalStateException("Cannot queue already queued executor"); + executor.onComputerQueue = true; + + if (executors.isEmpty()) Callbacks.setImmediate(callback); + executors.add(executor); + } + + private void workOnce() { + var executor = executors.poll(); + if (executor == null) throw new IllegalStateException("Working, but executor is null"); + if (!executor.onComputerQueue) throw new IllegalArgumentException("Working but not on queue"); + + executor.beforeWork(); + try { + executor.work(); + } catch (Exception e) { + e.printStackTrace(); + } + + if (executor.afterWork()) executors.push(executor); + if (!executors.isEmpty()) Callbacks.setImmediate(callback); + } + + public boolean hasPendingWork() { + return true; + } + + public long scaledPeriod() { + return 50 * 1_000_000L; + } + + public boolean stop(long timeout, TimeUnit unit) { + return true; + } +} diff --git a/projects/web/src/main/java/io/netty/handler/codec/http/TDefaultHttpHeaders.java b/projects/web/src/main/java/io/netty/handler/codec/http/TDefaultHttpHeaders.java new file mode 100644 index 000000000..ec4749606 --- /dev/null +++ b/projects/web/src/main/java/io/netty/handler/codec/http/TDefaultHttpHeaders.java @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package io.netty.handler.codec.http; + +import java.util.*; + +/** + * A replacement for {@link DefaultHttpHeaders}. + *

+ * The default implementation does additional conversion for dates, which ends up pulling in a lot of code we can't + * compile. + */ +public class TDefaultHttpHeaders extends HttpHeaders { + private final Map map = new HashMap<>(); + + @Override + public String get(String name) { + return map.get(normalise(name)); + } + + @Override + public List getAll(String name) { + var value = get(name); + return value == null ? List.of() : List.of(value); + } + + @Override + public List> entries() { + return List.copyOf(map.entrySet()); + } + + @Override + public boolean contains(String name) { + return get(name) != null; + } + + @Override + @Deprecated + public Iterator> iterator() { + return map.entrySet().iterator(); + } + + @Override + @SuppressWarnings("unchecked") + public Iterator> iteratorCharSequence() { + return (Iterator>) (Iterator) map.entrySet().iterator(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public Set names() { + return map.keySet(); + } + + @Override + public HttpHeaders add(String name, Object value) { + return set(name, value); + } + + @Override + public HttpHeaders set(String name, Object value) { + map.put(normalise(name), (String) value); + return this; + } + + @Override + public HttpHeaders remove(String name) { + map.remove(normalise(name)); + return this; + } + + @Override + public HttpHeaders clear() { + map.clear(); + return this; + } + + //region Uncalled/unsupported methods + @Override + public Integer getInt(CharSequence name) { + throw new UnsupportedOperationException(); + } + + @Override + public int getInt(CharSequence name, int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public Short getShort(CharSequence name) { + throw new UnsupportedOperationException(); + } + + @Override + public short getShort(CharSequence name, short defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public Long getTimeMillis(CharSequence name) { + throw new UnsupportedOperationException(); + } + + @Override + public long getTimeMillis(CharSequence name, long defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders add(String name, Iterable values) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders addInt(CharSequence name, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders addShort(CharSequence name, short value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders set(String name, Iterable values) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders setInt(CharSequence name, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders setShort(CharSequence name, short value) { + throw new UnsupportedOperationException(); + } + //endregion + + private static String normalise(String string) { + return string.toLowerCase(Locale.ROOT); + } +} diff --git a/projects/web/src/main/java/io/netty/util/TAsciiString.java b/projects/web/src/main/java/io/netty/util/TAsciiString.java new file mode 100644 index 000000000..80c0b7f64 --- /dev/null +++ b/projects/web/src/main/java/io/netty/util/TAsciiString.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package io.netty.util; + +import org.teavm.interop.NoSideEffects; + +/** + * A replacement for {@link AsciiString} which just wraps a normal string. + *

+ * {@link AsciiString} relies heavily on Netty's low-level (and often unsafe!) code, which doesn't run on Javascript. + */ +public final class TAsciiString implements CharSequence { + private final String value; + + private TAsciiString(String value) { + this.value = value; + } + + @NoSideEffects + public static TAsciiString cached(String value) { + return new TAsciiString(value); + } + + @Override + public int length() { + return value.length(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return value.subSequence(start, end); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof TAsciiString other && value.equals(other.value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/projects/web/src/main/java/org/slf4j/TLoggerFactory.java b/projects/web/src/main/java/org/slf4j/TLoggerFactory.java new file mode 100644 index 000000000..7bfe8ef85 --- /dev/null +++ b/projects/web/src/main/java/org/slf4j/TLoggerFactory.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package org.slf4j; + +import cc.tweaked.web.js.Console; +import org.slf4j.event.Level; +import org.slf4j.helpers.AbstractLogger; + +import java.io.Serial; +import java.util.Arrays; + +/** + * A replacement for SLF4J's {@link LoggerFactory}, which skips service loading, returning a logger which prints to the + * JS console. + */ +public final class TLoggerFactory { + private static final Logger INSTANCE = new LoggerImpl(); + + private TLoggerFactory() { + } + + public static Logger getLogger(Class klass) { + return INSTANCE; + } + + private static final class LoggerImpl extends AbstractLogger { + @Serial + private static final long serialVersionUID = 3442920913507872371L; + + @Override + protected String getFullyQualifiedCallerName() { + return "logger"; + } + + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String msg, Object[] arguments, Throwable throwable) { + if (arguments != null) msg += " " + Arrays.toString(arguments); + switch (level) { + case TRACE, DEBUG, INFO -> Console.log(msg); + case WARN -> Console.warn(msg); + case ERROR -> Console.error(msg); + } + + if (throwable != null) throwable.printStackTrace(); + } + + @Override + public boolean isTraceEnabled() { + return true; + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return true; + } + + @Override + public boolean isDebugEnabled() { + return true; + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return true; + } + + @Override + public boolean isInfoEnabled() { + return true; + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return true; + } + + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return true; + } + + @Override + public boolean isErrorEnabled() { + return true; + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return true; + } + } +} diff --git a/projects/web/src/main/java/org/slf4j/TMarkerFactory.java b/projects/web/src/main/java/org/slf4j/TMarkerFactory.java new file mode 100644 index 000000000..efab8fc52 --- /dev/null +++ b/projects/web/src/main/java/org/slf4j/TMarkerFactory.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package org.slf4j; + +import java.io.Serial; +import java.util.Collections; +import java.util.Iterator; + +/** + * A replacement for SLF4J's {@link MarkerFactory}, which skips service loading and always uses a constant + * {@link Marker}. + */ +public final class TMarkerFactory { + private static final Marker INSTANCE = new MarkerImpl(); + + private TMarkerFactory() { + } + + public static Marker getMarker(String name) { + return INSTANCE; + } + + private static final class MarkerImpl implements Marker { + @Serial + private static final long serialVersionUID = 6353565105632304410L; + + @Override + public String getName() { + return "unnamed"; + } + + @Override + public void add(Marker reference) { + } + + @Override + public boolean remove(Marker reference) { + return false; + } + + @Override + @Deprecated + public boolean hasChildren() { + return false; + } + + @Override + public boolean hasReferences() { + return false; + } + + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public boolean contains(Marker other) { + return false; + } + + @Override + public boolean contains(String name) { + return false; + } + } +}