diff --git a/lychee.toml b/lychee.toml index d1e9846f..874b7f35 100644 --- a/lychee.toml +++ b/lychee.toml @@ -8,4 +8,13 @@ remap = [ "https://labs.leaningtech.com http://127.0.0.1:4321" ] # Check links inside `` and `
` blocks
 include_verbatim = true
 
-exclude = [ '.*(twitter|github|linkedin)\.com.*', 'http://127\.0\.0\.1:(8000|8080).*', '.*rpm\.leaningtech\.com.*', '.*mydomain\.com.*', '.*microsoft\.com.*', 'file://.*' ]
+exclude = [
+	'.*(twitter|github|linkedin)\.com.*',
+	'http://127\.0\.0\.1:(8000|8080).*',
+	'.*rpm\.leaningtech\.com.*',
+	'.*mydomain\.com.*',
+	'.*microsoft\.com.*',
+	'file://.*',
+	'chrome://.*',
+	'wss?://*'
+]
diff --git a/public/cheerpj3/examples/swingset3-template.zip b/public/cheerpj3/examples/swingset3-template.zip
new file mode 100644
index 00000000..a5379cbd
Binary files /dev/null and b/public/cheerpj3/examples/swingset3-template.zip differ
diff --git a/src/content/docs/cheerpj2/03-getting-started/01-Java-applet.mdx b/src/content/docs/cheerpj2/03-getting-started/01-Java-applet.mdx
index d5d9fc8d..30416a8c 100644
--- a/src/content/docs/cheerpj2/03-getting-started/01-Java-applet.mdx
+++ b/src/content/docs/cheerpj2/03-getting-started/01-Java-applet.mdx
@@ -21,7 +21,7 @@ CheerpJ can run Java applets in the browser seamlessly. This page will help you
 
 ### 1. Create a basic HTML file
 
-```html title="index.html" {6-9, 12-20}
+```html title="index.html" {6-9,12-19}
 
 
 	
diff --git a/src/content/docs/cheerpj3/00-faq.md b/src/content/docs/cheerpj3/00-faq.md
new file mode 100644
index 00000000..321d265e
--- /dev/null
+++ b/src/content/docs/cheerpj3/00-faq.md
@@ -0,0 +1,55 @@
+---
+title: FAQ
+---
+
+## What is CheerpJ?
+
+CheerpJ is a solution for running unmodified Java client applications into browser-based HTML5/JavaScript web applications. CheerpJ consists of a full Java runtime environment in JavaScript, and of a on-the-fly compiler for dynamic class generation, to be deployed alongside the application.
+
+## What parts of the Java SE runtime are supported?
+
+The CheerpJ runtime environment is a full Java SE runtime in JavaScript. Differently from other technologies which provide a partial re-implementation written manually in JavaScript, we opted to replace the entire OpenJDK Java SE runtime to JavaScript and WebAssembly. The CheerpJ runtime is constituted of both JavaScript files and .jar archives. All CheerpJ runtime components are dynamically downloaded on demand by the application to minimise total download size. The CheerpJ runtime library is hosted by us on a dedicated CDN-backed domain, and we invite users to link to it in order to take advantage of caching and cross-application resource sharing.
+
+## Can I self-host the CheerpJ runtime?
+
+Please [contact us](https://cheerpj.com/contact/) to discuss self-hosting CheerpJ and its runtime on your infrastructure.
+
+## Can I use CheerpJ to run my legacy Java application in the browser? I have no longer access to the source code.
+
+Yes, you can run any Java SE application with CheerpJ without touching the source code. You only need all the .jar archives of your application.
+
+## Can I use Java libraries and integrate them in my HTML5 application using CheerpJ?
+
+Yes. Java methods can be exposed to JavaScript with an interface compatible with async/await for convenience.
+
+## Can I call JavaScript libraries or web APIs from Java?
+
+Yes, CheerpJ allows you to interoperate with any JavaScript or browser API. Java native methods implemented in JavaScript are supported.
+
+## Does CheerpJ support reflection?
+
+Yes.
+
+## Does CheerpJ support dynamic class generation?
+
+Yes.
+
+## When I run CheerpJ I see 404/403 errors in the browser console. What's going on?
+
+Ignore those errors. CheerpJ provides a filesystem implementation on top of HTTP. In this context it is absolutely ok for some files to be missing. CheerpJ will correctly interpret 404 errors as a file not found condition.
+
+## My application compiled with CheerpJ does not work and I just see the "CheerpJ runtime ready" on the top of the screen. What's going on?
+
+Many first time users get stuck at this point. The most common issues are:
+
+- Opening the HTML page directly from disk: The URL in the browser should always start with http:// or https://, if it starts with file:// CheerpJ will not work. You need to use a local web server during testing.
+- Forgetting to add "/app/" prefix to the JAR files used in Web page. CheerpJ implements a virtual filesystem with multiple mount points, the "/app/" prefix is required.
+- More in general, you can use the "Network tab" of the developer tools in the browser to check if the JAR is being correctly downloaded. If the JAR is never downloaded, or a 404 error is returned, something is wrong with the JAR path. If you don't see anything in the "Network tab", please reload the page while keeping the developer tools open.
+
+## Can I play Old School RuneScape using CheerpJ or the CheerpJ Applet Runner extension?
+
+Not yet. The main problem is that RuneScape requires low level network connections primitives (sockets) which are not provided by browsers at this time due to security concerns. In the future we might provide a paid add-on to the CheerpJ Applet Runner extension to support this use case via tunneling.
+
+## What is the status of CheerpJ?
+
+CheerpJ is actively developed by [Leaning Technologies Ltd](https://leaningtech.com), a British-Dutch company focused on compile-to-JavaScript and compile-to-WebAssembly solutions.
diff --git a/src/content/docs/cheerpj3/01-changelog.md b/src/content/docs/cheerpj3/01-changelog.md
new file mode 100644
index 00000000..490916ff
--- /dev/null
+++ b/src/content/docs/cheerpj3/01-changelog.md
@@ -0,0 +1,42 @@
+---
+title: Changelog
+---
+
+version 3.0 - September 22nd, 2023:
+
+```
+
+Focus on performance, particularly on startup time
+* Multiple fixes on the Cheerp compiler to better optimize JNI code
+* Multiple experiments on the JIT to better interact with V8 tiering
+```
+
+version 3.0 - September 15th, 2023:
+
+```
+Work on CJ3 Library mode, with a focus on performance and Java <-> JS type conversions
+* Optimized away context switching overhead on user native calls
+* Support many more type conversion between JS numbers/integers/booleans and Java primitive types
+* Support conversion between JS numbers and Java boxed types (i.e. java.lang.Integer)
+* JIT optimizations
+* Optimization of the core class loading code path
+```
+
+version 3.0 - September 8th, 2023:
+
+```
+JNLP support and performance work
+* Significant speed up of System.arrayCopy
+* JIT optimizations
+```
+
+version 3.0 - September 1st, 2023:
+
+```
+Focus on networking
+* Integrated Tailscale support for low level TCP/UDP traffic, same code we use for WebVM/CheerpX
+* Custom HTTP/HTTPS handlers are now enabled by default unless Tailscale is used. The handlers provide http/https support in common cases.
+* Optimized exception handling
+* Optimized code rendering
+* Multiple JIT optimizations
+```
diff --git a/src/content/docs/cheerpj3/02-migrating-from-cheerpj2.md b/src/content/docs/cheerpj3/02-migrating-from-cheerpj2.md
new file mode 100644
index 00000000..b221e58b
--- /dev/null
+++ b/src/content/docs/cheerpj3/02-migrating-from-cheerpj2.md
@@ -0,0 +1,58 @@
+---
+title: Migration from CheerpJ 2
+---
+
+CheerpJ 3 is a complete reimplementation of CheerpJ 2, and as such it is not fully backwards compatible. This page lists the API differences between the two versions.
+
+## `cheerpjify.py` removed
+
+No downloads are provided with CheerpJ 3.
+
+- AOT optimization: CheerpJ 3 uses a JIT compiler only, and as such it does not require any pre-processing of the jar files, like conversion to `.jar.js`.
+- `--natives`: JNI function implementations should be passed to `cheerpjInit` using the `natives` option. See the [JNI guide] for more information.
+
+## `cheerpjInit` and `cheerpjRunMain` are now asynchronous
+
+- The `cheerpjInit` function now returns a `Promise` that resolves when the initialization is complete.
+- The `cheerpjRunMain` function now returns a `Promise` that resolves when the jar has been loaded.
+
+## `cheerpjRunJarWithClasspath` removed
+
+Use [`cheerpjRunMain`] instead.
+
+## `cjNew` and `cjCall` replaced with CJ3Library API
+
+`cjNew` and `cjCall` have been removed in favour of [`cheerpjRunLibrary`].
+
+```js
+// CheerpJ 2
+cheerpjInit();
+cheerpjRunJar("/app/library.jar");
+let obj = await cjNew("com.library.MyClass");
+await cjCall(obj, "myMethod");
+
+// CheerpJ 3
+await cheerpjInit();
+const lib = await cheerpjRunLibrary("/app/library.jar");
+const MyClass = await lib.com.library.MyClass;
+const obj = await new MyClass();
+await obj.myMethod();
+```
+
+## `com.leaningtech.handlers` HTTP handler no longer needed
+
+Previously, CheerpJ 2 required a special Java property to be set in order for HTTP(S) requests to work. This is no longer needed.
+
+## WebWorker API not yet implemented
+
+This is a work-in-progress.
+
+## `cheerpj-dom.jar` removed
+
+Calling JavaScript functions from Java is now done using the `natives` option of `cheerpjInit`. See the [JNI guide] for more information.
+
+If you used the `com.leaningtech.client` package extensively, check out the [CJDom library](https://github.com/reportmill/CJDom) (not maintained by Leaning Technologies).
+
+[`cheerpjRunLibrary`]: /cheerpj3/reference/cheerpjRunLibrary
+[`cheerpjRunMain`]: /cheerpj3/reference/cheerpjRunMain
+[JNI guide]: /cheerpj3/guides/Implementing-Java-native-methods-in-JavaScript
diff --git a/src/content/docs/cheerpj3/10-getting-started/00-Java-app.md b/src/content/docs/cheerpj3/10-getting-started/00-Java-app.md
new file mode 100644
index 00000000..5394dd9b
--- /dev/null
+++ b/src/content/docs/cheerpj3/10-getting-started/00-Java-app.md
@@ -0,0 +1,84 @@
+---
+title: Run a Java application
+---
+
+CheerpJ can run a Java application in the browser with little to no modifications. This page will help you getting started with CheerpJ and running your first Java application in the browser.
+
+Java source code is not needed to use CheerpJ. If you are building your own application you should already have its `.jar` file(s).
+
+**To get started you will need:**
+
+- Your Java application file(s). You can also use this [TextDemo.jar](https://docs.oracle.com/javase/tutorialJWS/samples/uiswing/TextDemoProject/TextDemo.jar) sample.
+- An HTML file where your Java app will be wrapped
+- A simple HTTP server to test your webpage locally
+
+## 1. Create a project directory
+
+Let's start by creating a project folder where all your files will be. Please copy your java and future HTML files here.
+
+```shell
+
+mkdir directory_name
+
+```
+
+## 2. Create a basic HTML file
+
+Let's create a basic HTML file like the following example. Please notice the CheerpJ runtime environment has been integrated and initialized. In this example we are assuming your HTML file and your `.jar` files are under the project directory you just created.
+
+```html title="index.html" {6, 9-13}
+
+
+	
+		
+		CheerpJ test
+		
+	
+	
+		
+	
+
+```
+
+Alternatively, if your application is not designed to be executed with the command `java -jar` you can replace `cheerpjRunJar()` for `cheerpjRunMain()` and pass your qualified class name as an argument. For example:
+
+```js
+cheerpjRunMain(
+	"com.application.MyClassName",
+	"/app/my_application_archive.jar:/app/my_dependency_archive.jar",
+);
+```
+
+## 3. Host your page
+
+You can now serve this web page on a simple HTTP server, such as the http-server utility.
+
+```shell
+npm install http-server
+http-server -p 8080
+```
+
+> To test CheerpJ you must use a web server. Opening the `.html` page directly from the disk (for example, by double-clicking on it) is not supported.
+
+## What's going on?
+
+- CheerpJ loader is included from our cloud runtime as
+  ``.
+- CheerpJ runtime environment is initialized by `cheerpjInit()`.
+- `cheerpjCreateDisplay()` creates a graphical environment to contain all Java windows
+- `cheerpjRunMain()` executes the `main` method of `ChangeThisToYourClassName`. The second parameter is a `:` separated list of `.jar` files where application classes can be found (the classpath).
+- The `/app/` is a virtual file system mount point that reference the root of the web server this page is loaded from.
+
+## The result
+
+You will see the CheerpJ display on your browser with some loading messages before showing your application running. Depending on your application and the optimizations applied, this could take just a few seconds.
+
+## Further reading
+
+- [Runtime API reference](/cheerpj3/reference)
diff --git a/src/content/docs/cheerpj3/10-getting-started/01-Java-applet.mdx b/src/content/docs/cheerpj3/10-getting-started/01-Java-applet.mdx
new file mode 100644
index 00000000..ae4bfe08
--- /dev/null
+++ b/src/content/docs/cheerpj3/10-getting-started/01-Java-applet.mdx
@@ -0,0 +1,97 @@
+---
+title: Run a Java applet
+---
+
+import LinkButton from "../../../../components/LinkButton.astro";
+
+CheerpJ can run Java applets in the browser seamlessly. This page will help you getting started with CheerpJ for Java applets.
+
+**There are two different ways to run a Java Applet in the browser:**
+
+- [Running your own Java applet](/cheerpj3/getting-started/Java-applet#running-your-own-applet) using the CheerpJ runtime environment and the `` tag in your own webpage.
+- [Running a public applet](/cheerpj3/getting-started/Java-applet#running-a-public-applet) using the [CheerpJ Applet Runner](https://chrome.google.com/webstore/detail/cheerpj-applet-runner/bbmolahhldcbngedljfadjlognfaaein) Chrome extension for applets integrated with the applet tag `` on public websites.
+
+## Running your own applet
+
+**You will need:**
+
+- Your applet file(s)
+- An HTML file to wrap your applet
+- A basic HTTP server to test locally
+
+### 1. Create a basic HTML file
+
+```html title="index.html" {6, 9-17}
+
+
+	
+		
+		CheerpJ applet test
+		
+	
+	
+		
+		
+	
+
+```
+
+### 2. Host your page locally
+
+You can now serve this web page on a simple HTTP server, such as the http-server utility.
+
+```shell
+npm install http-server
+http-server -p 8080
+```
+
+### What's going on?
+
+- The `cheerpjInit();` initializes CheerpJ runtime environment.
+- The `` tag specifies the code base in a similar manner as the now deprecated `` tag.
+
+> To avoid potential conflicts with native Java we recommend replacing the original HTML tag with `cheerpj-` prefixed version. You should use ``, `` or `` depending on the original tag.
+
+## Running a public applet
+
+### 1. Install the CheerpJ applet runner
+
+CheerpJ Applet Runner is available for Chrome and Edge.
+
+
+ + + + +
+ +### 2. Go to a website with an applet + +Visit a page with a Java applet, [such as this one](http://www.neilwallis.com/projects/java/water/index.php) and click on the CheerpJ Applet Runner icon in the toolbar and enable CheerpJ. + +![](/cheerpj2/assets/cheerpj_applet_demo.gif) + +## The result + +You will see the CheerpJ display on your browser with some loading messages before showing your applet running. Depending on your application and the optimizations applied, this could take just a few seconds. + +## Further reading + +- [Runtime API reference](/cheerpj3/reference) diff --git a/src/content/docs/cheerpj3/10-getting-started/02-Java-library.md b/src/content/docs/cheerpj3/10-getting-started/02-Java-library.md new file mode 100644 index 00000000..d3a6f3be --- /dev/null +++ b/src/content/docs/cheerpj3/10-getting-started/02-Java-library.md @@ -0,0 +1,31 @@ +--- +title: Run a Java library +subtitle: Use Java classes in JavaScript +--- + +## 1. Include CheerpJ on your page + +```html + +``` + +## 2. Initialize CheerpJ and load your Java library + +```js +await cheerpjInit(); +const cj = await cheerpjRunLibrary("/app/library.jar"); +``` + +This will load `library.jar` from the root of your web server. + +## 3. Call Java from JavaScript + +```js +const MyClass = await cj.com.library.MyClass; +const obj = await new MyClass(); +await obj.myMethod(); +``` + +## Further reading + +- [`cheerpjRunLibrary` reference](/cheerpj3/reference/cheerpjRunLibrary) diff --git a/src/content/docs/cheerpj3/11-guides/File-System-support.md b/src/content/docs/cheerpj3/11-guides/File-System-support.md new file mode 100644 index 00000000..3acd30b4 --- /dev/null +++ b/src/content/docs/cheerpj3/11-guides/File-System-support.md @@ -0,0 +1,65 @@ +--- +title: File System support +--- + +CheerpJ provides full filesystem support for Java applications running with CheerpJ. + +Read only and read/write filesystems are exposed in Java and can be used to read, write and manipulate files as normally when running on a JVM. + +**Note**: CheerpJ provides access to a virtualized filesystem, which does not correspond to the local computer. Accessing local files from the browser is forbidden for security reasons. + +## File Systems in CheerpJ + +CheerpJ implements three main filesystem concepts: + +1. A read-only, HTTP-based filesystem +2. A read/write, IndexedDB-based, persistent filesystem +3. A read-only, memory based filesystem + +CheerpJ filesystems are implemented as UNIX-style virtual filesystems with multiple mount points. The default mount points are defined as follows: + +| Mount | Description | +| --------- | --------------------------------------------------------------------------------------------------------- | +| `/app/` | An HTTP-based read-only filesystem, used to access JARs and data from your local server | +| `/files/` | An IndexedDB-based, persistent read-write file system | +| `/lt/` | Another HTTP-based read-only filesystem, pointing to the CheerpJ runtime | +| `/str/` | A read-only filesystem to easily share JavaScript Strings or binary data (an `Uint8Array`) with Java code | + +## `/app/` mount point + +The /app/ mount point corresponds to a virtual read-only, HTTP-based filesystem. `/app/` is used to access JAR files and data from your local server. + +The `/app/` directory is virtual, and only exists inside of CheerpJ. It is required to distinguish files from the local server from runtime files and files stored in the browser database. + +The `/app/` directory refers to the root of your web server. So, assuming that your web server is available at `http://127.0.0.1:8080/`, here are some example file mappings: + +- `/app/example.jar` → `http://127.0.0.1:8080/example.jar` +- `/app/subdirectory/example.txt` → `http://127.0.0.1:8080/subdirectory/example.txt` + +## `/files/` mount point + +The `/files/` mount point corresponds to a virtual read-write, IndexedDB-based filesystem. `/files/` is used to store persistent data on the browser client. + +The `/files/` directory is a virtual concept used by CheerpJ to store and refer to files. + +## `/str/` mount point + +The `/str/` mount point is a simple read-only filesystem that can be populated from JavaScript to share data with Java code. + +From JavaScript you can add files into the filesystem using the `cheerpjAddStringFile` API. Example: + +```js +cheerpjAddStringFile("/str/fileName.txt", "Some text in a JS String"); +``` + +You can access this data from Java, for example: + +```java +import java.io.FileReader; +... + +FileReader f = new FileReader("/str/fileName.txt") +... +``` + +The `cheerpjAddStringFile` API can be used with JavaScript `String`s or `Uint8Array`s. `Uint8Array`s may be useful to provide binary data to the Java application, for example a user selected file coming from an HTML5 `` tag. diff --git a/src/content/docs/cheerpj3/11-guides/Implementing-Java-native-methods-in-JavaScript.md b/src/content/docs/cheerpj3/11-guides/Implementing-Java-native-methods-in-JavaScript.md new file mode 100644 index 00000000..e30a05a7 --- /dev/null +++ b/src/content/docs/cheerpj3/11-guides/Implementing-Java-native-methods-in-JavaScript.md @@ -0,0 +1,36 @@ +--- +title: Implementing native methods +subtitle: Java Native Interface (JNI) with CheerpJ +--- + +With CheerpJ, it is possible to implement Java 'native' methods (that would normally be implemented in C/C++ or other AOT-compiled language) in JavaScript, similarly to what would be done in regular Java using the Java Native Interface (JNI). + +As an example, consider the following Java class: + +```java title="TestClass.java" +package com.example; + +public class TestClass { + public native void alert(String str); +} +``` + +To provide an implementation of `alert`, pass it to the `cheerpjInit` function as a property of the `natives` object: + +```js +await cheerpjInit({ + natives: { + async Java_com_example_TestClass_alert(lib, str) { + window.alert(str); + }, + }, +}); +``` + +The name of methods in the `natives` object must be in the form `Java__`. + +The `lib` parameter is a [CJ3Library]. It can be used to access other classes and methods of the library. + +Parameters and return values of JNI calls are automatically converted between JavaScript and Java types. + +[CJ3Library]: /cheerpj3/reference/cheerpjRunLibrary#cj3library diff --git a/src/content/docs/cheerpj3/11-guides/Startup-time-optimization.md b/src/content/docs/cheerpj3/11-guides/Startup-time-optimization.md new file mode 100644 index 00000000..86c1aa42 --- /dev/null +++ b/src/content/docs/cheerpj3/11-guides/Startup-time-optimization.md @@ -0,0 +1,56 @@ +--- +title: Startup time optimization +--- + +This page is a collection of different steps to reduce the startup time of a Java application compiled with CheerpJ. + +## Overview + +Traditionally, users had to have Java preinstalled on their computer in order to run Java applications and applets. CheerpJ compiles Java to HTML5/JavaScript, allowing to run applications and applets on browser without users having to install any additional dependency on their computer. Similarly to their JVM counterparts, applications compiled to JavaScript with CheerpJ require runtime components to be loaded during execution. In CheerpJ, runtime components are JavaScript modules that are loaded on demand, only if required. + +The CheerpJ runtime is highly optimised to minimise the total download size of an 'average' application, totalling 10-20MB of data for a typical download (as a point of comparison, the approximate size of the Java runtime installer is over 60MB). All downloaded components of CheerpJ are cached by the browser, which reduces the download time in subsequent executions of a same application. + +This page provides a list of recommendations to reduce the one-time download size, and the resulting application first startup time. + +## Preload resources + +CheerpJ cannot predict which runtime resources will be required by an arbitrary application. CheerpJ runtime resources are therefore loaded on demand, one after the other, depending on the requirements of the application at run time. + +To take advantage of parallel downloads, and reduce download and startup time of a specific application in production, CheerpJ allows to pre-specify a list of resources (CheerpJ runtime modules) to be loaded at startup. + +This list of resources is to be specified manually when starting the CheerpJ environment in an HTML page. We also provide a simple profiling tool to automatically record and output a list of used resources during the execution of an application. + +By combining the use of this profiler together with the preloader, one can highly optimise the initial download and startup time of an application. Taking advantage of this is a simple 2-step process: + +1. Run the application normally using CheerpJ. After the application is loaded, open the JavaScript console of the browser (e.g. Ctrl+Shift+I on many browsers), and type: + +```js +cjGetRuntimeResources(); +``` + +The result will look like this: + +```js +{"/lts/file1.jar":[int, int, ...], "/lts/file2.jar":[int,int, ...]} +``` + +If the output is not visible fully, you can use: + +```js +document.write(cjGetRuntimeResources()); +``` + +The JavaScript console may enclose the string between quotes (`"`), which you should ignore. See [here](/cheerpj3/reference/cjGetRuntimeResources) for more information. + +2. Modify the CheerpJ integration to enable preloading. You will only need to change the `cheerpjInit` call, to pass the `preloadResources` option. For example: + +```js +cheerpjInit({ preloadResources: {"/lts/file1.jar":[int, int, ...], "/lts/file2.jar":[int,int, ...]} }); +``` + +> [!note] Important +> Please note that this has to be done in two steps, so the resources are loaded in a separate session from the full workflow. + +See [here](/cheerpj3/reference/cheerpjInit#preloadresources) for more information. + +When preloading is enabled CheerpJ will be able to download multiple resources in parallel with the execution of the program. This will greatly improve loading time. diff --git a/src/content/docs/cheerpj3/12-reference/00-cheerpjInit.md b/src/content/docs/cheerpj3/12-reference/00-cheerpjInit.md new file mode 100644 index 00000000..3bec4444 --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/00-cheerpjInit.md @@ -0,0 +1,198 @@ +--- +title: cheerpjInit +description: Set up and initialize the CheerpJ runtime environment +--- + +`cheerpjInit` must be called once in the page to setup and initialise the CheerpJ runtime environment. + +```ts +async function cheerpjInit(options?: { + version?: number; + fetch?: ( + url: string, + method: string, + postData: ArrayBuffer, + headers: unknown[], + ) => Promise; + status?: "splash" | "none" | "default"; + logCanvasUpdates?: boolean; + preloadResources?: { [key: string]: number[] }; + clipboardMode?: "permission" | "system" | "java"; + beepCallback?: () => void; + preloadProgress?: (preloadDone: number, preloadTotal: number) => void; + enableInputMethods?: boolean; + overrideShortcuts?: (evt: KeyboardEvent) => boolean; + appletParamFilter?: (originalName: string, paramValue: string) => string; + natives?: { [method: string]: Function }; + overrideDocumentBase?: string; + javaProperties?: string[]; + tailscaleControlUrl?: string; + tailscaleDnsUrl?: string; + tailscaleAuthKey?: string; + tailscaleLoginUrlCb?: (r: unknown) => void; +}): Promise; +``` + +## Parameters + +- **options (`object`, _optional_)** - Used to configure different settings of the CheerpJ runtime environment in the form `{ option: "value" }`. + +| **Option** | **Value expected type** | +| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| [`version`](/cheerpj3/reference/cheerpjInit#version) | `number` | +| [`fetch`](/cheerpj3/reference/cheerpjInit#fetch) | `(url: string, method: string, postData: ArrayBuffer, headers: unknown[]) => Promise` | +| [`status`](/cheerpj3/reference/cheerpjInit#status) | `"splash" or "none" or"default"` | +| [`logCanvasUpdates`](/cheerpj3/reference/cheerpjInit#logcanvasupdates) | `boolean` | +| [`preloadResources`](/cheerpj3/reference/cheerpjInit#preloadresources) | `{ [key: string]: number[] }` | +| [`clipboardMode`](/cheerpj3/reference/cheerpjInit#clipboardmode) | `"permission" or "system" or "java"` | +| `beepCallback` | `() => void` | +| [`preloadProgress`](/cheerpj3/reference/cheerpjInit#preloadprogress) | `(preloadDone: number, preloadTotal: number) => void` | +| [`enableInputMethods`](/cheerpj3/reference/cheerpjInit#enableinputmethods) | `boolean` | +| [`overrideShortcuts`](/cheerpj3/reference/cheerpjInit#overrideshortcuts) | `evt: KeyboardEvent) => boolean` | +| [`appletParamFilter`](/cheerpj3/reference/cheerpjInit#appletparamfilter) | `(originalName: string, paramValue: string) => string` | +| `natives` | `{ [method: string]: Function }` | +| `overrideDocumentBase` | `string` | +| [`javaProperties`](/cheerpj3/reference/cheerpjInit#javaproperties) | `string[]` | +| `tailscaleControlUrl` | `string` | +| `tailscaleDnsUrl` | `string` | +| `tailscaleAuthKey` | `string` | +| `tailscaleLoginUrlCb` | `() => void` | + +## Returns + +`cheerpjInit` returns a [Promise] which is resolved when the CheerpJ runtime environment is ready to be used. + +## Examples + +A description of each `cheerpjInit()` option with brief examples are given below. + +### `version` + +The Java runtime version to use. `8` is the only supported value at the moment. + +### `clipboardMode` + +By default CheerpJ supports an internal clipboard which is local to the Java application and is not integrated with the system clipboard. To change this behaviour you can initialize CheerpJ in the following way: + +```js +cheerpjInit({ clipboardMode: "system" }); +``` + +In `system` mode CheerpJ will share the clipboard with the system. Browsers enforce serious limitations on how the system clipboard can be accessed. In practice it is generally accessible when the `Ctrl+C` and `Ctrl+V` shortcuts are used (`Cmd+C` and `Cmd+V` on MacOSX). Due to these limitations the UX when using `clipboardMode:"system"` is: + +- `Ctrl+C`/`Cmd+C`: the user has to press the shortcut twice to give CheerpJ access to the system clipboard. CheerpJ will block the execution while waiting for the second `Ctrl+C`. +- `Ctrl+V`/`Cmd+V`: this shortcut behaves normally, there is no difference with native execution. +- Menu based Copy/Paste: `Ctrl+C`/`Ctrl+V` are needed to access the clipboard. CheerpJ will block the execution while waiting the appropriate shortcut. + +### `enableInputMethods` + +When this option is set to `true` CheerpJ will be able to receive text input from the input method framework of the platform. This is useful to support text input for languages such as Chinese, Japanese and Korean. + +```js +cheerpjInit({ enableInputMethods: true }); +``` + +### `javaProperties` + +An array of Java properties in the form `"key=value"`. They will be defined on the System object (System properties). This option should be used if command line arguments in the form `-Dkey=value` are required when using native Java. + +Example usage: + +```js +cheerpjInit({ javaProperties: ["prop1=value1", "prop2=value2"] }); +``` + +### `logCanvasUpdates` + +When set to `true`, it enables logs on the console about the display areas which are being updated. Useful to debug overdrawing. + +Example: + +```js +cheerpjInit({ logCanvasUpdates: true }); +``` + +### `overrideShortcuts` + +Some applications needs to internally handle keyboard shortcuts which are also used by the browser, for example Ctrl+F. Most users expect the standard browser behavior for these shortcuts and CheerpJ does not, by default, override them in any way. + +A CheerpJ-compiled application can take control of additional shortcuts by providing a callback function as the `overrideShortcuts` options of `cheerpjInit`. This callback receives the `KeyboardEvent` coming from the browser and should return `true` if the default browser behaviour should be prevented. + +Whenever possible we recommend _not_ to use browser reserved shortcuts, to maintain a consistent user experience. In any case, the following limitations apply: + +- Some shortcuts (Ctrl+T, Ctrl+N, Ctrl+W) are reserved by the browser and never received by the page itself. These _cannot_ be overridden +- Overriding (Ctrl+C/Ctrl+V) will prevent `clipboardMode:"system"` from working correctly. + +Example: + +```js +cheerpjInit({ + overrideShortcuts: function (e) { + // Let Java handle Ctrl+F + if (e.ctrlKey && e.keyCode == 70) return true; + return false; + }, +}); +``` + +### `preloadResources` + +By using `preloadResources`, you can provide CheerpJ with a list of runtime files which you know in advance will be required for the specific application. The list should be given as a JavaScript array of strings. + +Example: + +```js +cheerpjInit({ preloadResources: {"/lts/file1.jar":[int, int, ...], "/lts/file2.jar":[int,int, ...]} }); +``` + +See also [cjGetRuntimeResources]. + +### `preloadProgress` + +This callback may be used in combination with [`preloadResources`](#preloadresources) to monitor the loading of an application. The information provided is useful, for example, to display a loading/progress bar. + +Example: + +```js +function showPreloadProgress(preloadDone, preloadTotal) { + console.log("Percentage loaded " + (preloadDone * 100) / preloadTotal); +} + +await cheerpjInit({ preloadProgress: showPreloadProgress }); +``` + +### `status` + +This option determines the level of verbosity of CheerpJ in reporting status updates. + +- `"default"`: Enables status reporting during initialization and short-lived "Loading..." messages every time new runtime code is being downloaded. +- `"splash"`: Enabled status reporting only during initialization. There will be no feedback after the first window is shown on screen. +- `"none"`: Disable all status reporting. + +Example: + +```js +cheerpjInit({ status: "splash" }); +``` + +### `appletParamFilter` + +Some applications may need to have some parameter modified before getting those inside the applet. + +Example: + +```js +cheerpjInit({ + appletParamFilter: function (name, value) { + if (name === "httpServer") return value.replace("http", "https"); + return value; + }, +}); +``` + +### `fetch` + +This option is used to make a `fetch` request over the network. + +[cjGetRuntimeResources]: /cheerpj3/reference/cjGetRuntimeResources +[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise diff --git a/src/content/docs/cheerpj3/12-reference/01-cheerpjRunMain.md b/src/content/docs/cheerpj3/12-reference/01-cheerpjRunMain.md new file mode 100644 index 00000000..cb724de4 --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/01-cheerpjRunMain.md @@ -0,0 +1,38 @@ +--- +title: cheerpjRunMain +subtitle: Starts an application by executing the static main method of a Java class +--- + +```ts +async function cheerpjRunMain( + className: string, + classPath: string, + ...args: string[] +): Promise; +``` + +## Parameters + +- **className (`string`)** - The fully-qualified name of the class with a static main method to execute. For example, `com.application.MyClassName`. +- **classPath (`string`)** - The location of the class's jar in the [virtual filesystem], with its dependencies separated by `:`. +- **..args (`string[]`, _optional)_** - Arguments to pass to the main method. + +## Returns + +`cheerpjRunMain` returns a [Promise] which resolves with the [exit code] of the program. `0` indicates success, any other value indicates failure. + +## Example + +```js +const exitCode = await cheerpjRunMain( + "fully.qualified.ClassName", + "/app/my_application_archive.jar:/app/my_dependency_archive.jar", + arg1, + arg2, +); +console.log(`Program exited with code ${exitCode}`); +``` + +[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[exit code]: https://en.wikipedia.org/wiki/Exit_status#Java +[virtual filesystem]: /cheerpj3/guides/File-System-support diff --git a/src/content/docs/cheerpj3/12-reference/02-cheerpjRunJar.md b/src/content/docs/cheerpj3/12-reference/02-cheerpjRunJar.md new file mode 100644 index 00000000..e3cfd1c5 --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/02-cheerpjRunJar.md @@ -0,0 +1,31 @@ +--- +title: cheerpjRunJar +subtitle: Starts an application by executing its main class +--- + +```ts +async function cheerpjRunJar( + jarName: string, + ...args: string[] +): Promise; +``` + +## Parameters + +- **jarName (`string`)** - The location of the jar in the [virtual filesystem]. +- **..args (`string[]`, _optional)_** - Arguments to pass to the main method. + +## Returns + +`cheerpjRunJar` returns a [Promise] which resolves with the [exit code] of the program. `0` indicates success, any other value indicates failure. + +## Example + +```js +const exitCode = await cheerpjRunMain("/app/application.jar"); +console.log(`Program exited with code ${exitCode}`); +``` + +[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[exit code]: https://en.wikipedia.org/wiki/Exit_status#Java +[virtual filesystem]: /cheerpj3/guides/File-System-support diff --git a/src/content/docs/cheerpj3/12-reference/03-cheerpjRunLibrary.md b/src/content/docs/cheerpj3/12-reference/03-cheerpjRunLibrary.md new file mode 100644 index 00000000..3253c880 --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/03-cheerpjRunLibrary.md @@ -0,0 +1,85 @@ +--- +title: cheerpjRunLibrary +--- + +Allows to load a Java Library into JavaScript. + +```ts +async function cheerpjRunLibrary(classPath: string): Promise; +``` + +## Parameters + +- **classPath (`string`)** - The path to the library's jar file in the [virtual filesystem]. Pass an empty string to load the standard library only. + +## Returns + +`cheerpjRunLibrary` returns a [Promise] which resolves to a `CJ3Library` object. + +### CJ3Library + +This object can be used to access the classes and methods of the loaded library through property access. + +- To load a class, access it and await it. +- To call a static method, call it as a method on a loaded class and await it. +- To construct a class into an instance, use `await new`. +- To call an instance method, call it as a method on an instance of a loaded class and await it. + +Parameters and return values of method calls are automatically converted between JavaScript and Java types. + +> [!warning] Warning +> Array interop is not yet supported. + +## Examples + +### Using the standard library + +```js +await cheerpjInit(); +const lib = await cheerpjRunLibrary(""); + +const System = await lib.java.lang.System; +await System.out.println("Hello from Java"); +``` + +### Using a custom library + +Let's say we had a library called `example.jar` compiled from the following class: + +```java +package com.example; + +public class Example { + public void hello() { + System.out.println("Example says hello!"); + } +} +``` + +With `example.jar` being available on the web server at `/example.jar`, we could use it like so: + +```js +await cheerpjInit(); +const lib = await cheerpjRunLibrary("/app/example.jar"); + +const Example = await lib.com.example.Example; +const example = await new Example(); +await example.hello(); // Example says hello! +``` + +### Exception handling + +```js +await cheerpjInit(); +const lib = await cheerpjRunLibrary(""); + +try { + // Attempt to load a class that doesn't exist + await lib.java.lang.DoesntExist; +} catch (e) { + await e.printStackTrace(); // java.lang.ClassNotFoundException: java.lang.DoesntExist +} +``` + +[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[virtual filesystem]: /cheerpj3/guides/File-System-support diff --git a/src/content/docs/cheerpj3/12-reference/10-cheerpjCreateDisplay.md b/src/content/docs/cheerpj3/12-reference/10-cheerpjCreateDisplay.md new file mode 100644 index 00000000..d19a57ba --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/10-cheerpjCreateDisplay.md @@ -0,0 +1,58 @@ +--- +title: cheerpjCreateDisplay +description: Display GUI elements +--- + +`cheerpjCreateDisplay` adds an element to the DOM which will be used for graphical rendering. + +```ts +function cheerpjCreateDisplay( + width: number, + height: number, + parent?: HTMLElement, +): HTMLElement; +``` + +## Parameters + +- **width (`number`)** - The width of the display area in CSS pixels, or `-1` to match parent width. +- **height (`number`)** - The height of the display area in CSS pixels, or `-1` to match parent height. +- **parent (`HTMLElement`, _optional_)** - Element to add display as a child of. + +## Returns + +`cheerpjCreateDisplay` returns an [`HTMLElement`] representing the created display. + +## Examples + +### Create a display + +```js +cheerpjCreateDisplay(800, 600); +``` + +This creates a 800x600 display for rendering, and appends it to the document body. + +### Take up the whole page + +```js +cheerpjCreateDisplay(-1, -1, document.body); +``` + +This creates a display that takes up the whole page, and responds to changes in the page size. + +### Usage with React + +```jsx +import { useRef, useEffect } from "react"; + +function Display({ width, height }) { + const parent = useRef(); + useEffect(() => { + cheerpjCreateDisplay(width, height, parent); + }); + return
; +} +``` + +[`HTMLElement`]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement diff --git a/src/content/docs/cheerpj3/12-reference/20-cjFileBlob.md b/src/content/docs/cheerpj3/12-reference/20-cjFileBlob.md new file mode 100644 index 00000000..b8cfe06d --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/20-cjFileBlob.md @@ -0,0 +1,39 @@ +--- +title: cjFileBlob +subtitle: Read a file from the virtual filesystem +--- + +Used to read files from the `/files/` virtual filesystem. + +```ts +async function cjFileBlob(path: string): Promise; +``` + +## Parameters + +- **path (`string`)** - The path to the file to be read. Must begin with `/files/`. + +## Returns + +`cjFileBlob` returns a [Promise] which resolves to a [Blob] of the file contents. + +## Examples + +## Read a text file + +```js +const blob = await cjFileBlob("/files/file1.txt"); +const text = await blob.text(); +console.log(text); +``` + +## Read a binary file + +```js +const blob = await cjFileBlob("/files/file2.bin"); +const data = new Uint8Array(await blob.arrayBuffer()); +console.log(data); +``` + +[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[Blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob diff --git a/src/content/docs/cheerpj3/12-reference/21-cheerpjAddStringFile.md b/src/content/docs/cheerpj3/12-reference/21-cheerpjAddStringFile.md new file mode 100644 index 00000000..6cba93bc --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/21-cheerpjAddStringFile.md @@ -0,0 +1,25 @@ +--- +title: cheerpjAddStringFile +subtitle: Write a file into the virtual filesystem +--- + +Used to write files into the `/str/` filesystem. If the file already exists, it will be overwritten. + +```ts +function cheerpjAddStringFile(path: string, data: string | Uint8Array): void; +``` + +## Parameters + +- **path (`string`)** - The path to the file to overwrite. Must begin with `/str/`. +- **data (`string` or `Uint8Array`)** - File contents, as text or binary data. + +## Returns + +`cheerpjAddStringFile` does not return a value. + +## Example + +```js +cheerpjAddStringFile("/str/fileName.txt", "Some text in a JS String"); +``` diff --git a/src/content/docs/cheerpj3/12-reference/30-cjGetRuntimeResources.md b/src/content/docs/cheerpj3/12-reference/30-cjGetRuntimeResources.md new file mode 100644 index 00000000..aef1eeca --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/30-cjGetRuntimeResources.md @@ -0,0 +1,41 @@ +--- +title: cjGetRuntimeResources +--- + +Returns a JavaScript string representing the data that should be passed to [preloadResources]. Once parsed, it is an object containing the filenames that have been loaded from the runtime up to the time this function is called. + +See [startup time optimization](/cheerpj3/guides/Startup-time-optimization) for more information. + +```ts +function cjGetRuntimeResources(): string; +``` + +> [!note] Note +> This function is intended for use in the browser console. It is not intended to be called from within your application. + +## Parameters + +`cjGetRuntimeResources` does not take any parameters. + +## Returns + +`cjGetRuntimeResources` returns a string representing the files that have been loaded from the runtime. + +Parse this string with [JSON.parse] and pass it as [preloadResources] in future page loads. + +## Example + +In the browser console, type: + +```shell +cjGetRuntimeResources(); +``` + +The output would look like this: + +```js +'{"/lts/file1.jar":[int, int, ...], "/lts/file2.jar":[int,int, ...]}'; +``` + +[preloadResources]: /cheerpj3/reference/cheerpjInit#preloadresources +[JSON.parse]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse diff --git a/src/content/docs/cheerpj3/12-reference/31-cjGetProguardConfiguration.md b/src/content/docs/cheerpj3/12-reference/31-cjGetProguardConfiguration.md new file mode 100644 index 00000000..0d81c35a --- /dev/null +++ b/src/content/docs/cheerpj3/12-reference/31-cjGetProguardConfiguration.md @@ -0,0 +1,32 @@ +--- +title: cjGetProguardConfiguration +--- + +Triggers download of a configuration file which can be used to tree-shake JARs using [ProGuard]. + +```ts +function cjGetProguardConfiguration(): void; +``` + +> [!note] Note +> This function is intended for use in the browser console. It is not intended to be called from within your application. + +## Parameters + +`cjGetProguardConfiguration` does not take any parameters. + +## Returns + +`cjGetProguardConfiguration` does not return a value. It triggers a download of a `cheerpj.pro` file. + +## Example + +On the browser console type: + +```shell +cjGetProguardConfiguration(); +``` + +This will trigger the download of `cheerpj.pro` file. + +[ProGuard]: https://github.com/Guardsquare/proguard diff --git a/src/content/docs/cheerpj3/13-examples/00-swingset3.md b/src/content/docs/cheerpj3/13-examples/00-swingset3.md new file mode 100644 index 00000000..7193d3e3 --- /dev/null +++ b/src/content/docs/cheerpj3/13-examples/00-swingset3.md @@ -0,0 +1,114 @@ +--- +title: SwingSet3 +--- + +In this tutorial, we'll run the SwingSet3 application in the browser. Here's what we'll build: + + + +## Prerequisites + +- [Download the template project](/cheerpj3/examples/swingset3-template.zip) and unzip it +- [Node.js](https://nodejs.org/en/) (>= 18) + +The starting point of this example is an empty HTML page, the SwingSet3 jar, and its dependencies: + +``` +. +├── index.html +├── SwingSet3.jar +└── lib + ├── AnimatedTransitions.jar + ├── AppFramework.jar + ├── Filters.jar + ├── MultipleGradientPaint.jar + ├── TimingFramework.jar + ├── javaws.jar + ├── swing-layout-1.0.jar + ├── swing-worker.jar + └── swingx.jar +``` + +## 1. Run a web server + +To view the example, we need to host the files on a web server. [Vite](https://vitejs.dev/) is a convenient tool for this, as it automatically reloads the page when the files change. + +```sh +npx vite +``` + +Visit the URL shown in the terminal and you should see a blank page. Leave Vite running in the background for the remainder of this tutorial. + +## 2. Add CheerpJ to the document + +Let's add CheerpJ to the page by adding this script tag to the ``: + +```html title="index.html" + +``` + +## 3. Initialise CheerpJ and run the jar + +Add the following script tag to the ``: + +```html title="index.html" + +``` + +This will initialise CheerpJ, create a 800x600 display, and run the SwingSet3 jar. We use `type="module"` so that we can use [top-level await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#top_level_await). + +> [!question] What is `/app/SwingSet3.jar`? +> This is a [virtual filesystem](/cheerpj3/guides/File-System-support) which represents the root of the web server. + +Save the file and you should see SwingSet3 load and run in the browser. 🥳 + +## 4. Make the application take up the whole page + +The application takes up a small portion of the page, but for many applications we want to take up the whole page. + +To do this, we'll add a new element to the ``: + +```html title="index.html" +
+``` + +> [!note] Note +> Make sure you add the container element **before** the script which calls `cheerpjCreateDisplay`. + +And then add some CSS: + +```html title="index.html" + +``` + +Finally, update the script to use the container element: + +```html title="index.html" {3} + +``` + +Passing `-1` as the width and height tells CheerpJ to use the full size of the container element, and listen for resize events. + +View the page again, and you should see the application take up the entire window. Also notice that resizing the window resizes the application. + +## Source code + +[View full source code on GitHub](https://github.com/leaningtech/cheerpj-example-swingset3) diff --git a/src/content/docs/cheerpj3/index.mdx b/src/content/docs/cheerpj3/index.mdx index 13f1f292..d794aaf9 100644 --- a/src/content/docs/cheerpj3/index.mdx +++ b/src/content/docs/cheerpj3/index.mdx @@ -3,11 +3,96 @@ title: CheerpJ subtitle: JVM replacement for the browser --- -import BlogPostCard from "@/components/BlogPostCard.astro"; import LatestBlogPostPill from "@/components/LatestBlogPostPill.astro"; + + +CheerpJ is a **drop-in HTML5 replacement** for the JVM, and is compatible with 100% of Java 8, including Swing, reflection and dynamic class loading. + +![](/cheerpj2/assets/cheerpj_visual_2.png) + -**Coming soon!** +With CheerpJ, you can: + +- Run existing **Java applications** in the browser with no changes +- Include **Java applets** in webpages without legacy plugins +- Migrate **Java Web Start / JNLP** applications to work on modern systems +- Use Java libraries in JavaScript/TypeScript seamlessly + +## Getting started + +Know what you're building? Jump straight to the relevant tutorial: + + + +## What is CheerpJ? + +CheerpJ is a combination of two components: + +1. An optimising Java-to-JavaScript JIT compiler. +2. A full Java SE 8 runtime. + +Both are written in C++ and are compiled to WebAssembly & JavaScript using [Cheerp](/cheerp). + +## What is unique about CheerpJ? + +1. CheerpJ can handle 100% of Java 8, including Swing, reflection and dynamic class loading with no manual intervention on the code. +2. CheerpJ works directly on Java bytecode, and does not require access to the Java source code. +3. CheerpJ comes with a full Java SE runtime, inclusive of Swing/AWT. It supports audio, printing, and any other Java SE features. The runtime supports WebAssembly for optimal performance and size. +4. The JavaScript code generated by CheerpJ is highly optimised and garbage-collectible. +5. CheerpJ enables bidirectional Java-JavaScript interoperability. JavaScript libraries, as well as the DOM, can be called and manipulated from Java. Also, Java modules can be invoked from JavaScript. +6. CheerpJ supports Java multi-threading. In addition, it allows to create concurrent applications by using WebWorkers. + +## Licensing + +CheerpJ is free for technical evaluation and non-commercial use. For commercial use, see [licensing](https://cheerpj.com/licensing/) for details. + +## Demos + +Several demos of CheerpJ can be found [here](https://leaningtech.com/demo/). + +You can also see CheerpJ in action in [JavaFiddle](https://javafiddle.leaningtech.com/): + + + +## Feedback + +[Report issues on GitHub](https://github.com/leaningtech/cheerpj-meta/issues) - +[Join the Discord server](https://discord.leaningtech.com/) diff --git a/src/layouts/DocsArticle.astro b/src/layouts/DocsArticle.astro index 43fe52c6..ef1ca5f2 100644 --- a/src/layouts/DocsArticle.astro +++ b/src/layouts/DocsArticle.astro @@ -38,12 +38,15 @@ const contributeLinks = [ -