├── .convco └── template │ ├── commit.hbs │ ├── footer.hbs │ ├── header.hbs │ └── template.hbs ├── .github └── workflows │ ├── brew.yaml │ ├── ci.yaml │ ├── docs.yaml │ ├── pages.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── .versionrc ├── CNAME ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── Trunk.toml ├── clippy.toml ├── examples ├── behind-reverse-proxy │ ├── README.md │ └── nginx.conf ├── cargo-manifest │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── app.css │ │ ├── index.scss │ │ └── main.rs ├── cdylib │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── index.html │ └── src │ │ ├── app.css │ │ ├── index.scss │ │ ├── lib.rs │ │ └── script.js ├── failing-rust │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── index.html │ └── src │ │ └── main.rs ├── hooks │ ├── .cargo │ │ └── config.toml │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── build.rs │ ├── index.html │ ├── src │ │ ├── index.scss │ │ └── main.rs │ └── xtask │ │ ├── Cargo.lock │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs ├── initializer │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── index.html │ └── src │ │ ├── app.css │ │ ├── initializer.mjs │ │ └── main.rs ├── leptos │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ ├── main.rs │ │ └── script.mjs ├── no-rust │ ├── Trunk.toml │ ├── index.html │ └── index.scss ├── proxy │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── docker-compose.yaml │ ├── index.html │ └── src │ │ └── main.rs ├── seed │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── example-seed.png │ ├── index.html │ ├── public │ │ └── index.css │ └── src │ │ └── main.rs ├── target-path │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── assets │ │ └── rustacean-flat-happy.png │ ├── index.html │ ├── more-assets │ │ └── rustacean-flat-happy.svg │ └── src │ │ ├── app.css │ │ ├── index.scss │ │ ├── main.rs │ │ ├── not_minified.css │ │ ├── script.js │ │ └── script.mjs ├── vanilla │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── example-vanilla.png │ ├── index.html │ └── src │ │ ├── app.css │ │ ├── index.scss │ │ ├── main.rs │ │ ├── not_minified.css │ │ ├── script.js │ │ └── script.mjs ├── wasm_threads │ ├── .cargo │ │ └── config.toml │ ├── .vscode │ │ └── settings.json │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── assets │ │ └── _headers │ ├── index.html │ ├── rust-toolchain.toml │ └── src │ │ └── main.rs ├── webworker-gloo │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Trunk.toml │ ├── index.html │ └── src │ │ ├── bin │ │ ├── app.rs │ │ └── worker.rs │ │ └── lib.rs ├── webworker-module │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Trunk.toml │ ├── index.html │ └── src │ │ └── bin │ │ ├── app.rs │ │ └── worker.rs ├── webworker │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Trunk.toml │ ├── index.html │ └── src │ │ └── bin │ │ ├── app.rs │ │ └── worker.rs ├── yaml-config │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.yaml │ ├── index.html │ └── src │ │ ├── app.css │ │ ├── index.scss │ │ └── main.rs ├── yew-tailwindcss │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── example-yew-tailwindcss.png │ ├── index.html │ ├── src │ │ ├── main.rs │ │ ├── tailwind.css │ │ └── yew.svg │ └── tailwind.config.js ├── yew-tls │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── example-yew-tls.png │ ├── index.html │ ├── self_signed_certs │ │ ├── cert.pem │ │ └── key.pem │ └── src │ │ ├── app.css │ │ ├── index.scss │ │ ├── inline-scss.scss │ │ ├── main.rs │ │ └── yew.svg └── yew │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── Trunk.toml │ ├── example-yew.png │ ├── index.html │ └── src │ ├── app.css │ ├── index.scss │ ├── inline-scss.scss │ ├── main.rs │ └── yew.svg ├── guide ├── .gitignore ├── README.md ├── book.toml ├── src │ ├── SUMMARY.md │ ├── advanced │ │ ├── index.md │ │ ├── initializer.md │ │ ├── javascript_interop.md │ │ ├── library.md │ │ ├── paths.md │ │ ├── proxy.md │ │ └── startup_event.md │ ├── assets │ │ ├── index.md │ │ ├── minification.md │ │ └── sri.md │ ├── build │ │ └── hooks.md │ ├── commands │ │ └── index.md │ ├── configuration │ │ ├── index.md │ │ └── schema.md │ ├── contributing.md │ ├── getting-started │ │ ├── index.md │ │ ├── installation.md │ │ ├── pre-reqs.md │ │ └── project.md │ └── introduction.md └── theme │ ├── pagetoc.css │ └── pagetoc.js ├── schemas └── config.json ├── site ├── config.toml ├── content │ ├── _index.md │ ├── advanced.md │ ├── assets.md │ ├── commands.md │ └── configuration.md ├── sass │ ├── blog-post.scss │ └── custom.scss ├── static │ ├── rustacean-flat-happy.png │ └── rustacean-flat-happy.svg └── templates │ ├── blog-post.html │ ├── blog.html │ └── index.html ├── src ├── autoreload.js ├── build.rs ├── cmd │ ├── build.rs │ ├── clean.rs │ ├── config.rs │ ├── core.rs │ ├── mod.rs │ ├── serve.rs │ ├── tools.rs │ └── watch.rs ├── common │ ├── html_rewrite.rs │ └── mod.rs ├── config │ ├── manifest.rs │ ├── mod.rs │ ├── models │ │ ├── build.rs │ │ ├── clean.rs │ │ ├── core.rs │ │ ├── hook.rs │ │ ├── mod.rs │ │ ├── proxy.rs │ │ ├── serve.rs │ │ ├── source │ │ │ ├── cargo.rs │ │ │ └── mod.rs │ │ ├── test.rs │ │ ├── tools.rs │ │ └── watch.rs │ ├── rt │ │ ├── build.rs │ │ ├── clean.rs │ │ ├── core.rs │ │ ├── mod.rs │ │ ├── serve.rs │ │ └── watch.rs │ └── types │ │ ├── address_family.rs │ │ ├── base_url.rs │ │ ├── cross_origin.rs │ │ ├── duration.rs │ │ ├── minify.rs │ │ ├── mod.rs │ │ ├── uri.rs │ │ └── ws.rs ├── hooks.rs ├── main.rs ├── pipelines │ ├── copy_dir.rs │ ├── copy_dir_test.rs │ ├── copy_file.rs │ ├── copy_file_test.rs │ ├── css.rs │ ├── html.rs │ ├── icon.rs │ ├── inline.rs │ ├── js.rs │ ├── mod.rs │ ├── rust │ │ ├── initializer.js │ │ ├── mod.rs │ │ ├── output.rs │ │ ├── sri.rs │ │ ├── wasm_bindgen.rs │ │ └── wasm_opt.rs │ ├── sass.rs │ ├── tailwind_css.rs │ └── tailwind_css_extra.rs ├── processing │ ├── integrity.rs │ ├── minify.rs │ └── mod.rs ├── proxy.rs ├── serve │ ├── mod.rs │ └── proxy.rs ├── tls.rs ├── tools.rs ├── version │ ├── disabled.rs │ ├── enabled │ │ ├── mod.rs │ │ └── state.rs │ ├── enforce.rs │ └── mod.rs ├── watch.rs └── ws.rs └── tests └── data ├── bad-build-target.toml ├── bad-watch-ignore.toml ├── bad-watch-path.toml ├── trunk-version-any.toml ├── trunk-version-minor.toml ├── trunk-version-none.toml ├── trunk-version-prerelease.toml └── trunk-version-range.toml /.convco/template/commit.hbs: -------------------------------------------------------------------------------- 1 | * 2 | {{~#if scope}} **{{scope}}:** 3 | {{~/if}} 4 | {{#if references}} 5 | {{~#each references}} {{#if @root.linkReferences~}} 6 | [{{this.prefix}}{{this.issue}}]({{issueUrlFormat}}) 7 | {{~else}}{{this.prefix}}{{this.issue}} 8 | {{~/if}}{{/each}} 9 | {{~/if}} 10 | {{subject}} 11 | {{~#if hash}} {{#if @root.linkReferences}}([{{shortHash}}]({{commitUrlFormat}})){{else}}({{hash}}) 12 | {{~/if}} 13 | {{~/if}} 14 | 15 | -------------------------------------------------------------------------------- /.convco/template/footer.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/.convco/template/footer.hbs -------------------------------------------------------------------------------- /.convco/template/header.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if isPatch}}###{{else}}##{{/if}}{{#if @root.linkCompare}} [{{version}}]({{compareUrlFormat}}){{else}} {{version}}{{/if}}{{#if title}} "{{title}}"{{/if}}{{#if date}} ({{date}}){{/if}} -------------------------------------------------------------------------------- /.convco/template/template.hbs: -------------------------------------------------------------------------------- 1 | {{> header}} 2 | {{#if noteGroups}}{{#each noteGroups}} 3 | 4 | ## ⚠️ {{title}} 5 | 6 | {{#each notes}}* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{this.text}} 7 | {{/each}} 8 | {{/each}} 9 | {{/if}} 10 | 11 | {{#each commitGroups}} 12 | {{#if title}}{{#if @root.isPatch}} 13 | ##{{else}} 14 | ##{{/if}} {{title}}{{/if}} 15 | 16 | {{#each commits}} 17 | {{> commit root=@root}} 18 | {{/each}} 19 | {{/each}} 20 | -------------------------------------------------------------------------------- /.github/workflows/brew.yaml: -------------------------------------------------------------------------------- 1 | name: Brew 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | tag-name: 6 | description: 'The git tag name to bump the formula to.' 7 | required: true 8 | 9 | workflow_call: 10 | inputs: 11 | tag-name: 12 | type: string 13 | description: 'The git tag name to bump the formula to.' 14 | required: true 15 | secrets: 16 | HOMEBREW_GITHUB_API_TOKEN: 17 | required: true 18 | 19 | jobs: 20 | brew: 21 | name: Bump Brew Version 22 | runs-on: ubuntu-22.04 23 | steps: 24 | - name: Release | Brew 25 | uses: mislav/bump-homebrew-formula-action@v3 26 | with: 27 | tag-name: ${{ github.event.inputs.tag-name }} 28 | formula-name: trunk 29 | formula-path: Formula/t/trunk.rb 30 | env: 31 | COMMITTER_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | workflow_call: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - name: Setup | Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup | cargo-binstall 14 | run: | 15 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 16 | 17 | - name: Setup | Install | mdbook 18 | run: | 19 | cargo binstall -y mdbook 20 | 21 | - name: Setup | Install | mdbook-admonish 22 | run: | 23 | cargo binstall -y mdbook-admonish 24 | cd guide 25 | mdbook-admonish install 26 | 27 | - name: Build | Docs 28 | run: | 29 | cd guide 30 | mdbook build 31 | 32 | - name: Upload 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: book 36 | path: guide/book/html/ 37 | if-no-files-found: error 38 | -------------------------------------------------------------------------------- /.github/workflows/pages.yaml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | on: 3 | # Only run after a successful release 4 | workflow_call: 5 | # Allow running manually 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | concurrency: pages 12 | 13 | jobs: 14 | docs: 15 | uses: ./.github/workflows/docs.yaml 16 | 17 | deploy: 18 | runs-on: ubuntu-22.04 19 | needs: 20 | - docs 21 | 22 | steps: 23 | - name: Setup | Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | submodules: recursive 27 | 28 | - name: Setup | Zola 29 | run: | 30 | wget -qO- https://github.com/getzola/zola/releases/download/v0.18.0/zola-v0.18.0-x86_64-unknown-linux-gnu.tar.gz | tar -xzf - 31 | sudo install zola /usr/local/bin/zola 32 | 33 | - name: Build | Zola 34 | run: | 35 | cd site 36 | zola build 37 | 38 | - name: Fetch | Docs 39 | id: fetch-docs 40 | uses: actions/download-artifact@v4 41 | with: 42 | name: book 43 | path: site/public/guide 44 | 45 | - name: Dump | Site 46 | run: | 47 | find ./site/public 48 | 49 | - name: Deploy 50 | uses: peaceiris/actions-gh-pages@v4 51 | with: 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | publish_dir: ./site/public 54 | cname: trunkrs.dev 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist 3 | site/public/* 4 | .vscode 5 | !examples/wasm_threads/.vscode 6 | .idea/ 7 | .DS_Store 8 | /vendor 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "juice"] 2 | path = site/themes/juice 3 | url = https://github.com/huhu/juice.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | template: ".convco/template" 2 | linkReferences: false 3 | linkCompare: true 4 | header: "" -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | trunkrs.dev 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | contributing 2 | ============ 3 | We are all living beings, and what is most important is that we respect each other and work together. If you can not uphold this simple standard, then your contributions are not welcome. 4 | 5 | ## hacking 6 | Just a few simple items to keep in mind as you hack. 7 | 8 | - Pull request early and often. This helps to let others know what you are working on. **Please use GitHub's Draft PR mechanism** if your PR is not yet ready for review. 9 | - Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), a changelog will automatically be created from such commits 10 | - When making changes to the configuration, be sure to regenate the schema. This can be done by running: 11 | 12 | ```shell 13 | cargo run -- config generate-schema schemas/config.json 14 | ``` 15 | 16 | ## linting 17 | We are using clippy & rustfmt. Clippy is SO GREAT! Rustfmt ... has a lot more growing to do; however, we are using it for uniformity. 18 | 19 | Please be sure that you've configured your editor to use clippy & rustfmt, or execute them manually before submitting your code. CI will fail your PR if you do not. 20 | 21 | ## release workflow 22 | We follow [semver](https://semver.org/spec/v2.0.0.html) for versioning this system. 23 | 24 | - [ ] update `Cargo.toml` `version` & execute `cargo update` — this ensures that the `Cargo.lock` doesn't update during CI due to the new version number, which will cause CI failure. 25 | - [ ] ensure CI completes successfully. 26 | - [ ] add a new tag to the repo matching the new `Cargo.toml` `version`. Either via `git tag` or via the Github UI. 27 | - all release tags should start with the letter `v` followed by a semver version. 28 | - [ ] CI is configured for release tags and will create a new GitHub release, and will upload release artifacts to the release page. Verify that this process has completed successfully. 29 | 30 | ## SSL 31 | 32 | Trunk can use either `native-tls` or `rustls` for SSL support. `rustls` without `aws-lc-sys` is the default backend, which should build out-of-the-box on all platforms. 33 | 34 | To opt into a different one, you can use the following command: 35 | ```sh 36 | cargo build --no-default-features -F update_check,native-tls 37 | ``` 38 | If you want to use `native-tls` you can install OpenSSL using the instructions from one of the following resources: 39 | + https://stackoverflow.com/a/62729715/2961550 40 | + https://github.com/sfackler/rust-openssl/issues/1062#issuecomment-489441940 41 | Or try using [rust-openssl from PR #2139](https://github.com/sfackler/rust-openssl/pull/2139) by updating the `Cargo.toml` file with 42 | ```diff 43 | - openssl = { version = "0.10", default-features = false, optional = true } 44 | + openssl = { git = "https://github.com/micolous/rust-openssl", branch = "windows-build", default-features = false, optional = true } 45 | ``` -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | pre-build = [ 3 | "dpkg --add-architecture $CROSS_DEB_ARCH", 4 | "apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH libssl-dev" 5 | ] 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trunk 2 | 3 | [![Build Status](https://github.com/trunk-rs/trunk/actions/workflows/ci.yaml/badge.svg)](https://github.com/trunk-rs/trunk/actions) 4 | [![](https://img.shields.io/crates/v/trunk.svg?color=brightgreen&style=flat-square)](https://crates.io/crates/trunk) 5 | ![](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue?style=flat-square) 6 | [![Discord Chat](https://img.shields.io/discord/793890238267260958?logo=discord&style=flat-square)](https://discord.gg/JEPdBujTDr) 7 | [![](https://img.shields.io/crates/d/trunk?label=downloads%20%28crates.io%29&style=flat-square)](https://crates.io/crates/trunk) 8 | [![](https://img.shields.io/github/downloads/trunk-rs/trunk/total?label=downloads%20%28GH%29&style=flat-square)](https://github.com/trunk-rs/trunk/releases) 9 | ![](https://img.shields.io/homebrew/installs/dy/trunk?color=brightgreen&label=downloads%20%28brew%29&style=flat-square) 10 | 11 | **Build, bundle & ship your Rust WASM application to the web.** 12 |
13 | *”Pack your things, we’re going on an adventure!” ~ Ferris* 14 | 15 | Trunk is a WASM web application bundler for Rust. Trunk uses a simple, optional-config pattern for building & bundling WASM, JS snippets & other assets (images, css, scss) via a source HTML file. 16 | 17 | **📦 Dev server** - Trunk ships with a built-in server for rapid development workflows, as well as support for HTTP & WebSocket proxies. 18 | 19 | **🏗 Change detection** - Trunk watches your application for changes and triggers builds for you, including automatic browser reloading. 20 | 21 | ## Getting Started 22 | 23 | Head on over to the [Trunk website](https://trunkrs.dev), everything you need is there. A few quick links: 24 | 25 | - [Install](https://trunkrs.dev/#install) 26 | - Download a released binary: https://github.com/trunk-rs/trunk/releases 27 | - `cargo binstall trunk` (installing a pre-compiled binary using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall)) 28 | - `cargo install trunk --locked` (compile your own binary from crates.io) 29 | - `cargo install --git https://github.com/trunk-rs/trunk trunk` (compile your own binary from the most recent git commit) 30 | - `cargo install --path . trunk` (compile your own binary form your local source) 31 | - `brew install trunk` (installing from [Homebrew](https://brew.sh/)) 32 | - `nix-shell -p trunk` (installing from [nix packages](https://nixos.org/)) 33 | - [App Setup](https://trunkrs.dev//#app-setup) 34 | - [Assets](https://trunkrs.dev/assets/) 35 | - [Configuration](https://trunkrs.dev/configuration/) 36 | - [CLI Commands](https://trunkrs.dev/commands/) 37 | 38 | ## Examples 39 | 40 | Check out the example web applications we maintain in-repo under the `examples` directory. 41 | 42 | ## Contributing 43 | 44 | Anyone and everyone is welcome to contribute! Please review the [CONTRIBUTING.md](./CONTRIBUTING.md) document for more details. The best way to get started is to find an open issue, and then start hacking on implementing it. Letting other folks know that you are working on it, and sharing progress is a great approach. Open pull requests early and often, and please use GitHub's draft pull request feature. 45 | 46 | ### License 47 | 48 | trunk is licensed under the terms of the MIT License or the Apache License 2.0, at your choosing. 49 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | cognitive-complexity-threshold = 25 2 | allow-expect-in-tests = true 3 | -------------------------------------------------------------------------------- /examples/behind-reverse-proxy/README.md: -------------------------------------------------------------------------------- 1 | # Trunk behind a reverse proxy 2 | 3 | The idea is to have `trunk serve` running behind a reverse proxy. This may be suitable for testing, 4 | but `trunk serve` is not intended to host your application. 5 | 6 | **NOTE:** This is different to using trunk's built in proxy, which by itself acts as a reverse proxy, allowing to 7 | access other endpoints using the endpoint served by `trunk serve`. 8 | 9 | **NOTE**: All commands are relative to this file. 10 | 11 | ## Running the example 12 | 13 | As reverse proxy, we run an NGINX: 14 | 15 | ```shell 16 | podman run --rm --network=host -ti -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:z docker.io/library/nginx:latest 17 | ``` 18 | 19 | And then we serve the "vanilla" example using trunk: 20 | 21 | ```shell 22 | trunk serve ../vanilla/Trunk.toml --public-url /my-app --serve-base / 23 | ``` 24 | 25 | Or, using the development version: 26 | 27 | ```shell 28 | cargo run --manifest-path=../../Cargo.toml -- serve --config ../vanilla/Trunk.toml --public-url /my-app --serve-base / 29 | ``` 30 | 31 | ## Trying it out 32 | 33 | When you go to , you will see the NGINX welcome screen. 34 | 35 | When you go to , you will see the application served by `trunk`. 36 | -------------------------------------------------------------------------------- /examples/behind-reverse-proxy/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | keepalive_timeout 65; 22 | 23 | server { 24 | listen 9090; 25 | 26 | location /my-app/.well-known/trunk/ws { 27 | proxy_pass http://localhost:8080/.well-known/trunk/ws; 28 | 29 | proxy_http_version 1.1; 30 | proxy_set_header Upgrade $http_upgrade; 31 | proxy_set_header Connection "Upgrade"; 32 | } 33 | location /my-app/ { 34 | proxy_pass http://localhost:8080/; 35 | 36 | # required for trunk to create correct URLs 37 | proxy_set_header Host $http_host; 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/cargo-manifest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-manifest-example" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = [ 11 | "console", 12 | "Document", 13 | "HtmlElement", 14 | "Node", 15 | "Text", 16 | "Window", 17 | ] } 18 | 19 | [package.metadata.trunk.build] 20 | target = "index.html" 21 | dist = "dist" 22 | 23 | [package.metadata.trunk.serve] 24 | port = 9090 25 | -------------------------------------------------------------------------------- /examples/cargo-manifest/README.md: -------------------------------------------------------------------------------- 1 | Trunk | cargo-manifest 2 | ========================= 3 | An example application demonstrating building a vanilla Rust (no frameworks) WASM web application having the 4 | configuration in the `Cargo.toml` file instead. 5 | 6 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the 7 | web application rendered in your browser. 8 | 9 | -------------------------------------------------------------------------------- /examples/cargo-manifest/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Cargo Manifest Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/cargo-manifest/src/app.css: -------------------------------------------------------------------------------- 1 | body {} -------------------------------------------------------------------------------- /examples/cargo-manifest/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/cargo-manifest/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::window; 6 | 7 | fn start_app() { 8 | let document = window() 9 | .and_then(|win| win.document()) 10 | .expect("Could not access document"); 11 | let body = document.body().expect("Could not access document.body"); 12 | let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); 13 | body.append_child(text_node.as_ref()) 14 | .expect("Failed to append text"); 15 | } 16 | 17 | fn main() { 18 | set_panic_hook(); 19 | start_app(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/cdylib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cdylib-example" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | console_error_panic_hook = "0.1" 12 | wasm-bindgen = "0.2" 13 | wasm-bindgen-futures = "0.4" 14 | web-sys = { version = "0.3", features = ["Window", "Document", "HtmlElement", "Node", "Text"] } 15 | -------------------------------------------------------------------------------- /examples/cdylib/README.md: -------------------------------------------------------------------------------- 1 | Trunk | cdylib | web-sys 2 | ========================= 3 | An example application demonstrating building a vanilla Rust (no frameworks) WASM web application using web-sys as a 4 | library. 5 | 6 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the web application rendered in your browser. 7 | 8 | ![Rendered cdylib example](example-vanilla.png) 9 | -------------------------------------------------------------------------------- /examples/cdylib/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/cdylib/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | cdylib | web-sys 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/cdylib/src/app.css: -------------------------------------------------------------------------------- 1 | body {} 2 | -------------------------------------------------------------------------------- /examples/cdylib/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/cdylib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::window; 6 | 7 | fn start_app() { 8 | let document = window() 9 | .and_then(|win| win.document()) 10 | .expect("Could not access document"); 11 | let body = document.body().expect("Could not access document.body"); 12 | let text_node = document.create_text_node("Hello, world from cdylib Rust!"); 13 | body.append_child(text_node.as_ref()) 14 | .expect("Failed to append text"); 15 | } 16 | 17 | #[wasm_bindgen(inline_js = "export function snippetTest() { console.log('Hello from JS FFI!'); }")] 18 | extern "C" { 19 | fn snippetTest(); 20 | } 21 | 22 | #[wasm_bindgen(start)] 23 | pub async fn run() { 24 | set_panic_hook(); 25 | snippetTest(); 26 | start_app(); 27 | } 28 | -------------------------------------------------------------------------------- /examples/cdylib/src/script.js: -------------------------------------------------------------------------------- 1 | function testFromJavaScript() { 2 | console.log("Hello from JavaScript"); 3 | } -------------------------------------------------------------------------------- /examples/failing-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vanilla-example" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | this-crate-should-not-exist = { git = "https://does.not.exist.trunk-dev/" } 11 | -------------------------------------------------------------------------------- /examples/failing-rust/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Failing Rust 2 | ========================= 3 | 4 | An example project with a failing Rust project. This should throw a reasonble error and not fall back to the behavior 5 | of "not finding" a Rust project. 6 | -------------------------------------------------------------------------------- /examples/failing-rust/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/failing-rust/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Failing-Rust 7 | 8 | 9 | 10 | 11 |

Trunk with a failing cargo configuration

12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/failing-rust/src/main.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/failing-rust/src/main.rs -------------------------------------------------------------------------------- /examples/hooks/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --manifest-path ./xtask/Cargo.toml --" 3 | -------------------------------------------------------------------------------- /examples/hooks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hooks-examples" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = [ 11 | "console", 12 | "Document", 13 | "HtmlElement", 14 | "Node", 15 | "Text", 16 | "Window", 17 | ] } 18 | 19 | 20 | [features] 21 | default = [] 22 | a = [] 23 | 24 | -------------------------------------------------------------------------------- /examples/hooks/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Hooks 2 | ========================= 3 | An example using different hooks and ideas to run code during the build. 4 | 5 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the 6 | web application rendered in your browser and some output on the browser's console. 7 | 8 | ## Hook 9 | 10 | There is one `xtask` based build hook. Check out [`cargo-xtask`](https://github.com/matklad/cargo-xtask) to get better 11 | understanding of the pattern, as it's not an actual command you install. 12 | 13 | The `xtask` is triggered by Trunk's hook system and has access to the `TRUNK_*` environment variables during the hook 14 | execution. 15 | 16 | # `build.rs` 17 | 18 | You can also use a simple [`build.rs` build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html). This 19 | will be executed during the build of the main WASM application. It will be run on the host system though (not using 20 | `wasm32-unknown-unknown`). 21 | 22 | It does not have access to the Trunk env-vars, but does have access to all kind of `cargo` information, like the 23 | features enabled during compilation. If you run this example with `trunk --features a`, you will see a different output 24 | on the console. 25 | -------------------------------------------------------------------------------- /examples/hooks/Trunk.toml: -------------------------------------------------------------------------------- 1 | [watch] 2 | ignore = [ 3 | "xtask/target" 4 | ] 5 | 6 | # This uses the idea of cargo xtask, writing code in Rust rather than an OS dependent shell. 7 | # 8 | # Of course, you can also just use bash. 9 | [[hooks]] 10 | stage = "build" 11 | command = "cargo" 12 | command_arguments = ["xtask"] 13 | -------------------------------------------------------------------------------- /examples/hooks/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let function = if std::env::var_os("CARGO_FEATURE_A").is_some() { 3 | r#"function itDepends() { 4 | console.log("Feature A is active"); 5 | }"# 6 | } else { 7 | r#"function itDepends() { 8 | console.log("Feature A is not active"); 9 | }"# 10 | }; 11 | 12 | std::fs::write("dist/.stage/buildGenerated.js", function).expect("must write"); 13 | } 14 | -------------------------------------------------------------------------------- /examples/hooks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Hooks | web-sys 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/hooks/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/hooks/src/main.rs: -------------------------------------------------------------------------------- 1 | use console_error_panic_hook::set_once as set_panic_hook; 2 | use web_sys::window; 3 | 4 | fn start_app() { 5 | let document = window() 6 | .and_then(|win| win.document()) 7 | .expect("Could not access document"); 8 | let body = document.body().expect("Could not access document.body"); 9 | let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); 10 | body.append_child(text_node.as_ref()) 11 | .expect("Failed to append text"); 12 | } 13 | 14 | fn main() { 15 | set_panic_hook(); 16 | start_app(); 17 | } 18 | -------------------------------------------------------------------------------- /examples/hooks/xtask/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "deranged" 7 | version = "0.3.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 10 | dependencies = [ 11 | "powerfmt", 12 | ] 13 | 14 | [[package]] 15 | name = "num-conv" 16 | version = "0.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 19 | 20 | [[package]] 21 | name = "powerfmt" 22 | version = "0.2.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 25 | 26 | [[package]] 27 | name = "proc-macro2" 28 | version = "1.0.83" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" 31 | dependencies = [ 32 | "unicode-ident", 33 | ] 34 | 35 | [[package]] 36 | name = "quote" 37 | version = "1.0.36" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 40 | dependencies = [ 41 | "proc-macro2", 42 | ] 43 | 44 | [[package]] 45 | name = "serde" 46 | version = "1.0.202" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" 49 | dependencies = [ 50 | "serde_derive", 51 | ] 52 | 53 | [[package]] 54 | name = "serde_derive" 55 | version = "1.0.202" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" 58 | dependencies = [ 59 | "proc-macro2", 60 | "quote", 61 | "syn", 62 | ] 63 | 64 | [[package]] 65 | name = "syn" 66 | version = "2.0.66" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 69 | dependencies = [ 70 | "proc-macro2", 71 | "quote", 72 | "unicode-ident", 73 | ] 74 | 75 | [[package]] 76 | name = "time" 77 | version = "0.3.36" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 80 | dependencies = [ 81 | "deranged", 82 | "num-conv", 83 | "powerfmt", 84 | "serde", 85 | "time-core", 86 | ] 87 | 88 | [[package]] 89 | name = "time-core" 90 | version = "0.1.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 93 | 94 | [[package]] 95 | name = "unicode-ident" 96 | version = "1.0.12" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 99 | 100 | [[package]] 101 | name = "xtask" 102 | version = "0.1.0" 103 | dependencies = [ 104 | "time", 105 | ] 106 | -------------------------------------------------------------------------------- /examples/hooks/xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | time = "0.3" 9 | -------------------------------------------------------------------------------- /examples/hooks/xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | fn main() { 4 | let dist = PathBuf::from(std::env::var_os("TRUNK_STAGING_DIR").expect("unable eval dist dir")); 5 | 6 | let path = dist.join("generated.js"); 7 | 8 | println!("Generating file: {}", path.display()); 9 | 10 | let time = time::OffsetDateTime::now_utc(); 11 | 12 | std::fs::write( 13 | path, 14 | format!( 15 | r#"function generatedFunction() {{ 16 | console.log("I was generated at {time}"); 17 | }}"# 18 | ), 19 | ) 20 | .expect("must write file"); 21 | } 22 | -------------------------------------------------------------------------------- /examples/initializer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "initializer-example" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = ["Window", "Document", "HtmlElement", "Node", "Text"] } 11 | -------------------------------------------------------------------------------- /examples/initializer/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Initializer | web-sys 2 | ========================= 3 | An example application demonstrating building a WASM application with some custom initialization logic. 4 | 5 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the web application rendered in your browser. 6 | 7 | -------------------------------------------------------------------------------- /examples/initializer/Trunk.toml: -------------------------------------------------------------------------------- 1 | trunk-version = ">=0.19.0" 2 | 3 | [build] 4 | target = "index.html" 5 | dist = "dist" 6 | -------------------------------------------------------------------------------- /examples/initializer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Initializer | web-sys 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/initializer/src/app.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-size: 20pt; 4 | color: #111; 5 | font-family: sans-serif; 6 | } 7 | -------------------------------------------------------------------------------- /examples/initializer/src/initializer.mjs: -------------------------------------------------------------------------------- 1 | export default function myInitializer () { 2 | return { 3 | onStart: () => { 4 | console.log("Loading..."); 5 | console.time("trunk-initializer"); 6 | }, 7 | onProgress: ({current, total}) => { 8 | if (!total) { 9 | console.log("Loading...", current, "bytes"); 10 | } else { 11 | console.log("Loading...", Math.round((current/total) * 100), "%" ) 12 | } 13 | }, 14 | onComplete: () => { 15 | console.log("Loading... done!"); 16 | console.timeEnd("trunk-initializer"); 17 | }, 18 | onSuccess: (wasm) => { 19 | console.log("Loading... successful!"); 20 | console.log("WebAssembly: ", wasm); 21 | }, 22 | onFailure: (error) => { 23 | console.warn("Loading... failed!", error); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /examples/initializer/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use web_sys::window; 5 | 6 | fn start_app() { 7 | let document = window() 8 | .and_then(|win| win.document()) 9 | .expect("Could not access document"); 10 | let body = document.body().expect("Could not access document.body"); 11 | let text_node = 12 | document.create_text_node("Hello, world from Vanilla Rust with an initializer!"); 13 | body.append_child(text_node.as_ref()) 14 | .expect("Failed to append text"); 15 | } 16 | 17 | fn main() { 18 | set_panic_hook(); 19 | start_app(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/leptos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leptos-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | leptos = { version = "0.6.11", features = ["csr"] } 8 | -------------------------------------------------------------------------------- /examples/leptos/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Leptos 2 | ========================= 3 | An example application demonstrating building a WASM application with Leptos. 4 | 5 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the 6 | web application rendered in your browser. 7 | -------------------------------------------------------------------------------- /examples/leptos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Leptos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/leptos/src/main.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | 3 | #[component] 4 | pub fn App() -> impl IntoView { 5 | view! { 6 | 7 | 8 | My Leptos App 9 | 10 | 11 |

"Hello, world!"

12 | 13 | 14 | 15 | } 16 | } 17 | 18 | fn main() { 19 | mount_to_body(|| view! { }); 20 | } 21 | -------------------------------------------------------------------------------- /examples/leptos/src/script.mjs: -------------------------------------------------------------------------------- 1 | console.log("Hello World"); 2 | -------------------------------------------------------------------------------- /examples/no-rust/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/no-rust/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | No-Rust 7 | 8 | 9 | 10 | 11 | 12 |

Trunk without WASM

13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/no-rust/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | 9 | h1 { 10 | text-align: center; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/proxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxy-example" 3 | version = "0.1.0" 4 | authors = ["Anthony Dodd "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = ["Window", "Document", "HtmlElement", "Node", "Text"] } 11 | -------------------------------------------------------------------------------- /examples/proxy/README.md: -------------------------------------------------------------------------------- 1 | Trunk Proxy 2 | =========== 3 | An example demonstrating how to use the Trunk proxy system for various HTTP endpoints as well as WebSockets. 4 | 5 | There isn't much going on in this example as far as WASM is concerned, but have a look at the [`Trunk.toml`](./Trunk.toml) in this directory for exhaustive examples on how to configure and use Trunk proxies. 6 | 7 | ## Setup 8 | First, in a shell terminal, execute `docker compose run --service-ports echo-server`. This will start an echo server container which we will use as a generic backend for the proxies we've defined in this example. 9 | 10 | Next, in a different shell tab or window, execute `trunk serve`. This will build the demo application and will ultimately start the Trunk proxies defined in the [`Trunk.toml`](./Trunk.toml). 11 | 12 | From here, use cURL, xh, websocat or any other tool you would like to verify the proxies' functionality. All normal HTTP requests will be echoed back, and all WebSocket messages will be echoed back. 13 | -------------------------------------------------------------------------------- /examples/proxy/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | 5 | [[proxy]] 6 | # This WebSocket proxy example has a backend and ws field. This example will listen for 7 | # WebSocket connections at `/api/ws` and proxy them to `ws://localhost:9000/api/ws`. 8 | backend = "ws://localhost:9090/api/ws" 9 | ws = true 10 | 11 | [[proxy]] 12 | # Same as above, except it will listen at `/api/websocket` and will 13 | # proxy to `ws://localhost:9000/api/ws`. 14 | rewrite = "/api/websocket" 15 | backend = "ws://localhost:9090/api/ws" 16 | ws = true 17 | 18 | [[proxy]] 19 | # This proxy example has a backend and a rewrite field. Requests received on `rewrite` will be 20 | # proxied to the backend after stripping the `rewrite`. 21 | # E.G., `/api/v1/resource/x/y/z` -> `/resource/x/y/z` 22 | rewrite = "/api/v1/" 23 | backend = "http://localhost:9090/" 24 | request_headers = { "x-api-key" = "some-special-key" } 25 | 26 | [[proxy]] 27 | # This proxy specifies only the backend, which is the only required field. In this example, 28 | # request URIs are not modified when proxied. 29 | backend = "http://localhost:9090/api/v2/" 30 | -------------------------------------------------------------------------------- /examples/proxy/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | echo-server: 4 | image: jmalloc/echo-server:latest 5 | ports: 6 | - "9090:9090" 7 | environment: 8 | PORT: "9090" 9 | -------------------------------------------------------------------------------- /examples/proxy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk Proxy 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/proxy/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use web_sys::window; 5 | 6 | fn start_app() { 7 | let document = window().and_then(|win| win.document()).expect("could not access document"); 8 | let body = document.body().expect("could not access document.body"); 9 | let text_node = document.create_text_node("Trunk Proxy Demo App"); 10 | body.append_child(text_node.as_ref()).expect("failed to append text"); 11 | } 12 | 13 | fn main() { 14 | set_panic_hook(); 15 | start_app(); 16 | } 17 | -------------------------------------------------------------------------------- /examples/seed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seed-example" 3 | version = "0.1.0" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | enclose = "1" 9 | indexmap = { version = "1", features = ["serde-1"] } 10 | seed = "0.9" 11 | serde = "1" 12 | uuid = { version = "1", features = ["serde", "v4"] } 13 | wasm-bindgen = "0.2" 14 | -------------------------------------------------------------------------------- /examples/seed/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Seed 2 | ============ 3 | 4 | An example application demonstrating building a WASM web application using Trunk & Seed taken from: [Seed TodoMVC Example](https://github.com/seed-rs/seed/tree/master/examples/todomvc) 5 | 6 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the web application rendered in your browser. 7 | 8 | ![Rendered TodoMVC example](example-seed.png) 9 | -------------------------------------------------------------------------------- /examples/seed/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/seed/example-seed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/seed/example-seed.png -------------------------------------------------------------------------------- /examples/seed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Seed 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/target-path/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vanilla-example" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = ["Window", "Document", "HtmlElement", "Node", "Text"] } 11 | -------------------------------------------------------------------------------- /examples/target-path/README.md: -------------------------------------------------------------------------------- 1 | Trunk | target-dirs 2 | ========================= 3 | An example application demonstrating create a more complex target directory structure in the dist folder, based on 4 | the vanilla example. 5 | 6 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the 7 | web application rendered in your browser. 8 | -------------------------------------------------------------------------------- /examples/target-path/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/target-path/assets/rustacean-flat-happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/target-path/assets/rustacean-flat-happy.png -------------------------------------------------------------------------------- /examples/target-path/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | target-path 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
TestOutcomeExpected
copy-dirFerris the CrabShould see PNG image
copy-fileFerris the CrabShould see SVG image
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/target-path/src/app.css: -------------------------------------------------------------------------------- 1 | body {} -------------------------------------------------------------------------------- /examples/target-path/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/target-path/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::window; 6 | 7 | fn start_app() { 8 | let document = window() 9 | .and_then(|win| win.document()) 10 | .expect("Could not access document"); 11 | let body = document.body().expect("Could not access document.body"); 12 | let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); 13 | body.append_child(text_node.as_ref()) 14 | .expect("Failed to append text"); 15 | } 16 | 17 | #[wasm_bindgen(inline_js = "export function snippetTest() { console.log('Hello from JS FFI!'); }")] 18 | extern "C" { 19 | fn snippetTest(); 20 | } 21 | 22 | fn main() { 23 | set_panic_hook(); 24 | snippetTest(); 25 | start_app(); 26 | } 27 | -------------------------------------------------------------------------------- /examples/target-path/src/not_minified.css: -------------------------------------------------------------------------------- 1 | .should_not_be_minified:checked:hover { 2 | --empty-prop: ; 3 | background-color: black; 4 | } 5 | -------------------------------------------------------------------------------- /examples/target-path/src/script.js: -------------------------------------------------------------------------------- 1 | function testFromJavaScript() { 2 | console.log("Hello from JavaScript"); 3 | } -------------------------------------------------------------------------------- /examples/target-path/src/script.mjs: -------------------------------------------------------------------------------- 1 | function testFromJavaScriptModule() { 2 | console.log("Hello from JavaScript Module"); 3 | } 4 | 5 | testFromJavaScriptModule(); 6 | -------------------------------------------------------------------------------- /examples/vanilla/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vanilla-example" 3 | version = "0.1.0" 4 | authors = ["Philip Peterson "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = [ 11 | "console", 12 | "Document", 13 | "HtmlElement", 14 | "Node", 15 | "Text", 16 | "Window", 17 | ] } 18 | -------------------------------------------------------------------------------- /examples/vanilla/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Vanilla | web-sys 2 | ========================= 3 | An example application demonstrating building a vanilla Rust (no frameworks) WASM web application using web-sys. 4 | 5 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the web application rendered in your browser. 6 | 7 | ![Rendered Vanilla example](example-vanilla.png) 8 | -------------------------------------------------------------------------------- /examples/vanilla/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/vanilla/example-vanilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/vanilla/example-vanilla.png -------------------------------------------------------------------------------- /examples/vanilla/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Vanilla | web-sys 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/vanilla/src/app.css: -------------------------------------------------------------------------------- 1 | body {} -------------------------------------------------------------------------------- /examples/vanilla/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/vanilla/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::window; 6 | 7 | fn start_app() { 8 | let document = window() 9 | .and_then(|win| win.document()) 10 | .expect("Could not access document"); 11 | let body = document.body().expect("Could not access document.body"); 12 | let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); 13 | body.append_child(text_node.as_ref()) 14 | .expect("Failed to append text"); 15 | } 16 | 17 | #[wasm_bindgen(inline_js = "export function snippetTest() { console.log('Hello from JS FFI!'); }")] 18 | extern "C" { 19 | fn snippetTest(); 20 | } 21 | 22 | #[wasm_bindgen] 23 | pub fn wasm_ffi() { 24 | web_sys::console::log_1(&"Hello from WASM!".into()); 25 | } 26 | 27 | fn main() { 28 | set_panic_hook(); 29 | snippetTest(); 30 | start_app(); 31 | } 32 | -------------------------------------------------------------------------------- /examples/vanilla/src/not_minified.css: -------------------------------------------------------------------------------- 1 | .should_not_be_minified:checked:hover { 2 | --empty-prop: ; 3 | background-color: black; 4 | } 5 | -------------------------------------------------------------------------------- /examples/vanilla/src/script.js: -------------------------------------------------------------------------------- 1 | function testFromJavaScript() { 2 | console.log("Hello from JavaScript"); 3 | } -------------------------------------------------------------------------------- /examples/vanilla/src/script.mjs: -------------------------------------------------------------------------------- 1 | function testFromJavaScriptModule() { 2 | console.log("Hello from JavaScript Module"); 3 | } 4 | 5 | testFromJavaScriptModule(); 6 | -------------------------------------------------------------------------------- /examples/wasm_threads/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Translating complex cargo invocation into this config file, so that trunk will use the same setup 2 | # https://github.com/chemicstry/wasm_thread/blob/main/build_wasm.sh 3 | 4 | [target.wasm32-unknown-unknown] 5 | rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"] 6 | 7 | [unstable] 8 | build-std = ["std,panic_abort"] 9 | -------------------------------------------------------------------------------- /examples/wasm_threads/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // The build-std flag requires an explicit target. 3 | // This breaks the usual rust-analyzer setup. 4 | // But setting the target manually for the workspace fixes that. 5 | // "rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu", 6 | "rust-analyzer.cargo.target": "wasm32-unknown-unknown", 7 | } -------------------------------------------------------------------------------- /examples/wasm_threads/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_threads" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.7" 8 | console_log = { version = "1.0.0", features = ["color"] } 9 | log = "0.4.22" 10 | wasm_thread = "0.3.0" 11 | 12 | # Optimize all dependencies even in debug builds: 13 | [profile.dev.package."*"] 14 | opt-level = 2 15 | -------------------------------------------------------------------------------- /examples/wasm_threads/README.md: -------------------------------------------------------------------------------- 1 | # Web Workers with shared memory, using only a single WASM binary 2 | 3 | This is a port of the [wasm_threads `simple.rs` example](https://github.com/chemicstry/wasm_thread/tree/main?tab=readme-ov-file#simple). 4 | 5 | It should also work similarly with `wasm-bindgen-rayon` and other packages that use SharedArrayBuffer. 6 | 7 | An explanation of this approach is described [here](https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html). 8 | 9 | ## Limitations 10 | 11 | It has some significant advantages over the `webworker*' examples, but also some significant disadvantages. 12 | 13 | For starters, it requires cross-site isolation (setting 2 headers), which is [required for this approach to workers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements). 14 | We've added it to the trunk config. 15 | 16 | These same headers are also required for deployment. Github Pages does not allow setting headers, and alternatives such as using `` did not work in my tests, so these sites can't be deployed that way. Cloudflare Pages is a free alternative that does allow headers, and it worked for me. 17 | 18 | It also requires nightly Rust, because [the standard library needs to be rebuilt](https://github.com/RReverser/wasm-bindgen-rayon?tab=readme-ov-file#building-rust-code). 19 | 20 | Some libraries may not work correctly in this way, as they were written assuming the historical single-threaded wasm32 runtimes. wgpu` has the `fragile-send-sync-non-atomic-wasm` flag which, if set, will not work with this. For example, `egui` currently sets this flag, although it can be removed with a manual, not well tested [patch](https://github.com/9SMTM6/egui/commit/11b00084e34c8b0ff40bac82274291dff64c26db). 21 | 22 | Additional restrictions are listed [here](https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html#caveats) (some of which may be solved or worked around in libraries). Limitations specific to `wasm_thread' are explained in the source code comments. 23 | 24 | ## Advantages 25 | 26 | * Code sharing 27 | * Improves developer experience 28 | * Also means that the WASM binary is shared, which in extreme cases can be half the size of the deployed application. 29 | * Avoids the [web-worker cache-invalidation issue](https://github.com/trunk-rs/trunk/issues/405) 30 | * Memory sharing between threads 31 | * can be a huge performance gain 32 | 33 | 34 | ## Notes on applying this 35 | 36 | Note that this requires the [toolchain file](./rust-toolchain.toml) and the [cargo config](.cargo/config.toml). 37 | 38 | The `_headers` file and its copy in `index.html` is simply an example of how to set the headers using Cloudflare Pages. 39 | 40 | If you receive errors such as 41 | 42 | > [Firefox] The WebAssembly.Memory object cannot be serialized. The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers can be used to enable this. 43 | 44 | > [Chrome] SharedArrayBuffer transfer requires self.crossOriginIsolated. 45 | 46 | Then the headers are not set correctly. You can check the response headers on the `/` file in the Network tab of the browser developer tools. 47 | 48 | Errors such as 49 | > InternalError: too much recursion 50 | 51 | Were solved in the example by enabling optimizations. It was sufficient to do this on dependencies only, with 52 | 53 | ``` 54 | # in Cargo.toml 55 | # Optimize all dependencies even in debug builds: 56 | [profile.dev.package."*"] 57 | opt-level = 2 58 | ``` 59 | 60 | This will slow down clean rebuilds, but should not affect rebuild speed during normal development. 61 | 62 | ## Using rust-analyzer 63 | 64 | Since we use the build-std flag in the toolchain file, and that requires an explicit target to be set for compilation etc., this will break rust-analyzer in many setups. This can be solved by specifying an explicit target for the workspace, such as with the provided [config file for vscode](./.vscode/settings.json). 65 | -------------------------------------------------------------------------------- /examples/wasm_threads/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | 5 | [serve.headers] 6 | # see ./assets/_headers for more documentation 7 | "cross-origin-embedder-policy"= "require-corp" 8 | "cross-origin-opener-policy"= "same-origin" 9 | "cross-origin-resource-policy"= "same-site" 10 | -------------------------------------------------------------------------------- /examples/wasm_threads/assets/_headers: -------------------------------------------------------------------------------- 1 | # Sets headers on cloudflare pages as required for wasm_threads, because they rely on SharedArrayBuffer. 2 | # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements 3 | # https://developers.cloudflare.com/pages/configuration/headers/ 4 | /* 5 | # Alternatively `credentialless` also works 6 | # MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy 7 | cross-origin-embedder-policy: require-corp 8 | cross-origin-opener-policy: same-origin 9 | # not strictly required, just allows you to load assets from the same... subdomain IIRC. 10 | cross-origin-resource-policy: same-site 11 | -------------------------------------------------------------------------------- /examples/wasm_threads/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Trunk | wasm_threads 8 | 9 | 10 | 11 | 12 | 19 | 20 |

See the console for the thread output

21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/wasm_threads/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | # Before upgrading check that everything is available on all tier1 targets here: 2 | # https://rust-lang.github.io/rustup-components-history 3 | [toolchain] 4 | channel = "nightly-2024-08-02" 5 | targets = ["wasm32-unknown-unknown"] 6 | components = ["rust-src", "rustfmt", "clippy"] 7 | -------------------------------------------------------------------------------- /examples/wasm_threads/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use wasm_thread as thread; 4 | 5 | fn main() { 6 | #[cfg(target_arch = "wasm32")] 7 | { 8 | console_log::init().unwrap(); 9 | console_error_panic_hook::set_once(); 10 | } 11 | 12 | #[cfg(not(target_arch = "wasm32"))] 13 | env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info")); 14 | 15 | log::info!("Available parallelism: {:?}", thread::available_parallelism()); 16 | 17 | let mut threads = vec![]; 18 | 19 | for _ in 0..2 { 20 | threads.push(thread::spawn(|| { 21 | for i in 1..3 { 22 | log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id()); 23 | thread::sleep(Duration::from_millis(1)); 24 | } 25 | })); 26 | } 27 | 28 | for i in 1..3 { 29 | log::info!("hi number {} from the main thread {:?}!", i, thread::current().id()); 30 | } 31 | 32 | // It's not possible to do a scope on the main thread, because blocking waits are not supported, but we can use 33 | // scope inside web workers. 34 | threads.push(thread::spawn(|| { 35 | log::info!("Start scope test on thread {:?}", thread::current().id()); 36 | 37 | let mut a = vec![1, 2, 3]; 38 | let mut x = 0; 39 | 40 | thread::scope(|s| { 41 | let handle = s.spawn(|| { 42 | log::info!("hello from the first scoped thread {:?}", thread::current().id()); 43 | // We can borrow `a` here. 44 | log::info!("a = {:?}", &a); 45 | // Return a subslice of borrowed `a` 46 | &a[0..2] 47 | }); 48 | 49 | // Wait for the returned value from first thread 50 | log::info!("a[0..2] = {:?}", handle.join().unwrap()); 51 | 52 | s.spawn(|| { 53 | log::info!("hello from the second scoped thread {:?}", thread::current().id()); 54 | // We can even mutably borrow `x` here, 55 | // because no other threads are using it. 56 | x += a[0] + a[2]; 57 | }); 58 | 59 | log::info!( 60 | "Hello from scope \"main\" thread {:?} inside scope.", 61 | thread::current().id() 62 | ); 63 | }); 64 | 65 | // After the scope, we can modify and access our variables again: 66 | a.push(4); 67 | assert_eq!(x, a.len()); 68 | log::info!("Scope done x = {}, a.len() = {}", x, a.len()); 69 | })); 70 | 71 | // Wait for all threads, otherwise program exits before threads finish execution. 72 | // We can't do blocking join on wasm main thread though, but the browser window will continue running. 73 | #[cfg(not(target_arch = "wasm32"))] 74 | for handle in threads { 75 | handle.join().unwrap(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/webworker-gloo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webworker-gloo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "app" 8 | path = "src/bin/app.rs" 9 | 10 | [[bin]] 11 | name = "worker" 12 | path = "src/bin/worker.rs" 13 | 14 | [dependencies] 15 | console_error_panic_hook = "0.1" 16 | gloo-console = "0.3" 17 | gloo-worker = "0.5" 18 | wasm-bindgen = "0.2" 19 | web-sys = { version = "0.3", features = ["console"] } 20 | -------------------------------------------------------------------------------- /examples/webworker-gloo/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/webworker-gloo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Trunk | Web worker | Gloo 8 | 9 | 10 | 11 | 12 | 13 |
14 | Check the console! 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/webworker-gloo/src/bin/app.rs: -------------------------------------------------------------------------------- 1 | use gloo_console::log; 2 | use gloo_worker::Spawnable; 3 | use webworker_gloo::Multiplier; 4 | 5 | fn main() { 6 | console_error_panic_hook::set_once(); 7 | 8 | let bridge = Multiplier::spawner() 9 | .callback(move |((a, b), result)| { 10 | log!(format!("{} * {} = {}", a, b, result)); 11 | }) 12 | .spawn("./worker.js"); 13 | let bridge = Box::leak(Box::new(bridge)); 14 | 15 | bridge.send((2, 5)); 16 | bridge.send((3, 3)); 17 | bridge.send((50, 5)); 18 | } 19 | -------------------------------------------------------------------------------- /examples/webworker-gloo/src/bin/worker.rs: -------------------------------------------------------------------------------- 1 | use gloo_worker::Registrable; 2 | use webworker_gloo::Multiplier; 3 | 4 | fn main() { 5 | console_error_panic_hook::set_once(); 6 | 7 | Multiplier::registrar().register(); 8 | } 9 | -------------------------------------------------------------------------------- /examples/webworker-gloo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gloo_worker::{HandlerId, Worker, WorkerScope}; 2 | 3 | pub struct Multiplier {} 4 | 5 | impl Worker for Multiplier { 6 | type Input = (u64, u64); 7 | type Message = (); 8 | type Output = ((u64, u64), u64); 9 | 10 | fn create(_scope: &WorkerScope) -> Self { 11 | Self {} 12 | } 13 | 14 | fn update(&mut self, _scope: &WorkerScope, _msg: Self::Message) {} 15 | 16 | fn received(&mut self, scope: &WorkerScope, msg: Self::Input, id: HandlerId) { 17 | scope.respond(id, (msg, msg.0 * msg.1)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/webworker-module/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webworker-module-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1" 8 | js-sys = "0.3" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = [ 11 | "console", 12 | "DedicatedWorkerGlobalScope", 13 | "Document", 14 | "HtmlElement", 15 | "MessageEvent", 16 | "Node", 17 | "Text", 18 | "Url", 19 | "Window", 20 | "Worker", 21 | "WorkerOptions", 22 | "WorkerType", 23 | ] } 24 | -------------------------------------------------------------------------------- /examples/webworker-module/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/webworker-module/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | WebWorker | Module 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/webworker-module/src/bin/app.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Array; 2 | use wasm_bindgen::{prelude::*, JsCast}; 3 | use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType}; 4 | 5 | fn worker_new(url: &str) -> Worker { 6 | let mut options = WorkerOptions::new(); 7 | options.type_(WorkerType::Module); 8 | Worker::new_with_options(&url, &options).expect("failed to spawn worker") 9 | } 10 | 11 | fn main() { 12 | console_error_panic_hook::set_once(); 13 | 14 | let worker = worker_new("./worker_loader.js"); 15 | let worker_clone = worker.clone(); 16 | 17 | // NOTE: We must wait for the worker to report that it's ready to receive 18 | // messages. Any message we send beforehand will be discarded / ignored. 19 | // This is different from js-based workers, which can send messages 20 | // before the worker is initialized. 21 | // REASON: This is because javascript only starts processing MessageEvents 22 | // once the worker's script first yields to the javascript event loop. 23 | // For js workers this means that you can register the event listener 24 | // as first thing in the worker and will receive all previously sent 25 | // message events. However, loading wasm is an asynchronous operation 26 | // which yields to the js event loop before the wasm is loaded and had 27 | // a change to register the event listener. At that point js processes 28 | // the message events, sees that there isn't any listener registered, 29 | // and drops them. 30 | 31 | let onmessage = Closure::wrap(Box::new(move |msg: MessageEvent| { 32 | let worker_clone = worker_clone.clone(); 33 | let data = Array::from(&msg.data()); 34 | 35 | if data.length() == 0 { 36 | let msg = Array::new(); 37 | msg.push(&2.into()); 38 | msg.push(&5.into()); 39 | worker_clone 40 | .post_message(&msg.into()) 41 | .expect("sending message to succeed"); 42 | } else { 43 | let a = data 44 | .get(0) 45 | .as_f64() 46 | .expect("first array value to be a number") as u32; 47 | let b = data 48 | .get(1) 49 | .as_f64() 50 | .expect("second array value to be a number") as u32; 51 | let result = data 52 | .get(2) 53 | .as_f64() 54 | .expect("third array value to be a number") as u32; 55 | 56 | web_sys::console::log_1(&format!("{a} x {b} = {result}").into()); 57 | } 58 | }) as Box); 59 | worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); 60 | onmessage.forget(); 61 | } 62 | -------------------------------------------------------------------------------- /examples/webworker-module/src/bin/worker.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Array; 2 | use wasm_bindgen::{prelude::*, JsCast}; 3 | use web_sys::{DedicatedWorkerGlobalScope, MessageEvent}; 4 | 5 | fn main() { 6 | console_error_panic_hook::set_once(); 7 | web_sys::console::log_1(&"worker starting".into()); 8 | 9 | let scope = DedicatedWorkerGlobalScope::from(JsValue::from(js_sys::global())); 10 | let scope_clone = scope.clone(); 11 | 12 | let onmessage = Closure::wrap(Box::new(move |msg: MessageEvent| { 13 | web_sys::console::log_1(&"got message".into()); 14 | 15 | let data = Array::from(&msg.data()); 16 | let a = data 17 | .get(0) 18 | .as_f64() 19 | .expect("first array value to be a number") as u32; 20 | let b = data 21 | .get(1) 22 | .as_f64() 23 | .expect("second array value to be a number") as u32; 24 | 25 | data.push(&(a * b).into()); 26 | scope_clone 27 | .post_message(&data.into()) 28 | .expect("posting result message succeeds"); 29 | }) as Box); 30 | scope.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); 31 | onmessage.forget(); 32 | 33 | // The worker must send a message to indicate that it's ready to receive messages. 34 | scope 35 | .post_message(&Array::new().into()) 36 | .expect("posting ready message succeeds"); 37 | } 38 | -------------------------------------------------------------------------------- /examples/webworker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webworker-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1" 8 | js-sys = "0.3" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = [ 11 | "Blob", 12 | "BlobPropertyBag", 13 | "console", 14 | "DedicatedWorkerGlobalScope", 15 | "Document", 16 | "HtmlElement", 17 | "Location", 18 | "MessageEvent", 19 | "Node", 20 | "Text", 21 | "Url", 22 | "Window", 23 | "Worker", 24 | ] } 25 | -------------------------------------------------------------------------------- /examples/webworker/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/webworker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | WebWorker | Vanilla 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/webworker/src/bin/app.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Array; 2 | use wasm_bindgen::{prelude::*, JsCast}; 3 | use web_sys::{window, Blob, BlobPropertyBag, MessageEvent, Url, Worker}; 4 | 5 | fn worker_new(name: &str) -> Worker { 6 | let origin = window() 7 | .expect("window to be available") 8 | .location() 9 | .origin() 10 | .expect("origin to be available"); 11 | 12 | let script = Array::new(); 13 | script.push( 14 | &format!(r#"importScripts("{origin}/{name}.js");wasm_bindgen("{origin}/{name}_bg.wasm");"#) 15 | .into(), 16 | ); 17 | 18 | let blob = Blob::new_with_str_sequence_and_options( 19 | &script, 20 | BlobPropertyBag::new().type_("text/javascript"), 21 | ) 22 | .expect("blob creation succeeds"); 23 | 24 | let url = Url::create_object_url_with_blob(&blob).expect("url creation succeeds"); 25 | 26 | Worker::new(&url).expect("failed to spawn worker") 27 | } 28 | 29 | fn main() { 30 | console_error_panic_hook::set_once(); 31 | 32 | let worker = worker_new("worker"); 33 | let worker_clone = worker.clone(); 34 | 35 | // NOTE: We must wait for the worker to report that it's ready to receive 36 | // messages. Any message we send beforehand will be discarded / ignored. 37 | // This is different from js-based workers, which can send messages 38 | // before the worker is initialized. 39 | // REASON: This is because javascript only starts processing MessageEvents 40 | // once the worker's script first yields to the javascript event loop. 41 | // For js workers this means that you can register the event listener 42 | // as first thing in the worker and will receive all previously sent 43 | // message events. However, loading wasm is an asynchronous operation 44 | // which yields to the js event loop before the wasm is loaded and had 45 | // a change to register the event listener. At that point js processes 46 | // the message events, sees that there isn't any listener registered, 47 | // and drops them. 48 | 49 | let onmessage = Closure::wrap(Box::new(move |msg: MessageEvent| { 50 | let worker_clone = worker_clone.clone(); 51 | let data = Array::from(&msg.data()); 52 | 53 | if data.length() == 0 { 54 | let msg = Array::new(); 55 | msg.push(&2.into()); 56 | msg.push(&5.into()); 57 | worker_clone 58 | .post_message(&msg.into()) 59 | .expect("sending message to succeed"); 60 | } else { 61 | let a = data 62 | .get(0) 63 | .as_f64() 64 | .expect("first array value to be a number") as u32; 65 | let b = data 66 | .get(1) 67 | .as_f64() 68 | .expect("second array value to be a number") as u32; 69 | let result = data 70 | .get(2) 71 | .as_f64() 72 | .expect("third array value to be a number") as u32; 73 | 74 | web_sys::console::log_1(&format!("{a} x {b} = {result}").into()); 75 | } 76 | }) as Box); 77 | worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); 78 | onmessage.forget(); 79 | } 80 | -------------------------------------------------------------------------------- /examples/webworker/src/bin/worker.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Array; 2 | use wasm_bindgen::{prelude::*, JsCast}; 3 | use web_sys::{DedicatedWorkerGlobalScope, MessageEvent}; 4 | 5 | fn main() { 6 | console_error_panic_hook::set_once(); 7 | web_sys::console::log_1(&"worker starting".into()); 8 | 9 | let scope = DedicatedWorkerGlobalScope::from(JsValue::from(js_sys::global())); 10 | let scope_clone = scope.clone(); 11 | 12 | let onmessage = Closure::wrap(Box::new(move |msg: MessageEvent| { 13 | web_sys::console::log_1(&"got message".into()); 14 | 15 | let data = Array::from(&msg.data()); 16 | let a = data 17 | .get(0) 18 | .as_f64() 19 | .expect("first array value to be a number") as u32; 20 | let b = data 21 | .get(1) 22 | .as_f64() 23 | .expect("second array value to be a number") as u32; 24 | 25 | data.push(&(a * b).into()); 26 | scope_clone 27 | .post_message(&data.into()) 28 | .expect("posting result message succeeds"); 29 | }) as Box); 30 | scope.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); 31 | onmessage.forget(); 32 | 33 | // The worker must send a message to indicate that it's ready to receive messages. 34 | scope 35 | .post_message(&Array::new().into()) 36 | .expect("posting ready message succeeds"); 37 | } 38 | -------------------------------------------------------------------------------- /examples/yaml-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yaml-example" 3 | version = "0.1.0" 4 | authors = ["Jens Reimann "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | web-sys = { version = "0.3", features = [ 11 | "console", 12 | "Document", 13 | "HtmlElement", 14 | "Node", 15 | "Text", 16 | "Window", 17 | ] } 18 | 19 | [package.metadata.trunk.build] 20 | target = "index.html" 21 | dist = "dist" 22 | 23 | [package.metadata.trunk.serve] 24 | port = 9090 25 | -------------------------------------------------------------------------------- /examples/yaml-config/README.md: -------------------------------------------------------------------------------- 1 | Trunk | cargo-manifest 2 | ========================= 3 | An example application demonstrating building a vanilla Rust (no frameworks) WASM web application having the 4 | configuration in the `Trunk.yaml` file instead. 5 | 6 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the 7 | web application rendered in your browser. 8 | 9 | -------------------------------------------------------------------------------- /examples/yaml-config/Trunk.yaml: -------------------------------------------------------------------------------- 1 | $schema: "../../schemas/config.json" 2 | 3 | build: 4 | dist: dist 5 | target: index.html 6 | 7 | serve: 8 | port: 8080 9 | headers: 10 | Foo: bar 11 | -------------------------------------------------------------------------------- /examples/yaml-config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | YAML Configuration Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/yaml-config/src/app.css: -------------------------------------------------------------------------------- 1 | body {} -------------------------------------------------------------------------------- /examples/yaml-config/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | body { 5 | font-size: 20pt; 6 | color: #111; 7 | font-family: sans-serif; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/yaml-config/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::window; 6 | 7 | fn start_app() { 8 | let document = window() 9 | .and_then(|win| win.document()) 10 | .expect("Could not access document"); 11 | let body = document.body().expect("Could not access document.body"); 12 | let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); 13 | body.append_child(text_node.as_ref()) 14 | .expect("Failed to append text"); 15 | } 16 | 17 | fn main() { 18 | set_panic_hook(); 19 | start_app(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew-tailwindcss-example" 3 | version = "0.1.0" 4 | authors = ["Anthony Dodd ", "Danny Wensley "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | yew = "0.18" 11 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Yew | Tailwind 2 | ====================== 3 | An example application demonstrating building a WASM web application using Trunk, Yew & Tailwind. 4 | 5 | In order to run, this example requires a working nodejs installation that includes `npx`. 6 | 7 | Simply execute `trunk serve --open` from this example's directory, and you should see the following web application rendered in your browser. 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/example-yew-tailwindcss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/yew-tailwindcss/example-yew-tailwindcss.png -------------------------------------------------------------------------------- /examples/yew-tailwindcss/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Yew | Tailwind 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use console_error_panic_hook::set_once as set_panic_hook; 4 | use yew::prelude::*; 5 | 6 | struct App; 7 | 8 | impl Component for App { 9 | type Message = (); 10 | type Properties = (); 11 | 12 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 13 | Self 14 | } 15 | 16 | fn update(&mut self, _: Self::Message) -> bool { 17 | false 18 | } 19 | 20 | fn change(&mut self, _: Self::Properties) -> bool { 21 | false 22 | } 23 | 24 | fn view(&self) -> Html { 25 | let link_classes = 26 | "block px-4 py-2 hover:bg-black hover:text-white rounded border-black border"; 27 | let links = [ 28 | ("Trunk", "https://github.com/trunk-rs/trunk"), 29 | ("Yew", "https://yew.rs/"), 30 | ("Tailwind", "https://tailwindcss.com"), 31 | ]; 32 | html! { 33 |
34 | 43 |
44 | {view_card("Trunk", None, html! { 45 |

{"Trunk is a WASM web application bundler for Rust."}

46 | })} 47 | {view_card("Yew", Some("yew.svg"), html! { 48 |

{"Yew is a modern Rust framework for creating multi-threaded front-end web apps with WebAssembly."}

49 | })} 50 | {view_card("Tailwind CSS", None, html! { 51 |

{"Tailwind CSS is a library for styling markup using a comprehensive set of utility classes, no CSS required."}

52 | })} 53 |
54 |
55 | } 56 | } 57 | } 58 | 59 | fn view_card(title: &'static str, img_url: Option<&'static str>, content: Html) -> Html { 60 | html! { 61 |
62 | {for img_url.map(|url| html! { 63 | Logo 64 | })} 65 |

{title}

66 | {content} 67 |
68 | } 69 | } 70 | 71 | fn main() { 72 | set_panic_hook(); 73 | 74 | yew::start_app::(); 75 | } 76 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/src/yew.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/yew-tailwindcss/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "jit", 3 | content: { 4 | files: ["src/**/*.rs", "index.html"], 5 | }, 6 | darkMode: "media", // 'media' or 'class' 7 | theme: { 8 | extend: {}, 9 | }, 10 | variants: { 11 | extend: {}, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /examples/yew-tls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew-tls-example" 3 | version = "0.1.0" 4 | authors = ["Anthony Dodd "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | ybc = "0.4" 11 | yew = { version = "0.20", features = ["csr"] } 12 | 13 | [dependencies.web-sys] 14 | version = "0.3" 15 | features = [ 16 | "console", 17 | "Location", 18 | ] 19 | 20 | [features] 21 | default = [] 22 | demo-abc = [] 23 | demo-xyz = [] 24 | -------------------------------------------------------------------------------- /examples/yew-tls/README.md: -------------------------------------------------------------------------------- 1 | # Trunk | Yew | TLS 2 | 3 | An example application demonstrating building a WASM web application using Trunk, Yew & YBC serving with TLS enabled. 4 | 5 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you 6 | should see the following web application rendered in your browser. The application will be served using 7 | the self-signed TLS certificates located in `self_signed_certificates` and configured in `Trunk.toml`. 8 | 9 | ![Screenshot with TLS enabled](example-yew-tls.png) 10 | 11 | ## Creating new self-signed certificates 12 | 13 | > [!CAUTION] 14 | > Using self-signed certificates can be fine for a local, development scenario. Using them in any other scenario may be 15 | > dangerous. 16 | 17 | There are many ways to create self-signed certificates, here is one of them: 18 | 19 | ```shell 20 | openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \ 21 | -subj "/C=XX/CN=localhost" \ 22 | -keyout self_signed_certs/key.pem -out self_signed_certs/cert.pem 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/yew-tls/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | 5 | [serve] 6 | tls_key_path = "self_signed_certs/key.pem" 7 | tls_cert_path = "self_signed_certs/cert.pem" 8 | -------------------------------------------------------------------------------- /examples/yew-tls/example-yew-tls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/yew-tls/example-yew-tls.png -------------------------------------------------------------------------------- /examples/yew-tls/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Location Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/yew-tls/self_signed_certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFIzCCAwugAwIBAgIULDvgttrk8ocrRcbdlTiaV2oywvgwDQYJKoZIhvcNAQEL 3 | BQAwITELMAkGA1UEBhMCWFgxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzExMjAw 4 | NzE3MjRaFw0zMzExMTcwNzE3MjRaMCExCzAJBgNVBAYTAlhYMRIwEAYDVQQDDAls 5 | b2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCeEjH8HhEh 6 | RUt6lKvGUbzjRzEjBcdsanYIrlCd9TIkVOqcCCO+pYobvGRaklTHhjkrAZ8/FghE 7 | MEjSKdNg/YKjwXN9SB5GDdo3oFo3CntTwAfGKXoZYOOba/Dv46A98QOUd8BmN2IE 8 | 4Jlay1NfZryM0qfAE7llEYccZ5ybf+QLzhmnVp87kNe/6pLycxoEfF8ATPoZRuQg 9 | BHvNhBAz3ecaulJ46capcKMJvq4kp2EkO9q2KcLQZjFKFp2eA6G9NKLp4iGk3LaR 10 | 1OW5jJFWWCbcd8CVFClYS89jJozFieRGODg4SxBrwic0EcK+pphjwWx0O4RQZ7Sa 11 | yxpbwKkMqcAy4Q8hQ43/Mo6N3ntB3MvbbvVJrGUHhqlpwRCirWfjBXz53Lo3DOea 12 | YJc4l+ZxsmyZ2/5uLKhwlNqLFJL8oI+7K1AmVKXmQNVAfr9zaYg1o2sq7XxqnZ5o 13 | rBEV1Ie6WZvPE4mEXYb2Hr5kCkxsk4LNQ7QZs4+Wmz1FcnWYu2zikIwjDmT3FWoz 14 | OLLUcccrfPhSDJHeuZ2k3mGeHV2Kh/sb/eSqcVfpKQnrnGZkyz/3BK3e+8bUmEjH 15 | yLrLGo2SqHdTPNbQATFo7G1LB9C7GiyDPmrt5rJWjkWxAtlbZH1uS8AtEH1Gjyq4 16 | va2/2/WbDFvf0FvNSvOQ0sg9mtKciyMAlQIDAQABo1MwUTAdBgNVHQ4EFgQU7akd 17 | LXFHw/Wrcejg5vj4Wh1nd1QwHwYDVR0jBBgwFoAU7akdLXFHw/Wrcejg5vj4Wh1n 18 | d1QwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAIJP37/DT1w0i 19 | kFLsxJhGVteoVJgOLBqanlzUPkTuLIb20krqBGbKdF2TqD6K6JdwbarDyF9ji9AJ 20 | Z8EEvFfi28dDJEG9umlWFsxLsxkNZ4a7pNe8nWVaHqnBVn9aS3JSNOQ/+rbtrmAo 21 | /Jp0Aem4Ti6adKTTdpDedYSiI6xvO8VAC0+Wyjv4h/qBlfpfcMXCa1nZlYAU+KDk 22 | B6zcNLbMC9eydd6K1E0/T8uhQXYPTXSnxWKDoDAGWsY80fD52BiuspyG9KYH6wg2 23 | 5fw1mSi0qG2kDCdi6pm/ntvP6//F+E4FQhD0oBJrUiaYcxuh76wCMj97kEt9nGVL 24 | wPd3lLp06XIsVbKWXHqTbt/al3tMx4kiB+2+wcJbjrkvE86uDsZKJ0QoJD+h9qCQ 25 | SEDexAVRI52Ay3jKc6pN+JXpQ6lWwa5V0ATDp025krnDcsreiTXEehfhj1sk4anI 26 | rOzYGpCdmHc87uvXCUsJ0/omAyk3MXh+nRYOHJttAEZFL02OKPUIW+i05bIGdZXA 27 | I9iy3XIp8lclCkgHHdSzBIubXzDa+zJGHlKiaH+NsLz6DT2bkMXXNywfIx66P6ph 28 | bUX13t/73SUYU1P6xn7nZH5sWxKbMVgnUKJmuYjmaMnlGT/uZUTY0eFbsSOSkx33 29 | Ue2YKh9hpPUAeSzmzsyNFveNRN4SCd4= 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /examples/yew-tls/self_signed_certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCeEjH8HhEhRUt6 3 | lKvGUbzjRzEjBcdsanYIrlCd9TIkVOqcCCO+pYobvGRaklTHhjkrAZ8/FghEMEjS 4 | KdNg/YKjwXN9SB5GDdo3oFo3CntTwAfGKXoZYOOba/Dv46A98QOUd8BmN2IE4Jla 5 | y1NfZryM0qfAE7llEYccZ5ybf+QLzhmnVp87kNe/6pLycxoEfF8ATPoZRuQgBHvN 6 | hBAz3ecaulJ46capcKMJvq4kp2EkO9q2KcLQZjFKFp2eA6G9NKLp4iGk3LaR1OW5 7 | jJFWWCbcd8CVFClYS89jJozFieRGODg4SxBrwic0EcK+pphjwWx0O4RQZ7Sayxpb 8 | wKkMqcAy4Q8hQ43/Mo6N3ntB3MvbbvVJrGUHhqlpwRCirWfjBXz53Lo3DOeaYJc4 9 | l+ZxsmyZ2/5uLKhwlNqLFJL8oI+7K1AmVKXmQNVAfr9zaYg1o2sq7XxqnZ5orBEV 10 | 1Ie6WZvPE4mEXYb2Hr5kCkxsk4LNQ7QZs4+Wmz1FcnWYu2zikIwjDmT3FWozOLLU 11 | cccrfPhSDJHeuZ2k3mGeHV2Kh/sb/eSqcVfpKQnrnGZkyz/3BK3e+8bUmEjHyLrL 12 | Go2SqHdTPNbQATFo7G1LB9C7GiyDPmrt5rJWjkWxAtlbZH1uS8AtEH1Gjyq4va2/ 13 | 2/WbDFvf0FvNSvOQ0sg9mtKciyMAlQIDAQABAoICACMAhHD/d2uWnJAYO7E+iEUL 14 | jqXisbYgHOLvNorDsMa6xvimFMQyg9KAdPhZTsyfvCj5wvmFN29iAb4H5PalLDh2 15 | rcwgHBQHHTsUFuX5PDXfX9lRdegIvF2+GSwFi4YryE8n9UKtINdTpKGmRF/rr/S+ 16 | 8MZnl7X5m6F+j/8e4rB6gtO7KqdV+3BT0RY+xpQXtjGq2fae7ATq+T8Y7z2M9Y/F 17 | kl2T5pm+h1uxDX6wUaJkXpnXxXvAJUnymBRVWYs8DJcdq7WF6ieArR3ZMpWcD37i 18 | RZK9VCppHxMlhiZ30kR6Tniopoo2A9hT1nvBCB2OxyU1yKbFUAi20K9gig5zyEsi 19 | WxpIVWGu849h8PVcQjDFVg5N7lAYzliM9SnGEchLhLtIWo3TeUBvPMm+HPT4biIF 20 | MWFUEqrt4gAMlDg8UUUGql/DXNegReeaGNnBC4uPa5i4bur8mweYbfpAgppjLdcj 21 | Ep4XPUcSRCqF5IiwwEKC69UV0jFEvo3ktUq9feTb2xcbNy2eir4WmCxV0etp3d+d 22 | /jMObluwS2vRZnP2cLnnKIARaEae//lAE0OzAtQ3s7IZWWAA1Qkj0rUWPuAL3ggD 23 | lvhT8LE0w3ESftWuDSvkM2+bCBB9YdQ+vQUFmSvOR18/PXz5kcp6pFf6CSyuMGaP 24 | qQGJ4Ki175A2PJ17P7xBAoIBAQDe+5fI8JIiQFi98wpguGAX8oSmljPULy3RQ1CT 25 | zsie3iA+kFbFjJ09hMvFYJh+H/ppXbUAMnSGvumZd/c9dzbUbu5wIOyNxgP3QB5u 26 | qJGF94jRo1ELtF27AUpvNwDK5x8O85LjVvkw8dDMq0S/i9j/XQnU+qNa5NKRGz7F 27 | HXEIwvkuPupMH8noNtHcICg/iOjEo1z6cYUnz5Si3yn5fnz1NAeZJQQ82Wl7GqQa 28 | jxB/8TzEHj4VH6wgtaI+n8zIAfVinm98COOv3FdVtHh5uEbtZuNSGV8pS+LX8Z5n 29 | d0c4ll1xxxPZHy5NuS2rIXCrouHnY6TVA7tT0s/7eZZdJnPTAoIBAQC1eg2CzlxV 30 | pXOweSD0LlkOEfJk/E3JFbq/3ClCCb/Ys53VGWWI/p/YUy8ZiA1TSr64AEpvkNDc 31 | fHqOOSMDhyKKiryW9falRdqWuxnQuZjp9MJ/EKSD+lbOPypCFvQpzDbqup15tGSF 32 | u7JkmLaNhtf8ZVDB8j/ml+iFSJRWr+5lEe599MhhjsZu21vPi1SaIaAcOLhHwYmU 33 | kzwOomkUMLCtMuhios423oKxljMsM3RN10ZBnfrQIC6xaFqoxZUj8y5n7V7qm96d 34 | A3ExH8kIRX01pkR39H5+5t2uaTlqAQwhOD/Wp5aAuMU1RK8CPMLx2aYhJQO3qYId 35 | uBJaiPDfScD3AoIBADfDCRkp+BFyr/6Iec7oLLewYTXbmAh25+lOASeFbaw09jFP 36 | HTP3zxakT+UDG/9mddPFqE7tTzCnK3nCg2SunFqRhzUpNp7ZpwI82Z9orkOnPGLY 37 | iZr4tvvd/cREKAGVvLGbfd82T8jKoCGerumLym9Jz/yf1RwU3weEnbTjgPHk+DV1 38 | bFgvFunoU6DNteL1lOgfxk+m9dqX0Z7NWgzXcd5L7Z6BoEarQ/KSHfm6TzD/neeZ 39 | fIMB0aS4Z2NryOU3gCryXgTRoEbJPlnjcMFXsP0LXsuYJrEIFilUdf1/2aKwD5w1 40 | YaRJZOXw6hfCnA+AAyg/RegzL7Rtoq9Ln6uZwxsCggEAVw3bmVoAtR0PKCKTCk+J 41 | bNRzILpfXgzMbujf6cvfecZ/+g+aw2LRruYIfAjly4TZhE6vZmFjr8EO0VbbUv/q 42 | M+dlnE0VKiceOpw6QAQoNGKzQlzrblwSdBGc5eM48SwDVAp9LDq61eQ6KUOQnseG 43 | qI65dLAOs0E/y2ALuKHgjB9T9pwL27Cvw9H6cs71oJJsydzzmYhfxSPz9Vk3avYI 44 | aq03CMnsfimzLBlPJu7v5b9U9nkgLeKiQ3w5sShG6N3o7vXRSF3JJizWiagjuZIM 45 | BEZkJd7lJ178DIRxhEZIWU8wYHo4GjuIbosqILqhEX9E0DV/WQU5Y6Q/OarjUVmB 46 | 9QKCAQAX9Q2TdLxB+E2eWUYHpYU/zVo8y8rWWSMzxtHi1N3UitoC8QXYDozEUyJ3 47 | Xct/uNr/Swb5kATPOzdTdmJS+NPckIolpcxYngSWGezkkOddIAusXa94k5NsKgO6 48 | PPyRlkb0V2v1Kt8r3qV6jTv6kgdkozxcimatPMBdebZ/QfoI9KdcDhvtBdBAkDu6 49 | E1bErqqsaVtbXDMvMiEYJmbsqlPY8JMVfofFm9V0kMgX4JMnr7gxY0TZpp3Ajyqq 50 | bo9P1+Os23le744V/zCWWZE3u5CugMr4IfDH/wIZNXZwqXOgWTc5djc0GvEAgYim 51 | kx58VOIKdHkn/m7HBmPic7y9hYZo 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /examples/yew-tls/src/app.css: -------------------------------------------------------------------------------- 1 | body {} 2 | -------------------------------------------------------------------------------- /examples/yew-tls/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html {} 4 | -------------------------------------------------------------------------------- /examples/yew-tls/src/inline-scss.scss: -------------------------------------------------------------------------------- 1 | .trunk_yew_example_fancy_green { 2 | $nice_color: green; 3 | background-color: $nice_color; 4 | } 5 | -------------------------------------------------------------------------------- /examples/yew-tls/src/main.rs: -------------------------------------------------------------------------------- 1 | use console_error_panic_hook::set_once as set_panic_hook; 2 | use wasm_bindgen::prelude::*; 3 | use web_sys::window; 4 | use ybc::TileCtx::{Ancestor, Child, Parent}; 5 | use yew::prelude::*; 6 | 7 | #[function_component(App)] 8 | pub fn app() -> Html { 9 | let location_service = LocationService {}; 10 | let location = location_service.get_location().href().unwrap_or_default(); 11 | html! { 12 | <> 13 | 18 | {"Location Demo"} 19 | 20 | }} 21 | /> 22 | 23 | 28 | 29 | 30 | 31 | 32 | {"Location"} 33 |

{"The current location is: "} {location}

34 |
35 |
36 |
37 |
38 | 39 | }}> 40 |
41 | 42 | } 43 | } 44 | 45 | pub struct LocationService {} 46 | 47 | impl LocationService { 48 | pub fn get_location(&self) -> web_sys::Location { 49 | window().unwrap().location() 50 | } 51 | } 52 | 53 | #[wasm_bindgen(inline_js = "export function snippetTest() { console.log('Hello from JS FFI!'); }")] 54 | extern "C" { 55 | fn snippetTest(); 56 | } 57 | 58 | fn main() { 59 | set_panic_hook(); 60 | snippetTest(); 61 | 62 | // Show off some feature flag enabling patterns. 63 | #[cfg(feature = "demo-abc")] 64 | { 65 | web_sys::console::log_1(&"feature `demo-abc` enabled".into()); 66 | } 67 | #[cfg(feature = "demo-xyz")] 68 | { 69 | web_sys::console::log_1(&"feature `demo-xyz` enabled".into()); 70 | } 71 | 72 | yew::Renderer::::new().render(); 73 | } 74 | -------------------------------------------------------------------------------- /examples/yew-tls/src/yew.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/yew/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yew-example" 3 | version = "0.1.0" 4 | authors = ["Anthony Dodd "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | console_error_panic_hook = "0.1" 9 | wasm-bindgen = "0.2" 10 | ybc = "0.4" 11 | yew = { version = "0.20", features = ["csr"] } 12 | 13 | [dependencies.web-sys] 14 | version = "0.3" 15 | features = [ 16 | "console", 17 | ] 18 | 19 | [features] 20 | default = [] 21 | demo-abc = [] 22 | demo-xyz = [] 23 | -------------------------------------------------------------------------------- /examples/yew/README.md: -------------------------------------------------------------------------------- 1 | Trunk | Yew | ybc 2 | ================= 3 | An example application demonstrating building a WASM web application using Trunk, Yew & YBC. 4 | 5 | Once you've installed Trunk, simply execute `trunk serve --open` from this example's directory, and you should see the following web application rendered in your browser. 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/yew/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | -------------------------------------------------------------------------------- /examples/yew/example-yew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/examples/yew/example-yew.png -------------------------------------------------------------------------------- /examples/yew/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trunk | Yew | YBC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/yew/src/app.css: -------------------------------------------------------------------------------- 1 | body {} 2 | -------------------------------------------------------------------------------- /examples/yew/src/index.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html {} 4 | -------------------------------------------------------------------------------- /examples/yew/src/inline-scss.scss: -------------------------------------------------------------------------------- 1 | .trunk_yew_example_fancy_green { 2 | $nice_color: green; 3 | background-color: $nice_color; 4 | } 5 | -------------------------------------------------------------------------------- /examples/yew/src/yew.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | book/ 2 | 3 | # from addons 4 | 5 | mdbook-admonish.css 6 | -------------------------------------------------------------------------------- /guide/README.md: -------------------------------------------------------------------------------- 1 | # The book 2 | 3 | ## At least once 4 | 5 | You'll need to install some pre-requisites in to work build the book: 6 | 7 | ```shell 8 | cargo install mdbook --locked 9 | cargo install mdbook-admonish --locked 10 | mdbook-admonish install . 11 | ``` 12 | 13 | ## When editing 14 | 15 | ```shell 16 | mdbook serve 17 | ``` 18 | -------------------------------------------------------------------------------- /guide/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "The Trunk Guide" 3 | description = "Documention of Trunk, a web application bundler for Rust" 4 | src = "src" 5 | 6 | multilingual = false 7 | language = "en" 8 | 9 | authors = ["Jens Reimann "] 10 | 11 | [build] 12 | build-dir = "book/html" 13 | 14 | [rust] 15 | edition = "2021" 16 | 17 | [preprocessor.admonish] 18 | command = "mdbook-admonish" 19 | assets_version = "3.0.2" # do not edit: managed by `mdbook-admonish install` 20 | 21 | [output.html] 22 | git-repository-url = "https://github.com/trunk-rs/trunk" 23 | git-repository-icon = "fa-github" 24 | edit-url-template = "https://github.com/trunk-rs/trunk/edit/main/guide/{path}" 25 | 26 | additional-css = [ 27 | "./mdbook-admonish.css", 28 | "theme/pagetoc.css" 29 | ] 30 | additional-js = [ 31 | "theme/pagetoc.js" 32 | ] 33 | 34 | -------------------------------------------------------------------------------- /guide/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](introduction.md) 4 | 5 | --- 6 | 7 | * [Getting started](getting-started/index.md) 8 | * [Pre-requisites](getting-started/pre-reqs.md) 9 | * [Installation](getting-started/installation.md) 10 | * [First project](getting-started/project.md) 11 | * [Commands](commands/index.md) 12 | * [Configuration](configuration/index.md) 13 | * [Schema](configuration/schema.md) 14 | * [Hooks](build/hooks.md) 15 | * [Assets](assets/index.md) 16 | * [Minification](assets/minification.md) 17 | * [Sub-resource integrity](assets/sri.md) 18 | * [Advanced](advanced/index.md) 19 | * [JavaScript interoperability](advanced/javascript_interop.md) 20 | * [Startup event](advanced/startup_event.md) 21 | * [Application initializer](advanced/initializer.md) 22 | * [Library crate](advanced/library.md) 23 | * [Base URLs, public URLs, paths & reverse proxies](advanced/paths.md) 24 | * [Backend Proxy](advanced/proxy.md) 25 | --- 26 | 27 | [Contributing](contributing.md) 28 | -------------------------------------------------------------------------------- /guide/src/advanced/index.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | There are some more advanced topics, which will be described in the following subsections. 4 | -------------------------------------------------------------------------------- /guide/src/advanced/initializer.md: -------------------------------------------------------------------------------- 1 | 2 | # Initializer 3 | 4 | Since: `0.19.0-alpha.1`. 5 | 6 | Trunk supports tapping into the initialization process of the WebAssembly application. By 7 | default, this is not active and works the same way as with previous versions. 8 | 9 | The default process is that trunk injects a small JavaScript snippet, which imports the JavaScript loader generated 10 | by `wasm_bindgen` and calls the `init` method. That will fetch the WASM blob and run it. 11 | 12 | The downside of this is, that during this process, there's no feedback for the user. Neither when it takes a bit longer 13 | to load the WASM file, nor when something goes wrong. 14 | 15 | Now it is possible to tap into this process by setting `data-initializer` to a JavaScript module file. This module file 16 | is required to (default) export a function, which returns the "initializer" instance. Here is an example: 17 | 18 | ```javascript 19 | export default function myInitializer () { 20 | return { 21 | onStart: () => { 22 | // called when the loading starts 23 | }, 24 | onProgress: ({current, total}) => { 25 | // the progress while loading, will be called periodically. 26 | // "current" will contain the number of bytes of the WASM already loaded 27 | // "total" will either contain the total number of bytes expected for the WASM, or if the server did not provide 28 | // the content-length header it will contain 0. 29 | }, 30 | onComplete: () => { 31 | // called when the initialization is complete (successfully or failed) 32 | }, 33 | onSuccess: (wasm) => { 34 | // called when the initialization is completed successfully, receives the `wasm` instance 35 | }, 36 | onFailure: (error) => { 37 | // called when the initialization is completed with an error, receives the `error` 38 | } 39 | } 40 | }; 41 | ``` 42 | 43 | For a full example, see: . 44 | -------------------------------------------------------------------------------- /guide/src/advanced/javascript_interop.md: -------------------------------------------------------------------------------- 1 | # JavaScript interoperability 2 | 3 | Trunk will create the necessary JavaScript code to bootstrap and run the WebAssembly based application. It will also 4 | include all JavaScript snippets generated by `wasm-bindgen` for interfacing with JavaScript functionality. 5 | 6 | By default, functions exported from Rust, using `wasm-bingen`, can be accessed in the JavaScript code through the global 7 | variable `window.wasmBindings`. This behavior can be disabled, and the name can be customized. For more information 8 | see the [`rust` asset type](../assets/index.md#rust). 9 | 10 | ## Order of initialization 11 | 12 | The bindings will only be available and working when the application initialization has been completed. 13 | 14 | If your WebAssembly application renders code into the web page/DOM tree, which then calls from JavaScript into the 15 | WebAssembly application, then this will not be an issue, as the application is already initialized. 16 | 17 | However, if you want to call into the WebAssembly application from, for example, the `index.html` file itself, then 18 | you must delay that call until the application is started. 19 | 20 | This can be ensured by executing that code with the `TrunkApplicationStartup` event. Also see 21 | [Startup Event](startup_event.md). 22 | -------------------------------------------------------------------------------- /guide/src/advanced/library.md: -------------------------------------------------------------------------------- 1 | 2 | # Library crate 3 | 4 | Aside from having a `main` function, it is also possible to up your project as a `cdylib` project. In order to do that, 5 | add the following to your `Cargo.toml`: 6 | 7 | ```toml 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | ``` 11 | 12 | And then, define the entrypoint in your `lib.rs` like (does not need to be `async`): 13 | 14 | ```rust 15 | #[wasm_bindgen(start)] 16 | pub async fn run() {} 17 | ``` 18 | -------------------------------------------------------------------------------- /guide/src/advanced/paths.md: -------------------------------------------------------------------------------- 1 | # Base URLs, public URLs, paths & reverse proxies 2 | 3 | Since: `0.19.0-alpha.3`. 4 | 5 | Originally `trunk` had a single `--public-url`, which allowed to set the base URL of the hosted application. 6 | Plain and simple. This was a prefix for all URLs generated and acted as a base for `trunk serve`. 7 | 8 | Unfortunately, life isn't that simple and naming is hard. 9 | 10 | Today `trunk` was three paths: 11 | 12 | * The "public base URL": acting as a prefix for all generated URLs 13 | * The "serve base": acting as a scope/prefix for all things served by `trunk serve` 14 | * The "websocket base": acting as a base path for the auto-reload websocket 15 | 16 | All three can be configured, but there are reasonable defaults in place. By default, the serve base and websocket base 17 | default to the absolute path of the public base. The public base will have a slash appended if it doesn't have one. The 18 | public base can be one of: 19 | 20 | * Unset/nothing/default (meaning `/`) 21 | * An absolute URL (e.g. `http://domain/path/app`) 22 | * An absolute path (e.g. `/path/app`) 23 | * A relative path (e.g. `foo` or `./`) 24 | 25 | If the public base is an absolute URL, then the path of that URL will be used as serve and websocket base. If the public 26 | base is a relative path, then it will be turned into an absolute one. Both approaches might result in a dysfunctional 27 | application, based on your environment. There will be a warning on the console. However, by providing an explicit 28 | value using serve-base or ws-base, this can be fixed. 29 | 30 | Why is this necessary and when is it useful? It's mostly there to provide all the knobs/configurations for the case 31 | that weren't considered. The magic of public-url worked for many, but not for all. To support such cases, it 32 | is now possible to tweak all the settings, at the cost of more complexity. Having reasonable defaults should keep it 33 | simple for the simple cases. 34 | 35 | An example use case is a reverse proxy *in front* of `trunk serve`, which can't be configured to serve the trunk 36 | websocket at the location `trunk serve` expects it. Now, it is possible to have `--public-url` to choose the base when 37 | generating links, so that it looks correct when being served by the proxy. But also use `--serve-base /` to keep 38 | serving resource from the root. 39 | -------------------------------------------------------------------------------- /guide/src/advanced/proxy.md: -------------------------------------------------------------------------------- 1 | # Backend Proxy 2 | 3 | Trunk ships with a built-in proxy which can be enabled when running `trunk serve`. There are two ways to configure the 4 | proxy, each discussed below. All Trunk proxies will transparently pass along the request body, headers, and query 5 | parameters to the proxy backend. 6 | 7 | ### Proxy CLI Flags 8 | 9 | The `trunk serve` command accepts two proxy related flags. 10 | 11 | `--proxy-backend` specifies the URL of the backend server to which requests should be proxied. The URI segment of the 12 | given URL will be used as the path on the Trunk server to handle proxy requests. 13 | E.G., `trunk serve --proxy-backend=http://localhost:9000/api/` will proxy any requests received on the path `/api/` to 14 | the server listening at `http://localhost:9000/api/`. Further path segments or query parameters will be seamlessly 15 | passed along. 16 | 17 | `--proxy-rewrite` specifies an alternative URI on which the Trunk server is to listen for proxy requests. Any requests 18 | received on the given URI will be rewritten to match the URI of the proxy backend, effectively stripping the rewrite 19 | prefix. E.G., `trunk serve --proxy-backend=http://localhost:9000/ --proxy-rewrite=/api/` will proxy any requests 20 | received on `/api/` over to `http://localhost:9000/` with the `/api/` prefix stripped from the request, while everything 21 | following the `/api/` prefix will be left unchanged. 22 | 23 | `--proxy-insecure` allows the `--proxy-backend` url to use a self-signed certificate for https (or any 24 | officially [invalid](https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.danger_accept_invalid_certs) 25 | certs, including expired). This would be used when proxying to https such 26 | as `trunk serve --proxy-backend=https://localhost:3001/ --proxy-insecure` where the ssl cert was self-signed, such as 27 | with [mkcert](https://github.com/FiloSottile/mkcert), and routed through an https reverse proxy for the backend, such 28 | as [local-ssl-proxy](https://github.com/cameronhunter/local-ssl-proxy) 29 | or [caddy](https://caddyserver.com/docs/quick-starts/reverse-proxy). 30 | 31 | `--proxy-no-sytem-proxy` bypasses the system proxy when contacting the proxy backend. 32 | 33 | `--proxy-ws` specifies that the proxy is for a WebSocket endpoint. 34 | 35 | ### Config File 36 | 37 | The `Trunk.toml` config file accepts multiple `[[proxy]]` sections, which allows for multiple proxies to be configured. 38 | Each section requires at least the `backend` field, and optionally accepts the `rewrite` and `ws` fields, both 39 | corresponding to the `--proxy-*` CLI flags discussed above. 40 | 41 | As it is with other Trunk config, a proxy declared via CLI will take final precedence and will cause any config file 42 | proxies to be ignored, even if there are multiple proxies declared in the config file. 43 | 44 | The following is a snippet from the `Trunk.toml` file in the Trunk repo: 45 | 46 | ```toml 47 | [[proxy]] 48 | rewrite = "/api/v1/" 49 | backend = "http://localhost:9000/" 50 | ``` 51 | -------------------------------------------------------------------------------- /guide/src/advanced/startup_event.md: -------------------------------------------------------------------------------- 1 | # Startup event 2 | 3 | The initializer code snippet of Trunk will emit an event when the WebAssembly application has been loaded and started. 4 | 5 | ```admonish note 6 | This event is independent of the initializer functionality. 7 | ``` 8 | 9 | ## Definition 10 | 11 | The event is called `TrunkApplicationStarted` and is executed after the WebAssembly has been loaded and initialized. 12 | 13 | The event will have custom details: 14 | 15 | ```javascript 16 | { 17 | wasm // The web assembly instance 18 | } 19 | ``` 20 | 21 | ## Example 22 | 23 | The following snippet can be used to run code after the initialization of the WebAssembly application: 24 | 25 | ```html 26 | 35 | ``` 36 | 37 | Also see the vanilla example: . 38 | -------------------------------------------------------------------------------- /guide/src/assets/minification.md: -------------------------------------------------------------------------------- 1 | # Minification 2 | 3 | Trunk supports minifying assets. This is disabled by default and can be controlled on various levels. 4 | 5 | In any case, Trunk does not perform minification itself, but delegates the process to dependencies which do the actual 6 | implementation. In cases where minification breaks things, it will, most likely, be an issue with that dependency. 7 | 8 | Starting with Trunk 0.20.0, minification is disabled by default. It can be turned on from the command line using the 9 | `--minify` (or `-M`) switch. Alternatively, it can be controlled using the `build.minify` field in the `Trunk.toml` 10 | file. The value of this field is an enum, with the following possible values: `never` (default, never minify), 11 | `on_release` (minify when running Trunk with `--release`), `always` (always minify). 12 | 13 | When minification is enabled, all assets known to trunk (this excludes the `copy-dir` and `copy-file` opaque blobs to 14 | Trunk), will get minified. It is possible to opt out of this process on a per-asset basis using the `data-no-minify` 15 | attribute (see individual asset configuration). In this case, the asset will *never* get minified. 16 | -------------------------------------------------------------------------------- /guide/src/assets/sri.md: -------------------------------------------------------------------------------- 1 | # Sub-resource integrity 2 | 3 | Trunk can automatically generate hashes of files and add the `integrity` attribute for resources fetched by the web 4 | application. This is enabled by default, but can be overridden using the `data-integrity` attribute. See the different 5 | asset types. 6 | 7 | The following values are available: 8 | 9 | * `none` 10 | * `sha256` 11 | * `sha384` (default) 12 | * `sha512` 13 | -------------------------------------------------------------------------------- /guide/src/build/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | If you find that you need Trunk to perform an additional build action that isn't supported directly, then Trunk's 4 | flexible hooks system can be used to launch external processes at various stages in the pipeline. 5 | 6 | ## Build steps 7 | 8 | This is a brief overview of Trunk's build process for the purpose of describing when hooks are executed. Please note 9 | that the exact ordering may change in the future to add new features. 10 | 11 | - Step 1 — Read and parse the HTML file. 12 | - Step 2 — Produce a plan of all assets to be built. 13 | - Step 3 — Build all assets in parallel. 14 | - Step 4 — Finalize and write assets to staging directory. 15 | - Step 5 — Write HTML to staging directory. 16 | - Step 6 - Replace `dist` directory contents with staging directory contents. 17 | 18 | The hook stages correspond to this as follows: 19 | 20 | - `pre_build`: takes place before step 1. 21 | - `build`: takes place at the same time as step 3, executing in parallel with asset builds. 22 | - `post_build`: takes place after step 5 and before step 6. 23 | 24 | ## Hook execution 25 | 26 | Hooks can be declared exclusively in `Trunk.toml`, and consist of a `stage`, `command` and `command_arguments`: 27 | 28 | - `stage`: (required) one of `pre_build`, `build` or `post_build`. It specifies when in Trunk's build pipeline the hook 29 | is executed. 30 | - `command`: (required) the name or path to the desired executable. 31 | - `command_arguments`: (optional, defaults to none) any arguments to be passed, in the given order, to the executable. 32 | 33 | At the relevant point for each stage, all hooks for that stage are spawned simultaneously. After this, Trunk immediately 34 | waits for all the hooks to exit before proceeding, except in the case of the `build` stage, described further below. 35 | 36 | All hooks are executed using the same `stdin` and `stdout` as trunk. The executable is expected to return an error code 37 | of `0` to indicate success. Any other code will be treated as an error and terminate the build process. Additionally, 38 | the following environment variables are provided to the process: 39 | 40 | - `TRUNK_PROFILE`: the build profile in use. Currently, either `debug` or `release`. 41 | - `TRUNK_HTML_FILE`: the full path to the HTML file (typically `index.html` in `TRUNK_SOURCE_DIR`) used by trunk. 42 | - `TRUNK_SOURCE_DIR`: the full path to the source directory in use by Trunk. This is always the directory in 43 | which `TRUNK_HTML_FILE` resides. 44 | - `TRUNK_STAGING_DIR`: the full path of the Trunk staging directory. 45 | - `TRUNK_DIST_DIR`: the full path of the Trunk dist directory. 46 | - `TRUNK_PUBLIC_URL`: the configured public URL for Trunk. 47 | 48 | ## OS-specific overrides 49 | 50 | Often times you will want to perform the same build step on different OSes, requiring different commands. 51 | A typical example of this is using the `sh` command on Linux, but `cmd` on Windows. 52 | To accomodate this, you can optionally create OS-specific overrides for each hook. 53 | To do this, specify the default hook, then directly below it create a `[hooks.]` entry where `` 54 | can be one of `windows`, `macos`, or `linux`. Within this entry you must specify only the `command` and 55 | `command_argumnets` keys. You may provide multiple overrides for each hook. i.e. 56 | One for `windows`, one for `macos`, and one for `linux`. 57 | 58 | -------------------------------------------------------------------------------- /guide/src/commands/index.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Trunk ships with a set of CLI commands to help you in your development workflows. 4 | 5 | ## build 6 | 7 | `trunk build` runs a cargo build targeting the wasm32 instruction set, runs `wasm-bindgen` on the built WASM, and spawns 8 | asset build pipelines for any assets defined in the target `index.html`. 9 | 10 | Trunk leverages Rust's powerful concurrency primitives for maximum build speeds & throughput. 11 | 12 | ## watch 13 | 14 | `trunk watch` does the same thing as `trunk build`, but also watches the filesystem for changes, triggering new builds 15 | as changes are detected. 16 | 17 | ## serve 18 | 19 | `trunk serve` does the same thing as `trunk watch`, but also spawns a web server. 20 | 21 | ## clean 22 | 23 | `trunk clean` cleans up any build artifacts generated from earlier builds. 24 | 25 | ## config show 26 | 27 | `trunk config show` prints out Trunk's current config, before factoring in CLI arguments. Nice for testing & debugging. 28 | 29 | ## tools show 30 | 31 | `trunk tools show` prints out information about tools required by trunk and the project. It shows which tools are 32 | expected and which are found. 33 | -------------------------------------------------------------------------------- /guide/src/configuration/schema.md: -------------------------------------------------------------------------------- 1 | # Configuration Schema 2 | 3 | Trunk provides a JSON schema for the configuration model. This can be added to e.g. a YAML file using the following 4 | syntax: 5 | 6 | ```yaml 7 | $schema: "./schema.json" 8 | ``` 9 | 10 | ## Obtaining the schema 11 | 12 | You can generate the schema by running: 13 | 14 | ```bash 15 | trunk config generate-schema 16 | ``` 17 | 18 | Or directly write it to a file: 19 | 20 | ```bash 21 | trunk config generate-schema path/to/file 22 | ``` 23 | 24 | ## Editor/IDE support 25 | 26 | Your editor/IDE needs to support this functionality. Trunk only provides the schema. The following sections provide 27 | some information on how to use this. 28 | 29 | ### IntelliJ (and alike) 30 | 31 | IntelliJ based IDEs (including Rust Rover) do support JSON schemas in YAML and JSON files. You only need to reference 32 | the schema, like: 33 | 34 | ```yaml 35 | $schema: "./schema.json" 36 | ``` 37 | -------------------------------------------------------------------------------- /guide/src/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Anyone and everyone is welcome to contribute! Please review 4 | the [CONTRIBUTING.md](https://github.com/trunk-rs/trunk/blob/main/CONTRIBUTING.md) document for more details. The best 5 | way to get started is to find an open issue, and then start hacking on implementing it. Letting other folks know that 6 | you are working on it, and sharing progress is a great approach. Open pull requests early and often, and please use 7 | GitHub's draft pull request feature. 8 | -------------------------------------------------------------------------------- /guide/src/getting-started/index.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | The following subsections explain what is required to install and use `trunk`, and how you can start with a basic 4 | project. 5 | -------------------------------------------------------------------------------- /guide/src/getting-started/pre-reqs.md: -------------------------------------------------------------------------------- 1 | # Pre-requisites 2 | 3 | While `trunk` tries to fetch tools automatically as needed (unless you're running with `--offline`), some 4 | pre-requisites may be required, depending on your environment. 5 | 6 | ## Rust 7 | 8 | It might be obvious, but `trunk` requires an installation of Rust. Not only when installing `trunk` itself from sources, 9 | but also for compiling the Rust-based projects to WebAssembly. 10 | 11 | The instructions of installing Rust may vary based on your operating system, a reasonable default comes from the Rust 12 | project: 13 | 14 | Once installed, you should have the following tools available on your command line: 15 | 16 | * `rustup` 17 | * `cargo` 18 | 19 | ## WebAssembly target 20 | 21 | By default, the Rust installation will only install the target for your current machine. However, in this case, we want 22 | to cross-compile to WebAssembly. Therefore, it is required to install the target `wasm32-unknown-unknown`. Assuming 23 | you have installed Rust using the standard process and can use `rustup`, you can add the target using: 24 | 25 | ```shell 26 | rustup target add wasm32-unknown-unknown 27 | ``` 28 | -------------------------------------------------------------------------------- /guide/src/getting-started/project.md: -------------------------------------------------------------------------------- 1 | # A basic project 2 | 3 | For building the web application, `trunk` is running a combination of tools, mainly `cargo build` and `wasm-bindgen`. 4 | Therefore, having a simple `cargo` project and an `index.html` file as an entry-point is sufficient to get you started. 5 | 6 | ## Creating a project 7 | 8 | Start with creating a fresh Rust project and change into that folder: 9 | 10 | ```shell 11 | cargo new trunk-hello-world 12 | cd trunk-hello-world 13 | ``` 14 | 15 | Add some dependencies for the web: 16 | 17 | ```shell 18 | cargo add wasm-bindgen console_error_panic_hook 19 | cargo add web_sys -F Window,Document,HtmlElement,Text 20 | ``` 21 | 22 | Inside the newly created project, create a file `src/main.rs` with the following content: 23 | 24 | ```rust 25 | use web_sys::window; 26 | 27 | fn main() { 28 | console_error_panic_hook::set_once(); 29 | 30 | let document = window() 31 | .and_then(|win| win.document()) 32 | .expect("Could not access the document"); 33 | let body = document.body().expect("Could not access document.body"); 34 | let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); 35 | body.append_child(text_node.as_ref()) 36 | .expect("Failed to append text"); 37 | } 38 | ``` 39 | 40 | Create an `index.html` in the root of project: 41 | 42 | ```html 43 | 44 | 45 | 46 | 47 | Hello World 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | Then, start `trunk` inside that project to have it built and served: 55 | 56 | ```shell 57 | trunk serve --open 58 | ``` 59 | 60 | This should compile the project, run `wasm_bindgen`, create an `index.html` based on the original file which loads and 61 | initializes the application. 62 | 63 | The application itself is pretty basic, simply getting the document's body and adding a note. 64 | 65 | ## Next steps 66 | 67 | Most likely, you do not want to manually update the DOM tree of your application. You might want to add some assets, 68 | tweak the build process, use some more browser APIs, perform some HTTP requests, use existing crates for the web, and 69 | maybe even interface with the JavaScript world. However, all of this is an extension to this basic project we just 70 | created. 71 | 72 | Here are some pointers: 73 | 74 | * [`wasm_bindgen` documentation](https://rustwasm.github.io/wasm-bindgen) 75 | * HTML Frameworks (sorted by GitHub stars) 76 | * [Yew](https://yew.rs/) 77 | * [Leptos](https://github.com/gbj/leptos) 78 | * [Dioxus](https://dioxuslabs.com/) 79 | * [More](https://github.com/flosse/rust-web-framework-comparison?tab=readme-ov-file#frontend-frameworks-wasm) 80 | * More `trunk` [examples](https://github.com/trunk-rs/trunk/tree/main/examples) 81 | -------------------------------------------------------------------------------- /guide/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Trunk is a WebAssembly (WASM) web application bundler for Rust. Trunk uses a simple, optional-config pattern for 4 | building & bundling WASM, JS snippets & other assets (images, css, scss) via a source HTML file. 5 | 6 | Or in layman's terms: Rusty things go in, webby things come out. 7 | -------------------------------------------------------------------------------- /guide/theme/pagetoc.css: -------------------------------------------------------------------------------- 1 | main { 2 | display: none; 3 | } 4 | 5 | :root { 6 | --toc-width: 270px; 7 | --center-content-toc-shift: calc(-1 * var(--toc-width) / 2); 8 | } 9 | 10 | .nav-chapters { 11 | /* adjust width of buttons that bring to the previous or the next page */ 12 | min-width: 50px; 13 | } 14 | 15 | .previous { 16 | /* 17 | adjust the space between the left sidebar or the left side of the screen 18 | and the button that leads to the previous page 19 | */ 20 | margin-left: var(--page-padding); 21 | } 22 | 23 | @media only screen { 24 | main.wrapped { 25 | display: flex; 26 | } 27 | 28 | @media (max-width: 1179px) { 29 | .sidebar-hidden .sidetoc { 30 | display: none; 31 | } 32 | } 33 | 34 | @media (max-width: 1439px) { 35 | .sidebar-visible .sidetoc { 36 | display: none; 37 | } 38 | } 39 | 40 | @media (1180px <= width <= 1439px) { 41 | .sidebar-hidden main { 42 | position: relative; 43 | left: var(--center-content-toc-shift); 44 | } 45 | } 46 | 47 | @media (1440px <= width <= 1700px) { 48 | .sidebar-visible main { 49 | position: relative; 50 | left: var(--center-content-toc-shift); 51 | } 52 | } 53 | 54 | .content-wrap { 55 | overflow-y: auto; 56 | width: 100%; 57 | } 58 | 59 | .sidetoc { 60 | margin-top: 20px; 61 | margin-left: 10px; 62 | margin-right: auto; 63 | } 64 | .pagetoc { 65 | position: fixed; 66 | /* adjust TOC width */ 67 | width: var(--toc-width); 68 | height: calc(100vh - var(--menu-bar-height) - 0.67em * 4); 69 | overflow: auto; 70 | } 71 | .pagetoc a { 72 | border-left: 1px solid var(--sidebar-bg); 73 | color: var(--fg) !important; 74 | display: block; 75 | padding-bottom: 5px; 76 | padding-top: 5px; 77 | padding-left: 10px; 78 | text-align: left; 79 | text-decoration: none; 80 | } 81 | .pagetoc a:hover, 82 | .pagetoc a.active { 83 | background: var(--sidebar-bg); 84 | color: var(--sidebar-fg) !important; 85 | } 86 | .pagetoc .active { 87 | background: var(--sidebar-bg); 88 | color: var(--sidebar-fg); 89 | font-weight: bold; 90 | } 91 | .pagetoc .pagetoc-H2 { 92 | padding-left: 20px; 93 | font-size: 90%; 94 | } 95 | .pagetoc .pagetoc-H3 { 96 | padding-left: 40px; 97 | font-size: 90%; 98 | } 99 | .pagetoc .pagetoc-H4 { 100 | padding-left: 60px; 101 | font-size: 90%; 102 | } 103 | } 104 | 105 | @media print { 106 | .sidetoc { 107 | display: none; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /site/config.toml: -------------------------------------------------------------------------------- 1 | base_url = "https://trunkrs.dev/" 2 | 3 | title = "Trunk | Build, bundle & ship your Rust WASM application to the web" 4 | 5 | compile_sass = true 6 | build_search_index = true 7 | generate_feeds = true 8 | 9 | theme = "juice" 10 | 11 | [markdown] 12 | highlight_code = true 13 | highlight_theme = "inspired-github" 14 | 15 | [extra] 16 | favicon = "rustacean-flat-happy.svg" 17 | juice_logo_name = "Trunk" 18 | juice_logo_path = "rustacean-flat-happy.svg" 19 | juice_extra_menu = [ 20 | { title = "Guide", link = "https://trunkrs.dev/guide"}, 21 | { title = "GitHub", link = "https://github.com/trunk-rs/trunk"} 22 | ] 23 | -------------------------------------------------------------------------------- /site/content/commands.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Commands" 3 | description = "Commands" 4 | weight = 3 5 | +++ 6 | 7 | Trunk ships with a set of CLI commands to help you in your development workflows. 8 | 9 | # build 10 | `trunk build` runs a cargo build targeting the wasm32 instruction set, runs `wasm-bindgen` on the built WASM, and spawns asset build pipelines for any assets defined in the target `index.html`. 11 | 12 | Trunk leverages Rust's powerful concurrency primitives for maximum build speeds & throughput. 13 | 14 | # watch 15 | `trunk watch` does the same thing as `trunk build`, but also watches the filesystem for changes, triggering new builds as changes are detected. 16 | 17 | # serve 18 | `trunk serve` does the same thing as `trunk watch`, but also spawns a web server. 19 | 20 | # clean 21 | `trunk clean` cleans up any build artifacts generated from earlier builds. 22 | 23 | # config show 24 | `trunk config show` prints out Trunk's current config, before factoring in CLI arguments. Nice for testing & debugging. 25 | 26 | # tools show 27 | `trunk tools show` prints out information about tools required by trunk and the project. It shows which tools are expected and which are found. 28 | -------------------------------------------------------------------------------- /site/sass/blog-post.scss: -------------------------------------------------------------------------------- 1 | .trunk-blog-author { 2 | margin: 0 0 2em 0; 3 | padding: 0; 4 | font-size: 0.8em; 5 | 6 | p { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /site/sass/custom.scss: -------------------------------------------------------------------------------- 1 | // Fix the double indent of the juice theme 2 | .content pre > code { 3 | padding: unset; 4 | } 5 | 6 | // Fix the background color of the juice theme 7 | .content pre { 8 | background-color: var(--code-background-color) !important; 9 | } 10 | -------------------------------------------------------------------------------- /site/static/rustacean-flat-happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctron/trunk/fced298138fb3c0191917d8bb1559f861d3823e7/site/static/rustacean-flat-happy.png -------------------------------------------------------------------------------- /site/templates/blog-post.html: -------------------------------------------------------------------------------- 1 | {% import "juice/templates/_macros.html" as macros %} 2 | {% extends "index.html" %} 3 | 4 | {% block title %}{{ page.title }} | {{ super() }} {% endblock title %} 5 | 6 | {% block head %} 7 | {{ super() }} 8 | 9 | {% endblock head %} 10 | 11 | {% block header %} 12 |
13 | {{ macros::render_header() }} 14 |
15 | {% endblock header %} 16 | 17 | {% block content %} 18 |
19 | {% if page.extra.author %} 20 |

Author: {{page.extra.author}}

21 | {% endif %} 22 | {% if page.date %} 23 |

Date: {{page.date}}

24 | {% endif %} 25 |
26 | 27 |

{{ page.title }}

28 | 29 | {% if page.description %} 30 |

{{ page.description }}

31 | {% endif %} 32 | 33 | {{ page.content | safe }} 34 | {% endblock content %} 35 | -------------------------------------------------------------------------------- /site/templates/blog.html: -------------------------------------------------------------------------------- 1 | {% import "juice/templates/_macros.html" as macros %} 2 | {% extends "index.html" %} 3 | 4 | {% block title %}{{ section.title }} | {{ super() }} {% endblock title %} 5 | 6 | {% block head %} 7 | {% block rss %} 8 | 9 | {% endblock %} 10 | {% endblock head %} 11 | 12 | {% block header %} 13 |
14 | {{ macros::render_header() }} 15 |
16 | {% endblock header %} 17 | 18 | {% block toc %} 19 |
20 |

Subscribe

21 |
22 | {% for post in section.pages %} 23 |
24 | {{ post.title }} 25 |
26 | {% endfor %} 27 |
28 |
29 | {% endblock toc %} 30 | 31 | {% block content %} 32 |
{{ section.description }}
33 | {{ section.content | safe }} 34 | 35 | {% for post in section.pages %} 36 |

{{ post.title }}

37 |

{{ post.description }}

38 | {% endfor %} 39 | 40 | {% endblock content %} 41 | -------------------------------------------------------------------------------- /site/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "juice/templates/index.html" %} 2 | 3 | {% block head %} 4 | 5 | 6 | {% endblock head %} 7 | 8 | {% block hero %} 9 | 10 |
11 |

12 | Build, bundle & ship your Rust WASM application to the web 13 |

14 |

15 | ”Pack your things, we’re going on an adventure!”
~ Ferris 16 |

17 |
18 | Star 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | Discord Chat 29 | 30 | 31 | Number of downloads 32 |
33 |
34 | 35 | 36 |
37 | Explore More ⇩ 38 |
39 | 54 | {% endblock hero %} 55 | 56 | {% block sidebar %} 57 | {% endblock sidebar %} 58 | 59 | {% block footer %} 60 |
61 |

62 | 63 | Trunk Maintainers © 2024 64 | 65 |

66 |

67 | Ferris the crab is public domain. This Ferris can be found at 68 | rustacean.net 69 | and in the hearts of Rust users throughout the galaxy! 70 |

71 |

72 | Join us, resistance is futile 73 | Discord Chat 77 |

78 |
79 | {% endblock footer %} 80 | -------------------------------------------------------------------------------- /src/cmd/config.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{self, Configuration}; 2 | use anyhow::Result; 3 | use clap::{Args, Subcommand}; 4 | use std::{fs::File, io::stdout, path::PathBuf}; 5 | 6 | /// Trunk config controls. 7 | #[derive(Clone, Debug, Args)] 8 | #[command(name = "config")] 9 | pub struct Config { 10 | #[command(subcommand)] 11 | command: Command, 12 | } 13 | 14 | #[derive(Clone, Debug, Subcommand)] 15 | enum Command { 16 | /// Show Trunk's current config pre-CLI. 17 | Show, 18 | /// Generate the trunk configuration schema. 19 | GenerateSchema { 20 | /// Filename to write the schema to, defaults to ``. 21 | output: Option, 22 | }, 23 | } 24 | 25 | impl Config { 26 | #[tracing::instrument(skip(self, config), err)] 27 | pub async fn run(self, config: Option) -> Result<()> { 28 | match self.command { 29 | Command::Show => { 30 | let (cfg, _working_directory) = config::load(config).await?; 31 | println!("{:#?}", cfg); 32 | } 33 | Command::GenerateSchema { output } => { 34 | let schema = schemars::schema_for!(Configuration); 35 | 36 | match output { 37 | Some(file) => { 38 | serde_json::to_writer_pretty(File::create(&file)?, &schema)?; 39 | println!("Wrote schema to: {}", file.display()); 40 | } 41 | None => { 42 | serde_json::to_writer_pretty(stdout().lock(), &schema)?; 43 | } 44 | } 45 | } 46 | } 47 | Ok(()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cmd/core.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Configuration; 2 | use clap::Args; 3 | use semver::VersionReq; 4 | 5 | /// Core options 6 | #[derive(Clone, Debug, Args)] 7 | #[command(next_help_heading = "Core")] 8 | pub struct Core { 9 | /// Override the required trunk version 10 | #[arg(long, env = "TRUNK_REQUIRED_VERSION")] 11 | pub required_version: Option, 12 | } 13 | 14 | impl Core { 15 | /// apply CLI overrides to the configuration 16 | pub fn apply_to(self, mut config: Configuration) -> anyhow::Result { 17 | let Self { required_version } = self; 18 | 19 | config.core.trunk_version = required_version.unwrap_or(config.core.trunk_version); 20 | 21 | Ok(config) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | //! Trunk's subcommands 2 | 3 | pub mod build; 4 | pub mod clean; 5 | pub mod config; 6 | pub mod core; 7 | pub mod serve; 8 | pub mod tools; 9 | pub mod watch; 10 | -------------------------------------------------------------------------------- /src/cmd/tools.rs: -------------------------------------------------------------------------------- 1 | use crate::tools::{self, find_system}; 2 | use anyhow::Result; 3 | use clap::{Args, Subcommand}; 4 | use console::style; 5 | use std::fmt::{Display, Formatter}; 6 | use std::path::PathBuf; 7 | use strum::IntoEnumIterator; 8 | 9 | #[derive(Clone, Debug, Args)] 10 | #[command(name = "tools")] 11 | pub struct Tools { 12 | #[command(subcommand)] 13 | action: Option, 14 | } 15 | 16 | impl Tools { 17 | #[tracing::instrument(level = "trace", skip_all)] 18 | pub async fn run(self, _config: Option) -> Result<()> { 19 | match self.action { 20 | None | Some(ToolsSubcommands::Show) => { 21 | show_tools().await; 22 | } 23 | } 24 | Ok(()) 25 | } 26 | } 27 | 28 | #[derive(Clone, Debug, Subcommand)] 29 | pub enum ToolsSubcommands { 30 | /// Show Trunk's tool versions 31 | Show, 32 | } 33 | 34 | async fn show_tools() { 35 | for app in tools::Application::iter() { 36 | let (path, version) = find_system(app).await.unzip(); 37 | let path = OrNone(path.map(|p| p.display().to_string())); 38 | let version = OrNone(version); 39 | 40 | println!("{}", style(app.name()).bold()); 41 | println!(" Installed Version: {version}"); 42 | println!(" Default Version: {}", app.default_version()); 43 | println!( 44 | " Download URL: {}", 45 | OrError(app.url(app.default_version())) 46 | ); 47 | 48 | println!(" Location: {path}"); 49 | 50 | println!(); 51 | } 52 | } 53 | 54 | struct OrNone(pub Option); 55 | 56 | impl Display for OrNone 57 | where 58 | T: Display, 59 | { 60 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 61 | match &self.0 { 62 | Some(value) => value.fmt(f), 63 | None => f.write_str("n/a"), 64 | } 65 | } 66 | } 67 | 68 | struct OrError(pub Result); 69 | 70 | impl Display for OrError 71 | where 72 | T: Display, 73 | E: Display, 74 | { 75 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 76 | match &self.0 { 77 | Ok(value) => value.fmt(f), 78 | Err(err) => write!(f, ""), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/cmd/watch.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{ 3 | self, 4 | rt::{self, RtcBuilder, RtcWatch}, 5 | types::ConfigDuration, 6 | Configuration, 7 | }, 8 | watch::WatchSystem, 9 | }; 10 | use anyhow::{Context, Result}; 11 | use clap::Args; 12 | use std::{path::PathBuf, sync::Arc}; 13 | use tokio::sync::broadcast; 14 | 15 | /// Build & watch the Rust WASM app and all of its assets. 16 | #[derive(Clone, Args)] 17 | #[command(name = "watch")] 18 | #[command(next_help_heading = "Watch")] 19 | pub struct Watch { 20 | /// Watch specific file(s) or folder(s) [default: build target parent folder] 21 | #[arg(short, long, value_name = "path", env = "TRUNK_WATCH_WATCH")] 22 | pub watch: Option>, 23 | /// Paths to ignore [default: []] 24 | #[arg(short, long, value_name = "path", env = "TRUNK_WATCH_IGNORE")] 25 | pub ignore: Option>, 26 | /// Using polling mode for detecting changes 27 | #[arg(long, env = "TRUNK_WATCH_POLL")] 28 | pub poll: bool, 29 | /// The polling interval, when polling is enabled 30 | #[arg(long, env = "TRUNK_WATCH_POLL_INTERVAL", default_value = "5s")] 31 | pub poll_interval: ConfigDuration, 32 | /// Allow enabling a cooldown, discarding all change events during the build 33 | #[arg(long, env = "TRUNK_WATCH_ENABLE_COOLDOWN")] 34 | pub enable_cooldown: bool, 35 | /// Clear the screen before each run 36 | #[arg(short, long = "clear", env = "TRUNK_WATCH_CLEAR")] 37 | pub clear_screen: bool, 38 | 39 | // NOTE: flattened structures come last 40 | #[command(flatten)] 41 | pub build: super::build::Build, 42 | } 43 | 44 | impl Watch { 45 | // apply CLI overrides to the configuration 46 | pub fn apply_to(self, mut config: Configuration) -> Result { 47 | let Self { 48 | watch, 49 | ignore, 50 | poll: _, 51 | poll_interval: _, 52 | enable_cooldown: _, 53 | clear_screen: _, 54 | build, 55 | } = self; 56 | 57 | config.watch.watch = watch.unwrap_or(config.watch.watch); 58 | config.watch.ignore = ignore.unwrap_or(config.watch.ignore); 59 | 60 | let config = build.apply_to(config)?; 61 | 62 | Ok(config) 63 | } 64 | 65 | #[tracing::instrument(level = "trace", skip(self, config))] 66 | pub async fn run(self, config: Option) -> Result<()> { 67 | let (cfg, working_directory) = config::load(config).await?; 68 | 69 | let cfg = self.clone().apply_to(cfg)?; 70 | let cfg = RtcWatch::from_config(cfg, working_directory, |_, core| rt::WatchOptions { 71 | build: rt::BuildOptions { 72 | core, 73 | inject_autoloader: false, 74 | }, 75 | poll: self.poll.then_some(self.poll_interval.0), 76 | enable_cooldown: self.enable_cooldown, 77 | clear_screen: self.clear_screen, 78 | // in watch mode we can't report errors 79 | no_error_reporting: false, 80 | }) 81 | .await?; 82 | 83 | cfg.enforce_version()?; 84 | 85 | let (shutdown_tx, _shutdown_rx) = broadcast::channel(1); 86 | 87 | let mut system = WatchSystem::new(Arc::new(cfg), shutdown_tx.clone(), None, None).await?; 88 | 89 | system.build().await.ok(); 90 | let system_handle = tokio::spawn(system.run()); 91 | tokio::signal::ctrl_c() 92 | .await 93 | .context("error awaiting shutdown signal")?; 94 | tracing::debug!("received shutdown signal"); 95 | shutdown_tx.send(()).ok(); 96 | drop(shutdown_tx); // Ensure other components see the drop to avoid race conditions. 97 | system_handle 98 | .await 99 | .context("error awaiting system shutdown")?; 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/config/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{Context, Result}; 4 | use cargo_metadata::{Metadata, MetadataCommand, Package}; 5 | use tokio::task::spawn_blocking; 6 | 7 | /// A wrapper around the cargo project's metadata. 8 | #[derive(Clone, Debug)] 9 | pub struct CargoMetadata { 10 | /// The metadata parsed from the cargo project. 11 | pub metadata: Metadata, 12 | /// The metadata package info on this package. 13 | pub package: Package, 14 | /// The manifest path of the target Cargo.toml. 15 | pub manifest_path: String, 16 | } 17 | 18 | impl CargoMetadata { 19 | // Create a new instance from the Cargo.toml at the given path. 20 | pub async fn new(manifest: &Path) -> Result { 21 | let mut cmd = MetadataCommand::new(); 22 | cmd.manifest_path(dunce::simplified(manifest)); 23 | let metadata = spawn_blocking(move || cmd.exec()) 24 | .await 25 | .context("error awaiting spawned cargo metadata task")? 26 | .context("error getting cargo metadata")?; 27 | 28 | Self::from_metadata(metadata) 29 | } 30 | 31 | pub(crate) fn from_metadata(metadata: Metadata) -> Result { 32 | let package = metadata 33 | .root_package() 34 | .cloned() 35 | .context("could not find the root package of the target crate")?; 36 | 37 | // Get the path to the Cargo.toml manifest. 38 | let manifest_path = package.manifest_path.to_string(); 39 | 40 | Ok(Self { 41 | metadata, 42 | package, 43 | manifest_path, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! Trunk config. 2 | //! 3 | //! Trunk follows a layered configuration approach. There are reasonable defaults, the option 4 | //! to load from a configuration file, followed by overrides from the command line (or env-vars). 5 | //! 6 | //! There are four types of structs: Command Line, Serialization, Runtime Options, and Runtime. 7 | //! 8 | //! ## Command Line 9 | //! 10 | //! The command line structs are based on [`clap`] and support both arguments and environment 11 | //! variables. The command line structs can be found in their respective [`crate::cmd`] module. 12 | //! 13 | //! Most fields in those structs are optional, as the idea is to override what can be found in 14 | //! the configuration. 15 | //! 16 | //! Some options in the command line structs are more intended to influence "what" to run, rather 17 | //! than "how" to execute a command. If that's the case, then this option would not be part of 18 | //! the configuration coming from the configuration files. 19 | //! 20 | //! ## Serialization 21 | //! 22 | //! Trunk has a "project model", covering the aspects of what make up a Trunk project. This model 23 | //! is based on structs in the [`crate::config::models`] module and based on [`serde`]. It is 24 | //! loaded in from a configuration file (TOML, YAML, …) or even the `Cargo.toml` file's metadata. 25 | //! 26 | //! However, there is no hierarchical loading process. The first source found will be used. 27 | //! 28 | //! These structs do have a lot of non-optional fields with defaults. These structs should define 29 | //! the defaults to use if they are not provided in the configuration by the user. 30 | //! 31 | //! ## Runtime 32 | //! 33 | //! The runtime configuration structs in [`crate::config::rt`] contain all the information a Trunk 34 | //! command requires to execute, in the form the command requires it. 35 | //! 36 | //! ## Runtime Options 37 | //! 38 | //! Runtime options are a bit of an outlier. In some cases, the runtime configuration requires some 39 | //! information that doesn't come from the user, but from the command execute it. For example, 40 | //! the "build" process needs to know if the reload websocket needs to be injected. Such information 41 | //! is passed in via the Runtime Options. 42 | //! 43 | //! In some cases, it might also be possible that for some commands the command decides, but in 44 | //! other cases the user might influence the choice. This can also go into those options (see 45 | //! error reporting). 46 | //! 47 | //! ## Bringing it all together 48 | //! 49 | //! Here is how it works in general: 50 | //! 51 | //! * Trunk parses the command line arguments (including env-vars) via `clap` 52 | //! * Trunk finds a configuration source (taking the `--config` argument under consideration). 53 | //! * The configuration file is loaded via `serde` and/or `cargo_metadata`. 54 | //! * The configuration model is validated, this might trigger warnings about deprecated fields. 55 | //! * The command line arguments get applied to the configuration, overriding the configuration. 56 | //! * The command creates the runtime options required for the command. 57 | //! * Trunk creates the runtime configuration from the configuration and the runtime options. 58 | //! 59 | //! One aspect of this is that Trunk will always validate the full configuration, but will only 60 | //! create the required runtime configuration for the requested command. 61 | 62 | pub mod manifest; 63 | pub mod models; 64 | pub mod rt; 65 | pub mod types; 66 | 67 | /// The default name of the directory where final build artifacts are 68 | /// placed after a successful build. 69 | pub const DIST_DIR: &str = "dist"; 70 | /// The name of the directory used to stage build artifacts during an active build. 71 | pub const STAGE_DIR: &str = ".stage"; 72 | 73 | pub use manifest::CargoMetadata; 74 | pub use models::{load, Clean, Configuration, Hooks, Tools, Watch}; 75 | -------------------------------------------------------------------------------- /src/config/models/clean.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for "clean" 2 | use crate::config::models::ConfigModel; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | use std::path::PathBuf; 6 | 7 | /// Config options for the serve system. 8 | #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] 9 | pub struct Clean { 10 | /// The output dir for all final assets 11 | #[serde(default, skip_serializing_if = "Option::is_none")] 12 | #[deprecated(note = "Use the global dist field instead")] 13 | pub dist: Option, 14 | /// Optionally perform a cargo clean 15 | #[serde(default)] 16 | pub cargo: bool, 17 | } 18 | 19 | impl ConfigModel for Clean {} 20 | -------------------------------------------------------------------------------- /src/config/models/core.rs: -------------------------------------------------------------------------------- 1 | use crate::config::models::ConfigModel; 2 | use schemars::JsonSchema; 3 | use semver::VersionReq; 4 | use serde::{Deserialize, Serialize}; 5 | use std::path::PathBuf; 6 | 7 | /// Config options for the core project. 8 | #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] 9 | pub struct Core { 10 | #[serde(default)] 11 | // align that with cargo's `rust-version` 12 | #[serde(alias = "trunk-version")] 13 | #[schemars(with = "VersionReqSchema")] 14 | pub trunk_version: VersionReq, 15 | 16 | // the dist folder must be relative 17 | #[serde(default)] 18 | pub dist: Option, 19 | } 20 | 21 | #[derive(JsonSchema)] 22 | #[schemars(remote = "VersionReq")] 23 | struct VersionReqSchema(#[allow(dead_code)] String); 24 | 25 | impl ConfigModel for Core {} 26 | -------------------------------------------------------------------------------- /src/config/models/proxy.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{config::models::ConfigModel, config::types::Uri}; 4 | use schemars::JsonSchema; 5 | use serde::Deserialize; 6 | 7 | /// Config options for building proxies. 8 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)] 9 | pub struct Proxy { 10 | /// The URL of the backend to which requests are to be proxied. 11 | pub backend: Uri, 12 | /// An optional URI prefix which is to be used as the base URI for proxying requests, which 13 | /// defaults to the URI of the backend. 14 | /// 15 | /// When a value is specified, requests received on this URI will have this URI segment 16 | /// replaced with the URI of the `backend`. 17 | pub rewrite: Option, 18 | /// A set of headers to pass to the proxied backend. 19 | #[serde(default)] 20 | pub request_headers: HashMap, 21 | /// Configure the proxy for handling WebSockets. 22 | #[serde(default)] 23 | pub ws: bool, 24 | /// Configure the proxy to accept insecure certificates (danger!). 25 | #[serde(default)] 26 | pub insecure: bool, 27 | /// Configure the proxy to bypass the system proxy. 28 | #[serde(alias = "no-system-proxy")] 29 | #[serde(default)] 30 | pub no_system_proxy: bool, 31 | /// Automatically redirect proxy requests? `no_redirect` defaults to 32 | /// `false`, i.e. yes, follow redirects automatically. 33 | #[serde(default)] 34 | pub no_redirect: bool, 35 | } 36 | 37 | #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, JsonSchema)] 38 | pub struct Proxies(pub Vec); 39 | 40 | impl ConfigModel for Proxies {} 41 | -------------------------------------------------------------------------------- /src/config/models/source/cargo.rs: -------------------------------------------------------------------------------- 1 | //! Loading trunk's configuration from cargo's manifest 2 | 3 | use crate::config::{manifest, Configuration}; 4 | use std::path::Path; 5 | 6 | #[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize)] 7 | struct TrunkMetadata { 8 | #[serde(rename = "trunk")] 9 | #[serde(default)] 10 | pub configuration: Configuration, 11 | } 12 | 13 | /// Load the trunk configuration from the cargo manifest 14 | pub async fn from_manifest(file: impl AsRef) -> anyhow::Result { 15 | let manifest = manifest::CargoMetadata::new(file.as_ref()).await?; 16 | let TrunkMetadata { configuration } = 17 | serde_json::from_value::>(manifest.package.metadata)?.unwrap_or_default(); 18 | Ok(configuration) 19 | } 20 | 21 | #[cfg(test)] 22 | mod test { 23 | use crate::config::models::source::cargo::TrunkMetadata; 24 | use serde_json::Value; 25 | 26 | #[test] 27 | fn test_null() { 28 | let TrunkMetadata { configuration: _ } = serde_json::from_value::>(Value::Null) 29 | .expect("must not fail") 30 | .unwrap_or_default(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/config/models/source/mod.rs: -------------------------------------------------------------------------------- 1 | mod cargo; 2 | 3 | use crate::config::{models::ConfigModel, Configuration}; 4 | use anyhow::bail; 5 | use std::{ 6 | fs::File, 7 | io::BufReader, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | /// A configuration source 12 | pub enum Source { 13 | /// A configuration file (maybe TOML or YAML) 14 | File(PathBuf), 15 | /// A cargo manifest 16 | Manifest { file: PathBuf }, 17 | } 18 | 19 | const CANDIDATES: &[&str] = &[ 20 | // Trunk.toml goes first, as it was the default for a long time 21 | "Trunk.toml", 22 | ".trunk.toml", 23 | "Trunk.yaml", 24 | ".trunk.yaml", 25 | "Trunk.json", 26 | ".trunk.json", 27 | ]; 28 | 29 | impl Source { 30 | /// Find a first config source candidate in a directory 31 | pub fn find(path: &Path) -> anyhow::Result { 32 | for name in CANDIDATES { 33 | if let Some(file) = check_path(path, name) { 34 | return Ok(Source::File(file)); 35 | } 36 | } 37 | 38 | if let Some(file) = check_path(path, "Cargo.toml") { 39 | Ok(Source::Manifest { file }) 40 | } else { 41 | bail!("Unable to find any Trunk configuration"); 42 | } 43 | } 44 | 45 | /// Load the configuration from the source. 46 | /// 47 | /// This will validate and migrate anything that's required. It does not store any migrations. 48 | pub async fn load(self) -> anyhow::Result { 49 | match self { 50 | Self::File(file) => load_from(&file), 51 | Self::Manifest { file } => cargo::from_manifest(file).await, 52 | } 53 | .and_then(|mut cfg| { 54 | cfg.migrate()?; 55 | Ok(cfg) 56 | }) 57 | } 58 | } 59 | 60 | /// Load configuration from a file 61 | /// 62 | /// Currently supported formats are: 63 | /// 64 | /// * TOML 65 | /// * YAML 66 | /// * JSON 67 | fn load_from(file: &Path) -> anyhow::Result { 68 | match file.extension().map(|s| s.to_string_lossy()).as_deref() { 69 | Some("toml") => Ok(toml::from_str(&String::from_utf8(std::fs::read(file)?)?)?), 70 | Some("yaml") => Ok(serde_yaml::from_reader(BufReader::new(File::open(file)?))?), 71 | Some("json") => Ok(serde_json::from_reader(BufReader::new(File::open(file)?))?), 72 | 73 | Some(n) => { 74 | bail!("Unsupported configuration file type: {n}"); 75 | } 76 | None => { 77 | bail!("Missing configuration file extension"); 78 | } 79 | } 80 | } 81 | 82 | /// Check if a file can be found in a directory. 83 | fn check_path(path: &Path, name: &str) -> Option { 84 | let path = path.join(name); 85 | if path.is_file() { 86 | Some(path) 87 | } else { 88 | None 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/config/models/tools.rs: -------------------------------------------------------------------------------- 1 | use crate::config::models::ConfigModel; 2 | use crate::config::Configuration; 3 | use clap::Args; 4 | use schemars::JsonSchema; 5 | use serde::Deserialize; 6 | 7 | /// Config options for automatic application downloads. 8 | // **NOTE:** As there are no differences between the persistent configuration and the CLI overrides 9 | // at all, this struct is used for both configuration as well as CLI arguments. 10 | #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Args, JsonSchema)] 11 | #[command(next_help_heading = "Tools")] 12 | pub struct Tools { 13 | /// Version of `dart-sass` to use. 14 | #[serde(default)] 15 | #[arg(env = "TRUNK_TOOLS_SASS")] 16 | pub sass: Option, 17 | 18 | /// Version of `wasm-bindgen` to use. 19 | #[serde(default)] 20 | #[arg(env = "TRUNK_TOOLS_WASM_BINDGEN")] 21 | pub wasm_bindgen: Option, 22 | 23 | /// Version of `wasm-opt` to use. 24 | #[serde(default)] 25 | #[arg(env = "TRUNK_TOOLS_WASM_OPT")] 26 | pub wasm_opt: Option, 27 | 28 | /// Version of `tailwindcss-cli` to use. 29 | #[serde(default)] 30 | #[arg(env = "TRUNK_TOOLS_TAILWINDCSS")] 31 | pub tailwindcss: Option, 32 | } 33 | 34 | impl Tools { 35 | pub fn apply_to(self, mut config: Configuration) -> anyhow::Result { 36 | config.tools.sass = self.sass.or(config.tools.sass); 37 | config.tools.wasm_bindgen = self.wasm_bindgen.or(config.tools.wasm_bindgen); 38 | config.tools.wasm_opt = self.wasm_opt.or(config.tools.wasm_opt); 39 | config.tools.tailwindcss = self.tailwindcss.or(config.tools.tailwindcss); 40 | 41 | Ok(config) 42 | } 43 | } 44 | 45 | impl ConfigModel for Tools {} 46 | -------------------------------------------------------------------------------- /src/config/models/watch.rs: -------------------------------------------------------------------------------- 1 | use crate::config::models::ConfigModel; 2 | use schemars::JsonSchema; 3 | use serde::Deserialize; 4 | use std::path::PathBuf; 5 | 6 | /// Config options for the watch system. 7 | #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, JsonSchema)] 8 | pub struct Watch { 9 | /// Watch specific file(s) or folder(s) [default: build target parent folder] 10 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 11 | pub watch: Vec, 12 | 13 | /// Paths to ignore [default: []] 14 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 15 | pub ignore: Vec, 16 | } 17 | 18 | impl ConfigModel for Watch {} 19 | -------------------------------------------------------------------------------- /src/config/rt/clean.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | rt::{RtcBuilder, RtcCore}, 3 | Clean, Configuration, 4 | }; 5 | use std::ops::Deref; 6 | 7 | /// Runtime config for the clean system. 8 | #[derive(Clone, Debug)] 9 | pub struct RtcClean { 10 | pub core: RtcCore, 11 | /// Optionally perform a cargo clean. 12 | pub cargo: bool, 13 | /// Optionally clean tools. 14 | pub tools: bool, 15 | } 16 | 17 | impl Deref for RtcClean { 18 | type Target = RtcCore; 19 | 20 | fn deref(&self) -> &Self::Target { 21 | &self.core 22 | } 23 | } 24 | 25 | /// Runtime config options, on a per-run basis. 26 | #[derive(Clone, Debug)] 27 | pub struct CleanOptions { 28 | pub core: super::CoreOptions, 29 | pub tools: bool, 30 | } 31 | 32 | impl RtcClean { 33 | pub(crate) fn new(config: Configuration, opts: CleanOptions) -> anyhow::Result { 34 | let CleanOptions { 35 | core: core_opts, 36 | tools, 37 | } = opts; 38 | 39 | #[allow(deprecated)] 40 | let Configuration { 41 | core: core_config, 42 | clean: 43 | Clean { 44 | cargo, 45 | // We ignore the legacy `dist` field from the configuration for now. 46 | // We have a warning in place, and at some point remove this field. 47 | dist: _, 48 | }, 49 | .. 50 | } = config; 51 | 52 | let core = RtcCore::new(core_config, core_opts)?; 53 | 54 | Ok(Self { core, cargo, tools }) 55 | } 56 | } 57 | 58 | impl RtcBuilder for RtcClean { 59 | type Options = CleanOptions; 60 | 61 | async fn build(configuration: Configuration, options: Self::Options) -> anyhow::Result { 62 | Self::new(configuration, options) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/config/rt/core.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::models::Core, config::DIST_DIR, version::enforce_version_with}; 2 | use anyhow::Context; 3 | use semver::{Version, VersionReq}; 4 | use std::path::PathBuf; 5 | 6 | /// Runtime config for the core project. 7 | #[derive(Clone, Debug)] 8 | pub struct RtcCore { 9 | pub trunk_version: VersionReq, 10 | pub working_directory: PathBuf, 11 | pub dist: PathBuf, 12 | } 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct CoreOptions { 16 | pub working_directory: PathBuf, 17 | } 18 | 19 | impl RtcCore { 20 | pub(super) fn new(config: Core, opts: CoreOptions) -> anyhow::Result { 21 | let CoreOptions { working_directory } = opts; 22 | 23 | let trunk_version = config.trunk_version.clone(); 24 | 25 | let working_directory = dunce::canonicalize(&working_directory) 26 | .with_context(|| format!("unable to canonicalize '{}'", working_directory.display()))?; 27 | 28 | let dist = 29 | working_directory.join(config.dist.as_deref().unwrap_or_else(|| DIST_DIR.as_ref())); 30 | 31 | Ok(Self { 32 | trunk_version, 33 | working_directory, 34 | dist, 35 | }) 36 | } 37 | 38 | /// Ensure that we are the right trunk version for the project 39 | pub(crate) fn enforce_version(self: &RtcCore) -> anyhow::Result<()> { 40 | let actual = match Version::parse(crate::version::VERSION) { 41 | Err(err) => { 42 | tracing::warn!("Unable to parse trunk version, skipping version check: {err}"); 43 | return Ok(()); 44 | } 45 | Ok(version) => version, 46 | }; 47 | 48 | enforce_version_with(&self.trunk_version, actual) 49 | } 50 | 51 | #[cfg(test)] 52 | pub(super) fn new_test(root: &std::path::Path) -> Self { 53 | RtcCore { 54 | trunk_version: VersionReq::STAR, 55 | working_directory: root.to_path_buf(), 56 | dist: root.join(DIST_DIR), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/config/rt/mod.rs: -------------------------------------------------------------------------------- 1 | //! The runtime configuration 2 | //! 3 | //! This is what the system actually uses. 4 | 5 | mod build; 6 | mod clean; 7 | mod core; 8 | mod serve; 9 | mod watch; 10 | 11 | pub use build::*; 12 | pub use clean::*; 13 | pub use core::*; 14 | pub use serve::*; 15 | pub use watch::*; 16 | 17 | use crate::config::Configuration; 18 | use std::path::PathBuf; 19 | 20 | /// Build a runtime configuration from configuration and options. 21 | pub trait RtcBuilder: Sized { 22 | type Options: Sized; 23 | 24 | async fn build(configuration: Configuration, options: Self::Options) -> anyhow::Result; 25 | 26 | async fn from_config( 27 | configuration: Configuration, 28 | working_directory: PathBuf, 29 | f: F, 30 | ) -> anyhow::Result 31 | where 32 | F: FnOnce(&Configuration, CoreOptions) -> Self::Options, 33 | { 34 | let opts = f(&configuration, CoreOptions { working_directory }); 35 | Self::build(configuration, opts).await 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/config/types/address_family.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use schemars::JsonSchema; 3 | 4 | #[derive(Copy, Clone, Eq, PartialEq, Default, Debug, serde::Deserialize, ValueEnum, JsonSchema)] 5 | #[serde(rename_all = "lowercase")] 6 | pub enum AddressFamily { 7 | Ipv4, 8 | #[default] 9 | Ipv6, 10 | } 11 | -------------------------------------------------------------------------------- /src/config/types/cross_origin.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | /// Cross origin setting 4 | #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] 5 | pub enum CrossOrigin { 6 | #[default] 7 | Anonymous, 8 | UseCredentials, 9 | } 10 | 11 | impl CrossOrigin { 12 | pub fn from_str(s: &str) -> anyhow::Result { 13 | Ok(match s { 14 | "" | "anonymous" => CrossOrigin::Anonymous, 15 | "use-credentials" => CrossOrigin::UseCredentials, 16 | _ => return Err(CrossOriginParseError::InvalidValue), 17 | }) 18 | } 19 | } 20 | 21 | impl Display for CrossOrigin { 22 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | Self::Anonymous => write!(f, "anonymous"), 25 | Self::UseCredentials => write!(f, "use-credentials"), 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug, thiserror::Error)] 31 | pub enum CrossOriginParseError { 32 | #[error("invalid value")] 33 | InvalidValue, 34 | } 35 | -------------------------------------------------------------------------------- /src/config/types/duration.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer}; 2 | use std::str::FromStr; 3 | use std::time::Duration; 4 | 5 | /// A newtype to allow using humantime durations as clap and serde values. 6 | #[derive(Clone, Debug)] 7 | pub struct ConfigDuration(pub Duration); 8 | 9 | impl<'de> Deserialize<'de> for ConfigDuration { 10 | fn deserialize(deserializer: D) -> Result 11 | where 12 | D: Deserializer<'de>, 13 | { 14 | Ok(Self(humantime_serde::deserialize(deserializer)?)) 15 | } 16 | } 17 | 18 | impl FromStr for ConfigDuration { 19 | type Err = humantime::DurationError; 20 | 21 | fn from_str(s: &str) -> Result { 22 | Ok(Self(humantime::Duration::from_str(s)?.into())) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/config/types/minify.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] 5 | #[serde(rename_all = "snake_case")] 6 | pub enum Minify { 7 | /// Never minify 8 | #[default] 9 | Never, 10 | /// Minify for release builds 11 | OnRelease, 12 | /// Minify for all builds 13 | Always, 14 | } 15 | -------------------------------------------------------------------------------- /src/config/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types used by Trunk in its configuration 2 | 3 | mod address_family; 4 | mod base_url; 5 | mod cross_origin; 6 | mod duration; 7 | mod minify; 8 | mod uri; 9 | mod ws; 10 | 11 | pub use address_family::*; 12 | pub use base_url::*; 13 | pub use cross_origin::*; 14 | pub use duration::*; 15 | pub use minify::*; 16 | pub use uri::*; 17 | pub use ws::*; 18 | -------------------------------------------------------------------------------- /src/config/types/uri.rs: -------------------------------------------------------------------------------- 1 | use schemars::gen::SchemaGenerator; 2 | use schemars::schema::{Schema, SchemaObject}; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Deserializer}; 5 | use std::ops::Deref; 6 | use std::str::FromStr; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 9 | pub struct Uri( 10 | #[serde(deserialize_with = "crate::config::types::deserialize_uri")] pub axum::http::Uri, 11 | ); 12 | 13 | impl JsonSchema for Uri { 14 | fn schema_name() -> String { 15 | "Uri".to_string() 16 | } 17 | 18 | fn json_schema(gen: &mut SchemaGenerator) -> Schema { 19 | let mut schema: SchemaObject = String::json_schema(gen).into(); 20 | schema.format = Some("uri".into()); 21 | schema.into() 22 | } 23 | } 24 | 25 | impl Deref for Uri { 26 | type Target = axum::http::Uri; 27 | 28 | fn deref(&self) -> &Self::Target { 29 | &self.0 30 | } 31 | } 32 | 33 | impl From for Uri { 34 | fn from(value: axum::http::Uri) -> Self { 35 | Self(value) 36 | } 37 | } 38 | 39 | /// Deserialize a Uri from a string. 40 | pub fn deserialize_uri<'de, D, T>(data: D) -> Result 41 | where 42 | D: Deserializer<'de>, 43 | T: From, 44 | { 45 | let val = String::deserialize(data)?; 46 | axum::http::Uri::from_str(val.as_str()) 47 | .map(Into::into) 48 | .map_err(|err| serde::de::Error::custom(err.to_string())) 49 | } 50 | 51 | #[cfg(test)] 52 | mod test { 53 | use serde_json::json; 54 | 55 | fn assert_uri(uri: &str) { 56 | assert_eq!( 57 | serde_json::from_value::(json!(uri)) 58 | .expect("must parse") 59 | .to_string(), 60 | uri 61 | ); 62 | } 63 | 64 | #[test] 65 | fn deserialize() { 66 | assert_uri("/foo"); 67 | assert_uri("https://localhost/foo"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/config/types/ws.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use schemars::JsonSchema; 3 | use serde::Deserialize; 4 | use std::fmt::{Display, Formatter}; 5 | 6 | /// WebSocket protocol 7 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, ValueEnum, JsonSchema)] 8 | #[serde(rename_all = "lowercase")] 9 | pub enum WsProtocol { 10 | Wss, 11 | Ws, 12 | } 13 | 14 | impl Display for WsProtocol { 15 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 16 | write!( 17 | f, 18 | "{}", 19 | match self { 20 | WsProtocol::Wss => "wss", 21 | WsProtocol::Ws => "ws", 22 | } 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::rt::RtcBuild, pipelines::PipelineStage}; 2 | use anyhow::{bail, Context, Result}; 3 | use futures_util::stream::{FuturesUnordered, StreamExt}; 4 | use std::{process::Stdio, sync::Arc}; 5 | use tokio::{process::Command, task::JoinHandle}; 6 | 7 | /// A `FuturesUnordered` containing a `JoinHandle` for each hook-running task. 8 | pub type HookHandles = FuturesUnordered>>; 9 | 10 | /// Spawns tokio tasks for all hooks configured for the given `HookStage`. 11 | pub fn spawn_hooks(cfg: Arc, stage: PipelineStage) -> HookHandles { 12 | let futures: FuturesUnordered<_> = cfg 13 | .hooks 14 | .iter() 15 | .filter(|hook_cfg| hook_cfg.stage == stage) 16 | .map(|hook_cfg| { 17 | let mut command = Command::new(hook_cfg.command()); 18 | 19 | command 20 | .current_dir(&cfg.core.working_directory) 21 | .args(hook_cfg.command_arguments()) 22 | .stdout(Stdio::inherit()) 23 | .stderr(Stdio::inherit()) 24 | .env("TRUNK_PROFILE", if cfg.release { "release" } else { "debug" }) 25 | .env("TRUNK_HTML_FILE", &cfg.target) 26 | .env("TRUNK_SOURCE_DIR", &cfg.target_parent) 27 | .env("TRUNK_STAGING_DIR", &cfg.staging_dist) 28 | .env("TRUNK_DIST_DIR", &cfg.final_dist) 29 | .env("TRUNK_PUBLIC_URL", &cfg.public_url); 30 | 31 | tracing::info!(command_arguments = ?hook_cfg.command_arguments(), "spawned hook {}", hook_cfg.command()); 32 | 33 | let command_name = hook_cfg.command().clone(); 34 | tracing::info!(?stage, command = %command_name, "spawning hook"); 35 | tokio::spawn(async move { 36 | let status = command 37 | .spawn() 38 | .with_context(|| format!("error spawning hook call for {}", command_name))? 39 | .wait() 40 | .await 41 | .with_context(|| format!("error calling hook to {}", command_name))?; 42 | if !status.success() { 43 | bail!("hook call to {} returned a bad status", command_name); 44 | } 45 | tracing::info!("finished hook {}", command_name); 46 | Ok(()) 47 | }) 48 | }) 49 | .collect(); 50 | 51 | futures 52 | } 53 | 54 | /// Waits for all of the given hooks to finish. 55 | pub async fn wait_hooks(mut futures: HookHandles) -> Result<()> { 56 | while let Some(result) = futures.next().await { 57 | result??; 58 | } 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /src/pipelines/copy_dir.rs: -------------------------------------------------------------------------------- 1 | //! Copy-dir asset pipeline. 2 | 3 | use super::{data_target_path, Attrs, TrunkAssetPipelineOutput, ATTR_HREF}; 4 | use crate::{ 5 | common::{copy_dir_recursive, html_rewrite::Document, target_path}, 6 | config::rt::RtcBuild, 7 | }; 8 | use anyhow::{Context, Result}; 9 | use std::path::PathBuf; 10 | use std::sync::Arc; 11 | use tokio::fs; 12 | use tokio::task::JoinHandle; 13 | 14 | /// A CopyDir asset pipeline. 15 | pub struct CopyDir { 16 | /// The ID of this pipeline's source HTML element. 17 | id: usize, 18 | /// Runtime build config. 19 | cfg: Arc, 20 | /// The path to the dir being copied. 21 | path: PathBuf, 22 | /// Optional target path inside the dist dir. 23 | target_path: Option, 24 | } 25 | 26 | impl CopyDir { 27 | pub const TYPE_COPY_DIR: &'static str = "copy-dir"; 28 | 29 | pub async fn new( 30 | cfg: Arc, 31 | html_dir: Arc, 32 | attrs: Attrs, 33 | id: usize, 34 | ) -> Result { 35 | // Build the path to the target asset. 36 | let href_attr = attrs.get(ATTR_HREF).context( 37 | r#"required attr `href` missing for element"#, 38 | )?; 39 | let mut path = PathBuf::new(); 40 | path.extend(href_attr.split('/')); 41 | if !path.is_absolute() { 42 | path = html_dir.join(path); 43 | } 44 | let target_path = data_target_path(&attrs)?; 45 | 46 | Ok(Self { 47 | id, 48 | cfg, 49 | path, 50 | target_path, 51 | }) 52 | } 53 | 54 | /// Spawn the pipeline for this asset type. 55 | #[tracing::instrument(level = "trace", skip(self))] 56 | pub fn spawn(self) -> JoinHandle> { 57 | tokio::spawn(self.run()) 58 | } 59 | 60 | /// Run this pipeline. 61 | #[tracing::instrument(level = "trace", skip(self))] 62 | async fn run(self) -> Result { 63 | let rel_path = crate::common::strip_prefix(&self.path); 64 | tracing::debug!(path = ?rel_path, "copying directory"); 65 | 66 | let canonical_path = fs::canonicalize(&self.path).await.with_context(|| { 67 | format!("error taking canonical path of directory {:?}", &self.path) 68 | })?; 69 | let dir_name = canonical_path.file_name().with_context(|| { 70 | format!("could not get directory name of dir {:?}", &canonical_path) 71 | })?; 72 | 73 | let dir_out = target_path( 74 | &self.cfg.staging_dist, 75 | self.target_path.as_deref(), 76 | Some(dir_name), 77 | ) 78 | .await?; 79 | copy_dir_recursive(canonical_path, dir_out).await?; 80 | 81 | tracing::debug!(path = ?rel_path, "finished copying directory"); 82 | Ok(TrunkAssetPipelineOutput::CopyDir(CopyDirOutput(self.id))) 83 | } 84 | } 85 | 86 | /// The output of a CopyDir build pipeline. 87 | pub struct CopyDirOutput(usize); 88 | 89 | impl CopyDirOutput { 90 | pub async fn finalize(self, dom: &mut Document) -> Result<()> { 91 | dom.remove(&super::trunk_id_selector(self.0)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/pipelines/copy_file.rs: -------------------------------------------------------------------------------- 1 | //! Copy-file asset pipeline. 2 | 3 | use crate::{ 4 | common::{html_rewrite::Document, target_path}, 5 | config::rt::RtcBuild, 6 | pipelines::{ 7 | data_target_path, AssetFile, AssetFileType, Attrs, TrunkAssetPipelineOutput, ATTR_HREF, 8 | }, 9 | }; 10 | use anyhow::{Context, Result}; 11 | use std::path::PathBuf; 12 | use std::sync::Arc; 13 | use tokio::task::JoinHandle; 14 | 15 | /// A CopyFile asset pipeline. 16 | pub struct CopyFile { 17 | /// The ID of this pipeline's source HTML element. 18 | id: usize, 19 | /// Runtime build config. 20 | cfg: Arc, 21 | /// The asset file being processed. 22 | asset: AssetFile, 23 | /// Optional target path inside the dist dir. 24 | target_path: Option, 25 | } 26 | 27 | impl CopyFile { 28 | pub const TYPE_COPY_FILE: &'static str = "copy-file"; 29 | 30 | pub async fn new( 31 | cfg: Arc, 32 | html_dir: Arc, 33 | attrs: Attrs, 34 | id: usize, 35 | ) -> Result { 36 | // Build the path to the target asset. 37 | let href_attr = attrs.get(ATTR_HREF).context( 38 | r#"required attr `href` missing for element"#, 39 | )?; 40 | let mut path = PathBuf::new(); 41 | path.extend(href_attr.split('/')); 42 | let asset = AssetFile::new(&html_dir, path).await?; 43 | 44 | let target_path = data_target_path(&attrs)?; 45 | 46 | Ok(Self { 47 | id, 48 | cfg, 49 | asset, 50 | target_path, 51 | }) 52 | } 53 | 54 | /// Spawn the pipeline for this asset type. 55 | #[tracing::instrument(level = "trace", skip(self))] 56 | pub fn spawn(self) -> JoinHandle> { 57 | tokio::spawn(self.run()) 58 | } 59 | 60 | /// Run this pipeline. 61 | #[tracing::instrument(level = "trace", skip(self))] 62 | async fn run(self) -> Result { 63 | let rel_path = crate::common::strip_prefix(&self.asset.path); 64 | tracing::debug!(path = ?rel_path, "copying file"); 65 | 66 | let dir_out = 67 | target_path(&self.cfg.staging_dist, self.target_path.as_deref(), None).await?; 68 | 69 | let _ = self 70 | .asset 71 | .copy( 72 | &self.cfg.staging_dist, 73 | &dir_out, 74 | false, 75 | false, 76 | AssetFileType::Other, 77 | ) 78 | .await?; 79 | tracing::debug!(path = ?rel_path, "finished copying file"); 80 | 81 | Ok(TrunkAssetPipelineOutput::CopyFile(CopyFileOutput(self.id))) 82 | } 83 | } 84 | 85 | /// The output of a CopyFile build pipeline. 86 | pub struct CopyFileOutput(usize); 87 | 88 | impl CopyFileOutput { 89 | pub async fn finalize(self, dom: &mut Document) -> Result<()> { 90 | dom.remove(&super::trunk_id_selector(self.0)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/pipelines/copy_file_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | use std::sync::Arc; 4 | 5 | use anyhow::{Context, Result}; 6 | 7 | use crate::config::rt::RtcBuild; 8 | use crate::pipelines::copy_file::*; 9 | use crate::pipelines::ATTR_HREF; 10 | 11 | /// A fixture for setting up basic test config. 12 | async fn setup_test_config() -> Result<(tempfile::TempDir, Arc, PathBuf)> { 13 | let tmpdir = tempfile::tempdir().context("error building tempdir for test")?; 14 | let cfg = Arc::new(RtcBuild::new_test(tmpdir.path()).await?); 15 | let asset_file = tmpdir.path().join("test_file"); 16 | tokio::fs::write(&asset_file, b"abc123") 17 | .await 18 | .context("error writing test file contents")?; 19 | Ok((tmpdir, cfg, asset_file)) 20 | } 21 | 22 | #[tokio::test] 23 | async fn err_new_missing_href() -> Result<()> { 24 | // Assemble. 25 | let (tmpdir, cfg, _) = setup_test_config().await?; 26 | 27 | // Action. 28 | let res = CopyFile::new(cfg, Arc::new(tmpdir.into_path()), Default::default(), 0).await; 29 | 30 | // Assert. 31 | anyhow::ensure!( 32 | res.is_err(), 33 | "unexpected success while constructing CopyFile pipeline, expected error on missing \ 34 | `href` attr" 35 | ); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[tokio::test] 41 | async fn ok_new() -> Result<()> { 42 | // Assemble. 43 | let (tmpdir, cfg, _) = setup_test_config().await?; 44 | let mut attrs = HashMap::new(); 45 | attrs.insert(ATTR_HREF.into(), "test_file".into()); 46 | 47 | // Action. 48 | let res = CopyFile::new(cfg, Arc::new(tmpdir.into_path()), attrs, 0).await; 49 | 50 | // Assert. 51 | anyhow::ensure!( 52 | res.is_ok(), 53 | "unexpected failure while constructing CopyFile pipeline" 54 | ); 55 | 56 | Ok(()) 57 | } 58 | 59 | #[tokio::test] 60 | async fn ok_run_basic_copy() -> Result<()> { 61 | // Assemble. 62 | let (tmpdir, cfg, asset_file) = setup_test_config().await?; 63 | let copy_location = cfg.staging_dist.join("test_file"); 64 | let mut attrs = HashMap::new(); 65 | attrs.insert(ATTR_HREF.into(), "test_file".into()); 66 | let cmd = CopyFile::new(cfg, Arc::new(tmpdir.into_path()), attrs, 0) 67 | .await 68 | .context("error constructing CopyFile pipeline")?; 69 | 70 | // Action. 71 | let _out = cmd 72 | .spawn() 73 | .await 74 | .context("unexpected task join error from pipeline")? 75 | .context("unexpected pipeline error")?; 76 | 77 | // Assert. 78 | let orig = tokio::fs::read_to_string(asset_file) 79 | .await 80 | .context("error reading original file")?; 81 | let copied = tokio::fs::read_to_string(copy_location) 82 | .await 83 | .context("error reading original file")?; 84 | anyhow::ensure!( 85 | orig == copied, 86 | "unexpected content after copy, expected '{}' == '{}'", 87 | orig, 88 | copied 89 | ); 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /src/pipelines/rust/initializer.js: -------------------------------------------------------------------------------- 1 | async function __trunkInitializer(init, source, sourceSize, initializer, initWithObject) { 2 | if (initializer === undefined) { 3 | return await init(initWithObject ? { module_or_path: source } : source); 4 | } 5 | 6 | const { 7 | onStart, onProgress, onComplete, onSuccess, onFailure 8 | } = initializer; 9 | 10 | onStart?.(); 11 | 12 | const response = fetch(source) 13 | .then((response) => { 14 | const reader = response.body.getReader(); 15 | const headers = response.headers; 16 | const status = response.status; 17 | const statusText = response.statusText; 18 | 19 | const total = sourceSize; 20 | let current = 0; 21 | 22 | const stream = new ReadableStream({ 23 | start(controller) { 24 | function push() { 25 | reader.read().then(({done, value}) => { 26 | if (done) { 27 | onProgress?.({current: total, total}); 28 | controller.close(); 29 | return; 30 | } 31 | 32 | current += value.byteLength; 33 | onProgress?.({current, total}); 34 | controller.enqueue(value); 35 | push(); 36 | }); 37 | } 38 | 39 | push(); 40 | }, 41 | }); 42 | 43 | return { 44 | stream, init: { 45 | headers, status, statusText 46 | } 47 | }; 48 | }) 49 | .then(({stream, init}) => 50 | new Response(stream, init), 51 | ); 52 | 53 | return init(initWithObject ? { module_or_path: response } : response) 54 | .then((value) => { 55 | onComplete?.(); 56 | onSuccess?.(value); 57 | return value; 58 | }, (reason) => { 59 | onComplete?.(); 60 | onFailure?.(reason); 61 | return reason; 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/pipelines/rust/wasm_opt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use std::str::FromStr; 3 | 4 | /// Different optimization levels that can be configured with `wasm-opt`. 5 | #[derive(PartialEq, Eq)] 6 | pub enum WasmOptLevel { 7 | /// Default optimization passes. 8 | Default, 9 | /// No optimization passes, skipping the wasp-opt step. 10 | Off, 11 | /// Run quick & useful optimizations. useful for iteration testing. 12 | One, 13 | /// Most optimizations, generally gets most performance. 14 | Two, 15 | /// Spend potentially a lot of time optimizing. 16 | Three, 17 | /// Also flatten the IR, which can take a lot more time and memory, but is useful on more 18 | /// nested / complex / less-optimized input. 19 | Four, 20 | /// Default optimizations, focus on code size. 21 | S, 22 | /// Default optimizations, super-focusing on code size. 23 | Z, 24 | } 25 | 26 | impl FromStr for WasmOptLevel { 27 | type Err = anyhow::Error; 28 | 29 | fn from_str(s: &str) -> anyhow::Result { 30 | Ok(match s { 31 | "" => Self::Default, 32 | "0" => Self::Off, 33 | "1" => Self::One, 34 | "2" => Self::Two, 35 | "3" => Self::Three, 36 | "4" => Self::Four, 37 | "s" | "S" => Self::S, 38 | "z" | "Z" => Self::Z, 39 | _ => bail!("unknown wasm-opt level `{}`", s), 40 | }) 41 | } 42 | } 43 | 44 | impl AsRef for WasmOptLevel { 45 | fn as_ref(&self) -> &str { 46 | match self { 47 | Self::Default => "", 48 | Self::Off => "0", 49 | Self::One => "1", 50 | Self::Two => "2", 51 | Self::Three => "3", 52 | Self::Four => "4", 53 | Self::S => "s", 54 | Self::Z => "z", 55 | } 56 | } 57 | } 58 | 59 | impl Default for WasmOptLevel { 60 | fn default() -> Self { 61 | Self::Default 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/processing/minify.rs: -------------------------------------------------------------------------------- 1 | use minify_js::TopLevelMode; 2 | 3 | /// perform JS minification 4 | pub fn minify_js(bytes: Vec, mode: TopLevelMode) -> Vec { 5 | let mut result: Vec = vec![]; 6 | let session = minify_js::Session::new(); 7 | 8 | match minify_js::minify(&session, mode, &bytes, &mut result) { 9 | Ok(()) => result, 10 | Err(err) => { 11 | tracing::warn!("Failed to minify JS: {err}"); 12 | bytes 13 | } 14 | } 15 | } 16 | 17 | /// perform CSS minification 18 | pub fn minify_css(bytes: Vec) -> Vec { 19 | use lightningcss::stylesheet::*; 20 | 21 | /// wrap CSS minification to isolate borrowing the original content 22 | fn minify(css: &str) -> Result { 23 | // parse CSS 24 | 25 | let mut css = StyleSheet::parse(css, ParserOptions::default()).map_err(|err| { 26 | tracing::warn!("CSS parsing failed, skipping: {err}"); 27 | })?; 28 | 29 | css.minify(MinifyOptions::default()).map_err(|err| { 30 | tracing::warn!("CSS minification failed, skipping: {err}"); 31 | })?; 32 | 33 | Ok(css 34 | .to_css(PrinterOptions { 35 | minify: true, 36 | ..Default::default() 37 | }) 38 | .map_err(|err| { 39 | tracing::warn!("CSS generation failed, skipping: {err}"); 40 | })? 41 | .code) 42 | } 43 | 44 | match std::str::from_utf8(&bytes) { 45 | Ok(css) => minify(css).map(String::into_bytes).unwrap_or(bytes), 46 | Err(_) => bytes, 47 | } 48 | } 49 | 50 | /// perform HTML minification 51 | pub fn minify_html(html: &[u8]) -> Vec { 52 | let mut minify_cfg = minify_html::Cfg::spec_compliant(); 53 | minify_cfg.minify_css = true; 54 | minify_cfg.minify_js = true; 55 | minify_cfg.keep_closing_tags = true; 56 | minify_html::minify(html, &minify_cfg) 57 | } 58 | -------------------------------------------------------------------------------- /src/processing/mod.rs: -------------------------------------------------------------------------------- 1 | //! Functionality for processing 2 | 3 | pub mod integrity; 4 | pub mod minify; 5 | -------------------------------------------------------------------------------- /src/tls.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub enum TlsConfig { 3 | #[cfg(feature = "rustls")] 4 | Rustls { 5 | config: axum_server::tls_rustls::RustlsConfig, 6 | }, 7 | #[cfg(feature = "native-tls")] 8 | Native { 9 | config: axum_server::tls_openssl::OpenSSLConfig, 10 | }, 11 | } 12 | 13 | #[cfg(feature = "rustls")] 14 | impl From for TlsConfig { 15 | fn from(config: axum_server::tls_rustls::RustlsConfig) -> Self { 16 | Self::Rustls { config } 17 | } 18 | } 19 | 20 | #[cfg(feature = "native-tls")] 21 | impl From for TlsConfig { 22 | fn from(config: axum_server::tls_openssl::OpenSSLConfig) -> Self { 23 | Self::Native { config } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/version/disabled.rs: -------------------------------------------------------------------------------- 1 | pub fn update_check(_skip: bool) {} 2 | -------------------------------------------------------------------------------- /src/version/enabled/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::version::{ 2 | enabled::state::{State, Versions}, 3 | NAME, VERSION, 4 | }; 5 | use semver::Version; 6 | use std::time::Duration; 7 | use tracing::instrument; 8 | 9 | mod state; 10 | 11 | #[instrument] 12 | pub fn update_check(skip: bool) { 13 | tracing::trace!("Update check"); 14 | 15 | if skip { 16 | return; 17 | } 18 | 19 | tracing::debug!("Spawning update check"); 20 | 21 | // We need to spawn this in a dedicated tokio runtime, as otherwise this would block 22 | // the current tokio runtime from exiting. There seems to be an issue with where even 23 | // with an aborted spawned task, tokio will wait for it to end indefinitely. 24 | std::thread::spawn(|| { 25 | perform_update_check(); 26 | }); 27 | } 28 | 29 | /// Check if there's a newer version available 30 | #[cfg(feature = "update_check")] 31 | #[tokio::main] 32 | async fn perform_update_check() { 33 | tracing::debug!("Performing update check"); 34 | 35 | let versions = match state::need_check().await { 36 | State::NotNeeded(versions) => { 37 | tracing::debug!("No refresh needed"); 38 | versions 39 | } 40 | State::Needed => match most_recent().await { 41 | Err(err) => { 42 | tracing::debug!("Failed to check for new version: {err}"); 43 | return; 44 | } 45 | Ok(versions) => { 46 | tracing::debug!("New versions: {versions:?}"); 47 | state::record_checked(versions.clone()).await; 48 | versions 49 | } 50 | }, 51 | }; 52 | 53 | announce_version(&versions); 54 | } 55 | 56 | /// Announce a new version if it is newer than our current 57 | #[cfg(feature = "update_check")] 58 | fn announce_version(versions: &Versions) { 59 | let Ok(current) = Version::parse(VERSION) else { 60 | tracing::debug!("Failed to parse the current version ({VERSION})"); 61 | return; 62 | }; 63 | 64 | let most_recent = match current.pre.is_empty() { 65 | false => &versions.prerelease, 66 | true => &versions.release, 67 | }; 68 | 69 | let Some(most_recent) = most_recent else { 70 | return; 71 | }; 72 | 73 | tracing::debug!("Current: {current}, Most recent: {most_recent}"); 74 | 75 | if most_recent > ¤t { 76 | tracing::info!( 77 | "{icon}Found an update of {NAME}: {VERSION} -> {most_recent}", 78 | icon = crate::common::UPDATE 79 | ); 80 | } 81 | } 82 | 83 | async fn most_recent() -> anyhow::Result { 84 | tracing::debug!("Checking for updates"); 85 | 86 | let client = 87 | crates_io_api::AsyncClient::new(&format!("{NAME}/{VERSION}"), Duration::from_secs(1))?; 88 | let response = client.get_crate(NAME).await?; 89 | 90 | let versions = response 91 | .versions 92 | .into_iter() 93 | .filter(|v| !v.yanked) 94 | .map(|v| v.num) 95 | .filter_map(|v| Version::parse(&v).ok()) 96 | .collect::>(); 97 | 98 | let release = versions.iter().filter(|v| v.pre.is_empty()).max().cloned(); 99 | let prerelease = versions.iter().max().cloned(); 100 | 101 | Ok(Versions { 102 | release, 103 | prerelease, 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /src/version/enabled/state.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | use serde::{Deserialize, Serialize}; 3 | use std::io::ErrorKind; 4 | use std::path::PathBuf; 5 | use time::{Duration, OffsetDateTime}; 6 | 7 | const CHECK_PERIOD: Duration = Duration::days(1); 8 | 9 | /// Get the path to the state file. 10 | fn state_file() -> Option { 11 | let dirs = directories::BaseDirs::new()?; 12 | let path = dirs.state_dir().unwrap_or_else(|| dirs.data_local_dir()); 13 | 14 | Some(path.join(env!("CARGO_PKG_NAME")).join("update.json")) 15 | } 16 | 17 | #[derive(Clone, Debug, Serialize, Deserialize)] 18 | struct StateInformation { 19 | #[serde(with = "time::serde::rfc3339")] 20 | pub last_check: OffsetDateTime, 21 | 22 | pub versions: Versions, 23 | } 24 | 25 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 26 | pub struct Versions { 27 | /// The most recent released version 28 | #[serde(default, skip_serializing_if = "Option::is_none")] 29 | pub release: Option, 30 | 31 | /// The most recent version, including pre-releases 32 | #[serde(default, skip_serializing_if = "Option::is_none")] 33 | pub prerelease: Option, 34 | } 35 | 36 | #[derive(Clone, Debug)] 37 | pub enum State { 38 | NotNeeded(Versions), 39 | Needed, 40 | } 41 | 42 | /// Evaluate if we are due for a check 43 | /// 44 | /// Returns `false` if anything goes wrong. 45 | pub async fn need_check() -> State { 46 | let Some(file) = state_file() else { 47 | tracing::debug!("Unable to find a user home. Skipping update checks."); 48 | return State::NotNeeded(Default::default()); 49 | }; 50 | 51 | let state = match tokio::fs::read(&file).await { 52 | Err(err) if err.kind() == ErrorKind::NotFound => return State::Needed, 53 | Err(err) => { 54 | tracing::debug!( 55 | "Failed to check update state file ({}), skipping: {err}", 56 | file.display() 57 | ); 58 | return State::NotNeeded(Default::default()); 59 | } 60 | Ok(state) => state, 61 | }; 62 | 63 | let Ok(state) = serde_json::from_slice::(&state) else { 64 | // if we can't read the file, check and re-write 65 | return State::Needed; 66 | }; 67 | 68 | let diff = OffsetDateTime::now_utc() - state.last_check; 69 | 70 | tracing::debug!("Time since last check: {diff}"); 71 | 72 | if diff > CHECK_PERIOD { 73 | State::Needed 74 | } else { 75 | State::NotNeeded(state.versions) 76 | } 77 | } 78 | 79 | /// Record that we did perform a check. 80 | /// 81 | /// Silently ignores errors. 82 | pub async fn record_checked(versions: Versions) { 83 | let Some(file) = state_file() else { 84 | tracing::debug!("Unable to find a user home. Skipping update checks."); 85 | return; 86 | }; 87 | 88 | let state = match serde_json::to_vec(&StateInformation { 89 | last_check: OffsetDateTime::now_utc(), 90 | versions, 91 | }) { 92 | Ok(state) => state, 93 | Err(err) => { 94 | tracing::debug!("Unable to serialize state file: {err}"); 95 | return; 96 | } 97 | }; 98 | 99 | if let Some(parent) = file.parent() { 100 | if let Err(err) = tokio::fs::create_dir_all(parent).await { 101 | tracing::debug!( 102 | "Failed to create parent directory for update state ({}): {err}", 103 | parent.display() 104 | ); 105 | return; 106 | } 107 | } 108 | 109 | if let Err(err) = tokio::fs::write(&file, state).await { 110 | tracing::debug!( 111 | "Failed to write update state file ({}): {err}", 112 | file.display() 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/version/enforce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use semver::{Version, VersionReq}; 3 | 4 | /// Ensure that we are the right trunk version for the project 5 | pub(crate) fn enforce_version_with(required: &VersionReq, actual: Version) -> anyhow::Result<()> { 6 | tracing::debug!("Enforce version - actual: {actual}, required: {required}"); 7 | 8 | if required == &VersionReq::STAR { 9 | // this should match, but does not match any pre-release version. Which we still accept in this case. 10 | return Ok(()); 11 | } 12 | 13 | let outcome = required.matches(&actual); 14 | tracing::debug!("Current version: {actual}, required version: {required}, matches: {outcome}"); 15 | 16 | if !outcome { 17 | bail!("Project requires a trunk version of '{required}', the current trunk version is: '{actual}'"); 18 | } 19 | 20 | Ok(()) 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use super::*; 26 | use rstest::rstest; 27 | 28 | #[rstest] 29 | // requires, actual, pass? 30 | #[case("*", "0.19.0", true)] 31 | #[case("*", "0.19.0-alpha.1", true)] 32 | #[case("0.19", "0.19.0", true)] 33 | #[case("0.19.0", "0.19.0", true)] 34 | #[case("0.19.0", "0.19.1", true)] 35 | // this may come unexpected, but for 0.x version, a minor version change is a breaking change 36 | #[case("0.20.0", "0.19.0", false)] 37 | #[case("0.19.0-alpha.2", "0.19.0-alpha.1", false)] 38 | #[case("0.19.0-alpha.2", "0.19.0-alpha.2", true)] 39 | #[case("0.19.0-alpha.2", "0.19.0-alpha.3", true)] 40 | #[case("0.19.0-alpha.2", "0.19.0", true)] 41 | #[case("0.19.0-alpha.2", "0.19.1", true)] 42 | // this may come unexpected, but for 0.x version, a minor version change is a breaking change 43 | #[case("0.19.0-alpha.2", "0.20.0", false)] 44 | #[case("0.19.1", "0.19.0", false)] 45 | #[case("0.19.1", "0.19.0-alpha.1", false)] 46 | #[case("0.19.1", "0.19.1-alpha.1", false)] 47 | #[case("0.20.0", "0.19.0-alpha.1", false)] 48 | #[case("0.20.0", "0.19.0", false)] 49 | #[case("0.20.0", "0.19.1-alpha.1", false)] 50 | #[case("0.20.0", "0.19.1", false)] 51 | // a way to say: 0.19.0 or greater 52 | #[case(">=0.19.0", "0.19.0", true)] 53 | #[case(">=0.19.0", "0.19.1", true)] 54 | #[case(">=0.19.0", "0.20.0", true)] 55 | // a way to say: 0.19.0-alpha.2 or greater 56 | #[case(">=0.19.0-alpha.2", "0.19.0-alpha.1", false)] 57 | #[case(">=0.19.0-alpha.2", "0.19.0-alpha.2", true)] 58 | #[case(">=0.19.0-alpha.2", "0.19.0-rc.1", true)] 59 | #[case(">=0.19.0-alpha.2", "0.19.0", true)] 60 | // The following case comes unexpected 61 | #[case(">=0.19.0-alpha.2", "0.20.0-alpha.1", false)] 62 | #[case(">=0.19.0-alpha.2", "0.20.0", true)] 63 | fn test_requires( 64 | #[case] required: VersionReq, 65 | #[case] actual: Version, 66 | #[case] expected: bool, 67 | ) { 68 | assert_eq!(expected, enforce_version_with(&required, actual).is_ok()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/version/mod.rs: -------------------------------------------------------------------------------- 1 | mod enforce; 2 | 3 | #[cfg(feature = "update_check")] 4 | mod enabled; 5 | #[cfg(feature = "update_check")] 6 | pub use enabled::update_check; 7 | 8 | #[cfg(not(feature = "update_check"))] 9 | mod disabled; 10 | #[cfg(not(feature = "update_check"))] 11 | pub use disabled::update_check; 12 | 13 | pub(crate) use enforce::enforce_version_with; 14 | 15 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 16 | #[cfg(feature = "update_check")] 17 | const NAME: &str = env!("CARGO_PKG_NAME"); 18 | -------------------------------------------------------------------------------- /src/ws.rs: -------------------------------------------------------------------------------- 1 | use crate::serve; 2 | use axum::extract::ws::{Message, WebSocket}; 3 | use futures_util::{SinkExt, StreamExt}; 4 | use std::sync::Arc; 5 | use tokio_stream::wrappers::WatchStream; 6 | 7 | /// (outgoing) communication messages with the websocket 8 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | #[serde(tag = "type", content = "data")] 11 | pub enum ClientMessage { 12 | Reload, 13 | BuildFailure { reason: String }, 14 | } 15 | 16 | #[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 17 | pub enum State { 18 | #[default] 19 | Ok, 20 | Failed { 21 | reason: String, 22 | }, 23 | } 24 | 25 | pub(crate) async fn handle_ws(mut ws: WebSocket, state: Arc) { 26 | let mut rx = WatchStream::new(state.ws_state.clone()); 27 | tracing::debug!("autoreload websocket opened"); 28 | 29 | let mut first = true; 30 | 31 | loop { 32 | tokio::select! { 33 | msg = ws.recv() => { 34 | match msg { 35 | Some(Ok(Message::Close(reason))) => { 36 | tracing::debug!("received close from browser: {reason:?}"); 37 | let _ = ws.send(Message::Close(reason)).await; 38 | let _ = ws.close().await; 39 | return 40 | } 41 | Some(Ok(Message::Ping(msg))) => { 42 | tracing::trace!("responding to Ping"); 43 | let _ = ws.send(Message::Pong(msg)).await; 44 | } 45 | Some(Ok(msg)) => { 46 | tracing::debug!("received message from browser: {msg:?} (ignoring)"); 47 | } 48 | Some(Err(err))=> { 49 | tracing::debug!("autoreload websocket closed: {err}"); 50 | return 51 | } 52 | None => { 53 | tracing::debug!("lost websocket"); 54 | return 55 | } 56 | } 57 | } 58 | state = rx.next() => { 59 | 60 | let state = match state { 61 | Some(state) => state, 62 | None => { 63 | tracing::debug!("state watcher closed"); 64 | return 65 | }, 66 | }; 67 | 68 | tracing::trace!("Build state changed: {state:?}"); 69 | 70 | let msg = match state { 71 | State::Ok if first => { 72 | // If the state is ok, and it's the first message we would send, discard it, 73 | // as this would cause a reload right after connecting. On the other side, 74 | // we want to send out a failed build even after reconnecting. 75 | first = false; 76 | tracing::trace!("Discarding first reload trigger"); 77 | None 78 | }, 79 | State::Ok => Some(ClientMessage::Reload), 80 | State::Failed { reason } => Some(ClientMessage::BuildFailure { reason }), 81 | }; 82 | 83 | tracing::trace!("Message to send: {msg:?}"); 84 | 85 | if let Some(msg) = msg { 86 | if let Ok(text) = serde_json::to_string(&msg) { 87 | if let Err(err) = ws.send(Message::Text(text.into())).await { 88 | tracing::info!("autoload websocket failed to send: {err}"); 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | tracing::debug!("exiting WS handler"); 98 | } 99 | -------------------------------------------------------------------------------- /tests/data/bad-build-target.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | -------------------------------------------------------------------------------- /tests/data/bad-watch-ignore.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Needed to make the test work. 3 | target = "../../examples/yew/index.html" 4 | 5 | [watch] 6 | ignore = ["fake.html"] 7 | -------------------------------------------------------------------------------- /tests/data/bad-watch-path.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Needed to make the test work. 3 | target = "../../examples/seed/index.html" 4 | 5 | [watch] 6 | watch = ["fake-dir"] 7 | -------------------------------------------------------------------------------- /tests/data/trunk-version-any.toml: -------------------------------------------------------------------------------- 1 | trunk-version = "*" 2 | 3 | [build] 4 | target = "../../examples/yew/index.html" 5 | -------------------------------------------------------------------------------- /tests/data/trunk-version-minor.toml: -------------------------------------------------------------------------------- 1 | trunk-version = "0.19" 2 | 3 | [build] 4 | target = "../../examples/yew/index.html" 5 | -------------------------------------------------------------------------------- /tests/data/trunk-version-none.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "../../examples/yew/index.html" 3 | -------------------------------------------------------------------------------- /tests/data/trunk-version-prerelease.toml: -------------------------------------------------------------------------------- 1 | trunk-version = "0.19.0-alpha.1" 2 | 3 | [build] 4 | target = "../../examples/yew/index.html" 5 | -------------------------------------------------------------------------------- /tests/data/trunk-version-range.toml: -------------------------------------------------------------------------------- 1 | trunk-version = ">=0.17, <0.19" 2 | 3 | [build] 4 | target = "../../examples/yew/index.html" 5 | --------------------------------------------------------------------------------