├── .github └── workflows │ ├── main.yml │ └── pr.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.Apache-2.0 ├── LICENSE.MIT ├── README.md ├── examples ├── backend-and-frontend │ ├── Cargo.toml │ ├── README.md │ ├── backend │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── frontend │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── run │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── rust-toolchain ├── custom-cli-command │ ├── .dockerignore │ ├── Cargo.toml │ ├── README.md │ ├── backend │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── frontend │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── run │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ └── rust-toolchain ├── frontend-only │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── main.rs └── run-an-example.rs ├── rust-toolchain ├── src ├── lib.rs └── prebuilt_wasm_opt.rs ├── tests ├── examples.rs ├── test-binaryen │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── test-cargo-helper │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── test-crate-name-vs-pkg-name │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── test-crates.rs ├── test-default-build-path │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ ├── fancy.css │ │ └── index.html ├── test-no-serve │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs └── test-sass │ ├── Cargo.toml │ ├── src │ ├── lib.rs │ └── main.rs │ └── styles │ ├── _test3.scss │ ├── subdirectory │ ├── _test6.scss │ ├── test5.scss │ └── test7.scss │ ├── test1.scss │ ├── test2.sass │ └── test4.scss └── wasm-run-proc-macro ├── Cargo.toml ├── LICENSE.Apache-2.0 ├── LICENSE.MIT ├── README.md └── src ├── attr_parser.rs ├── lib.rs └── main_generator.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | schedule: 7 | - cron: 0 0 1 * * 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | cargo-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout source 17 | uses: actions/checkout@v2 18 | 19 | - uses: Swatinem/rust-cache@v1 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v1 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | 27 | - name: cargo test 28 | uses: actions-rs/cargo@v1 29 | with: 30 | command: test 31 | args: --workspace 32 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | cargo-test-and-lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout source 15 | uses: actions/checkout@v2 16 | 17 | - uses: Swatinem/rust-cache@v1 18 | 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v1 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v1 24 | 25 | - name: cargo test 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: test 29 | args: --workspace 30 | 31 | - name: rustfmt 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: fmt 35 | args: --all -- --check 36 | 37 | - name: clippy 38 | uses: actions-rs/clippy-check@v1 39 | with: 40 | token: ${{ secrets.GITHUB_TOKEN }} 41 | args: -- -D warnings 42 | 43 | build-windows: 44 | needs: [cargo-test-and-lint] 45 | runs-on: windows-latest 46 | steps: 47 | - name: Checkout source 48 | uses: actions/checkout@v2 49 | 50 | - uses: Swatinem/rust-cache@v1 51 | 52 | - name: Install target wasm32-unknown-unknown 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: stable 56 | target: wasm32-unknown-unknown 57 | 58 | - name: cargo test (windows) 59 | uses: actions-rs/cargo@v1 60 | with: 61 | command: test 62 | args: --workspace 63 | 64 | build-osx-x86: 65 | needs: [cargo-test-and-lint] 66 | runs-on: macos-latest 67 | steps: 68 | - name: Checkout source 69 | uses: actions/checkout@v2 70 | 71 | - uses: Swatinem/rust-cache@v1 72 | 73 | - name: Install target wasm32-unknown-unknown 74 | uses: actions-rs/toolchain@v1 75 | with: 76 | toolchain: stable 77 | target: wasm32-unknown-unknown 78 | 79 | - name: cargo test (OSX x86) 80 | uses: actions-rs/cargo@v1 81 | with: 82 | command: test 83 | args: --workspace 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | build 4 | public 5 | Dockerfile 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-run" 3 | version = "0.9.3" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/IMI-eRnD-Be/wasm-run" 9 | homepage = "https://github.com/IMI-eRnD-Be/wasm-run" 10 | documentation = "https://docs.rs/wasm-run" 11 | include = ["src/**/*.rs", "README.md", "LICENSE.Apache-2.0", "LICENSE.MIT"] 12 | keywords = ["wasm", "cargo"] 13 | categories = ["development-tools::build-utils"] 14 | description = "Build tool that replaces `cargo run` to build WASM projects" 15 | 16 | [features] 17 | default = ["dev-server", "prebuilt-wasm-opt"] 18 | dev-server = ["tide", "async-std", "wasm-run-proc-macro/serve"] 19 | prebuilt-wasm-opt = ["binary-install", "platforms", "tempfile"] 20 | sass = ["sass-rs", "walkdir"] 21 | 22 | [dependencies] 23 | anyhow = "1.0" 24 | async-std = { version = "1.7.0", optional = true } 25 | binary-install = { version = "0.0.2", optional = true } 26 | binaryen = { version = "0.12", optional = true } 27 | cargo_metadata = "0.12.1" 28 | downcast-rs = "1.2.0" 29 | env_logger = "0.9.0" 30 | futures = { version = "0.3.8" } # TODO should be optional but it's breaking for some reason 31 | fs_extra = "1.2.0" 32 | log = "=0.4.13" # TODO 0.4.14 has issue with env_logger 33 | notify = "4.0.12" 34 | once_cell = "1.5.2" 35 | platforms = { version = "1.0.3", optional = true } 36 | sass-rs = { version = "0.2.2", optional = true } 37 | structopt = "0.3" 38 | tempfile = { version = "3.1.0", optional = true } 39 | tide = { version = "0.15", optional = true } 40 | walkdir = { version = "2.3.1", optional = true } 41 | wasm-bindgen-cli-support = "0.2.68" 42 | wasm-run-proc-macro = { path = "./wasm-run-proc-macro", version = "^0.8.0"} 43 | 44 | [workspace] 45 | members = [ 46 | "examples/frontend-only", 47 | "tests/test-cargo-helper", 48 | "tests/test-crate-name-vs-pkg-name", 49 | "tests/test-default-build-path", 50 | "wasm-run-proc-macro", 51 | ] 52 | -------------------------------------------------------------------------------- /LICENSE.Apache-2.0: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 IMI Hydronic Engineering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This repository is now deprecated in favor of 2 | [xtask-wasm](https://github.com/rustminded/xtask-wasm). 3 | Only patch fixes will be accepted.** 4 | 5 | # wasm-run 6 | 7 | ![Rust](https://github.com/IMI-eRnD-Be/wasm-run/workflows/main/badge.svg) 8 | [![Latest Version](https://img.shields.io/crates/v/wasm-run.svg)](https://crates.io/crates/wasm-run) 9 | [![Docs.rs](https://docs.rs/wasm-run/badge.svg)](https://docs.rs/wasm-run) 10 | [![LOC](https://tokei.rs/b1/github/IMI-eRnD-Be/wasm-run)](https://github.com/IMI-eRnD-Be/wasm-run) 11 | [![Dependency Status](https://deps.rs/repo/github/IMI-eRnD-Be/wasm-run/status.svg)](https://deps.rs/repo/github/IMI-eRnD-Be/wasm-run) 12 | ![License](https://img.shields.io/crates/l/wasm-run) 13 | 14 | ## Synopsis 15 | 16 | Build tool that replaces `cargo run` to build WASM projects. Just like webpack, `wasm-run` 17 | offers a great deal of customization. 18 | 19 | To build your WASM project you normally need an external tool like `wasm-bindgen`, `wasm-pack` 20 | or `cargo-wasm`. `wasm-run` takes a different approach: it's a library that you install as a 21 | dependency to your project. Because of that you don't need any external tool, the 22 | tooling is built as part of your dependencies, which makes the CI easier to set up and reduce 23 | the hassle for new comers to start working on the project. 24 | 25 | To build your project for production you can use the command `cargo run -- build`. You can also 26 | run a development server that rebuilds automatically when the code changes: 27 | `cargo run -- serve`. It doesn't rebuild everything, only the backend if the backend changed or 28 | the frontend if the frontend changed. 29 | 30 | **Please note that there is a space between `--` and `build` and between `--` and `serve`!** 31 | 32 | One of the main advantage of this library is that it provides greater customization: you can 33 | set a few hooks during the build process in order to customize the build directory or use a 34 | template to generate your index.html, download some CSS, ... you name it. I personally use it 35 | to reduce the amount of files by bundling the CSS and the JS into the `index.html` so I had 36 | only two files (`index.html`, `app_bg.wasm`). 37 | 38 | ## Examples 39 | 40 | There are 3 basic examples to help you get started quickly: 41 | 42 | - a ["frontend-only"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/frontend-only) 43 | example for a frontend only app that rebuilds the app when a file change is detected; 44 | - a ["backend-and-frontend"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/backend-and-frontend) 45 | example using the web framework Rocket (backend) which uses Rocket itself to serve the file 46 | during the development (any file change is also detected and it rebuilds and restart 47 | automatically). 48 | - a ["custom-cli-command"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/custom-cli-command) 49 | example that adds a custom CLI command named `build-docker-image` which build the backend, 50 | the frontend and package the whole thing in a container image. 51 | 52 | ## Usage 53 | 54 | All the details about the hooks can be found on the macro [`main`]. 55 | 56 | ## Additional Information 57 | 58 | * You can use this library to build examples in the `examples/` directory of your project. 59 | `cargo run --example your_example -- serve`. But you will need to specify the name of the 60 | WASM crate in your project and it must be present in the workspace. Please check the 61 | ["run-an-example"](https://github.com/IMI-eRnD-Be/wasm-run/blob/main/examples/run-an-example.rs) 62 | example. 63 | * If you want to use your own backend you will need to disable the `dev-server` feature 64 | by disabling the default features. You can use the `full-restart` feature to force the 65 | backend to also be recompiled when a file changes (otherwise only the frontend is 66 | re-compiled). You will also need to specify `run_server` to the macro arguments to run your 67 | backend. 68 | * You can add commands to the CLI by adding variants in the `enum`. 69 | * You can add parameters to the `Build` and `Serve` commands by overriding them. Please check 70 | the documentation on the macro `main`. 71 | * If you run `cargo run -- serve --profiling`, the WASM will be optimized. 72 | 73 | ## Features 74 | 75 | * `prebuilt-wasm-opt`: if you disable the default features and enable this feature, a binary 76 | of wasm-opt will be downloaded from GitHub and used to optimize the WASM. By default, 77 | wasm-opt is compiled among the dependencies (`binaryen`). This is useful if you run into 78 | troubles for building `binaryen-sys`. (`binaryen` cannot be built on Netlify at the 79 | moment.) 80 | * `sass`: support for SASS and SCSS. All SASS and SCSS files found in the directories 81 | `styles/`, `assets/`, `sass/` and `css/` will be automatically transpiled to CSS and placed 82 | in the build directory. This can be configured by overriding: 83 | [`BuildArgs::build_sass_from_dir`], [`BuildArgs::sass_lookup_directories`], 84 | [`BuildArgs::sass_options`] or completely overriden in the [`Hooks::post_build`] hook. 85 | `sass-rs` is re-exported in the prelude of `wasm-run` for this purpose. 86 | * `full-restart`: when this feature is active, the command is entirely restarted when changes 87 | are detected when serving files for development (`cargo run -- serve`). This is useful with 88 | custom `serve` command that uses a custom backend and if you need to detect changes in the 89 | backend code itself. 90 | 91 | License: MIT OR Apache-2.0 92 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "backend", 4 | "frontend", 5 | "run", 6 | ] 7 | default-members = ["run"] 8 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/README.md: -------------------------------------------------------------------------------- 1 | Backend and Frontend 2 | ==================== 3 | 4 | Development Server 5 | ------------------ 6 | 7 | This will run the backend in a subprocess and restart when changes are detected 8 | in the files used by the backend. 9 | 10 | The frontend will be rebuilt when changes are detected in the frontend files. 11 | 12 | ``` 13 | cargo run -- serve 14 | ``` 15 | 16 | You can now go to http://localhost:8000 17 | 18 | Production Build 19 | ---------------- 20 | 21 | *Note:* This will build only the frontend. You must take care of the backend 22 | build on your own. 23 | 24 | ``` 25 | cargo run -- build 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | rocket = "0.4.6" 9 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/backend/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | 3 | use rocket::response::NamedFile; 4 | use std::path::{Path, PathBuf}; 5 | 6 | #[rocket::get("/")] 7 | pub fn index() -> Option { 8 | NamedFile::open(Path::new("build").join("index.html")).ok() 9 | } 10 | 11 | #[rocket::get("/")] 12 | pub fn files(file: PathBuf) -> Option { 13 | NamedFile::open(Path::new("build").join(file)).ok() 14 | } 15 | 16 | fn main() { 17 | rocket::ignite() 18 | .mount("/", rocket::routes![index, files]) 19 | .launch(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "^0.2" 12 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | extern "C" { 5 | #[wasm_bindgen(js_namespace = console)] 6 | fn log(message: &str); 7 | } 8 | 9 | #[wasm_bindgen(start)] 10 | pub fn run_app() -> Result<(), JsValue> { 11 | log("Hello World!"); 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/run/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "run" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | structopt = "0.3" 9 | wasm-run = { path = "../../..", default-features = false, features = [] } 10 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/run/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[wasm_run::main("frontend", "backend")] 4 | #[derive(StructOpt, Debug)] 5 | enum Cli {} 6 | -------------------------------------------------------------------------------- /examples/backend-and-frontend/rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = [] 4 | targets = ["wasm32-unknown-unknown", "x86_64-unknown-linux-musl"] 5 | -------------------------------------------------------------------------------- /examples/custom-cli-command/.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | ** 3 | 4 | # Allow files and directories 5 | !/target/x86_64-unknown-linux-musl/release/backend 6 | !/build/** 7 | -------------------------------------------------------------------------------- /examples/custom-cli-command/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "backend", 4 | "frontend", 5 | "run", 6 | ] 7 | default-members = ["run"] 8 | -------------------------------------------------------------------------------- /examples/custom-cli-command/README.md: -------------------------------------------------------------------------------- 1 | Custom CLI Command 2 | ================== 3 | 4 | You can add your own CLI commands. In this example, we added the command 5 | `build-container-image` which makes a Docker image that builds the backend, the 6 | frontend and pack the whole thing together in a container image using Docker. 7 | 8 | Run: 9 | 10 | ``` 11 | cargo run -- build-container-image 12 | ``` 13 | 14 | Output: 15 | 16 | ``` 17 | Compiling wasm-run v0.6.2-alpha.0 (/home/cecile/repos/wasm-run) 18 | Compiling backend v0.1.0 (/home/cecile/repos/wasm-run/examples/backend-and-frontend/backend) 19 | Compiling frontend v0.1.0 (/home/cecile/repos/wasm-run/examples/backend-and-frontend/frontend) 20 | Finished dev [unoptimized + debuginfo] target(s) in 10.43s 21 | Running `target/debug/frontend build-container-image` 22 | Building frontend... 23 | Finished release [optimized] target(s) in 0.04s 24 | Building backend... 25 | Compiling backend v0.1.0 (/home/cecile/repos/wasm-run/examples/backend-and-frontend/backend) 26 | Finished release [optimized] target(s) in 0.97s 27 | Building container image... 28 | Sending build context to Docker daemon 6.173MB 29 | Step 1/4 : FROM gcr.io/distroless/static 30 | ---> b5f53c952b8e 31 | Step 2/4 : ADD target/x86_64-unknown-linux-musl/release/backend /backend 32 | ---> Using cache 33 | ---> f094442bbf3b 34 | Step 3/4 : ADD build /build 35 | ---> Using cache 36 | ---> bc350d9befcc 37 | Step 4/4 : ENTRYPOINT ["/backend"] 38 | ---> Using cache 39 | ---> c704e0415e57 40 | Successfully built c704e0415e57 41 | Successfully tagged wasm-run-example:latest 42 | ``` 43 | -------------------------------------------------------------------------------- /examples/custom-cli-command/backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | rocket = "0.4.6" 9 | -------------------------------------------------------------------------------- /examples/custom-cli-command/backend/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | 3 | use rocket::response::NamedFile; 4 | use std::path::{Path, PathBuf}; 5 | 6 | #[rocket::get("/")] 7 | pub fn index() -> Option { 8 | NamedFile::open(Path::new("build").join("index.html")).ok() 9 | } 10 | 11 | #[rocket::get("/")] 12 | pub fn files(file: PathBuf) -> Option { 13 | NamedFile::open(Path::new("build").join(file)).ok() 14 | } 15 | 16 | fn main() { 17 | rocket::ignite() 18 | .mount("/", rocket::routes![index, files]) 19 | .launch(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/custom-cli-command/frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "^0.2" 12 | -------------------------------------------------------------------------------- /examples/custom-cli-command/frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | extern "C" { 5 | #[wasm_bindgen(js_namespace = console)] 6 | fn log(message: &str); 7 | } 8 | 9 | #[wasm_bindgen(start)] 10 | pub fn run_app() -> Result<(), JsValue> { 11 | log("Hello World!"); 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /examples/custom-cli-command/run/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "run" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | structopt = "0.3" 9 | wasm-run = { path = "../../..", default-features = false, features = [] } 10 | -------------------------------------------------------------------------------- /examples/custom-cli-command/run/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::Write; 3 | use std::path::Path; 4 | use std::process::Command; 5 | use structopt::StructOpt; 6 | use wasm_run::prelude::*; 7 | 8 | #[wasm_run::main("frontend", "backend", other_cli_commands)] 9 | #[derive(StructOpt, Debug)] 10 | enum Cli { 11 | BuildContainerImage, 12 | } 13 | 14 | fn other_cli_commands(cli: Cli, metadata: &Metadata, _package: &Package) -> anyhow::Result<()> { 15 | match cli { 16 | Cli::BuildContainerImage => { 17 | println!("Building frontend..."); 18 | Cli::build()?; 19 | 20 | println!("Building backend..."); 21 | metadata 22 | .cargo(|command| { 23 | command.args(&[ 24 | "build", 25 | "--release", 26 | "-p", 27 | "backend", 28 | "--target", 29 | "x86_64-unknown-linux-musl", 30 | ]); 31 | })? 32 | .wait_success()?; 33 | 34 | println!("Building container image..."); 35 | 36 | let dockerfile = Path::new("Dockerfile"); 37 | let mut f = fs::File::create(&dockerfile)?; 38 | writeln!(f, "FROM gcr.io/distroless/static")?; 39 | writeln!( 40 | f, 41 | "ADD target/x86_64-unknown-linux-musl/release/backend /backend" 42 | )?; 43 | writeln!(f, "ADD build /build")?; 44 | writeln!(f, "ENTRYPOINT [\"/backend\"]")?; 45 | drop(f); 46 | 47 | let status = Command::new("docker") 48 | .args(&["build", "-t", "wasm-run-example:latest", "."]) 49 | .status() 50 | .unwrap(); 51 | if !status.success() { 52 | anyhow::bail!("failed to build container image"); 53 | } 54 | 55 | Ok(()) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/custom-cli-command/rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = [] 4 | targets = ["wasm32-unknown-unknown", "x86_64-unknown-linux-musl"] 5 | -------------------------------------------------------------------------------- /examples/frontend-only/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend-only" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../.." } 15 | structopt = "0.3" 16 | -------------------------------------------------------------------------------- /examples/frontend-only/README.md: -------------------------------------------------------------------------------- 1 | Frontend-Only 2 | ============= 3 | 4 | Development Server 5 | ------------------ 6 | 7 | *Note:* there is an issue at the moment with cargo that forces you to add 8 | `--bin ` even if there is only one package and only one 9 | `bin`. https://github.com/rust-lang/cargo/issues/9235 10 | 11 | ``` 12 | cargo run --bin frontend-only -- serve 13 | ``` 14 | 15 | You can now go to http://localhost:3000 16 | 17 | Production Build 18 | ---------------- 19 | 20 | *Note:* there is an issue at the moment with cargo that forces you to add 21 | `--bin ` even if there is only one package and only one 22 | `bin`. https://github.com/rust-lang/cargo/issues/9235 23 | 24 | ``` 25 | cargo run --bin frontend-only -- build 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/frontend-only/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This is needed only of the this in wasm-run. 2 | // You should remove this as it prevents the autocompletion to work properly. 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | use wasm_bindgen::prelude::*; 6 | 7 | #[wasm_bindgen] 8 | extern "C" { 9 | #[wasm_bindgen(js_namespace = console)] 10 | fn log(message: &str); 11 | } 12 | 13 | #[wasm_bindgen(start)] 14 | pub fn run_app() -> Result<(), JsValue> { 15 | log("Hello World!"); 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /examples/frontend-only/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[wasm_run::main] 4 | #[derive(StructOpt, Debug)] 5 | enum Cli {} 6 | -------------------------------------------------------------------------------- /examples/run-an-example.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates how to run an example using `cargo --example `. 2 | //! 3 | //! In this repository you can run `cargo run --example run-an-example` and it will run this crate 4 | //! which will run the frontend-only package available in the workspace of this repository under 5 | //! `examples/frontend-only`. 6 | 7 | use structopt::StructOpt; 8 | 9 | // This will use the crate "frontend-only" in the workspace as frontend 10 | #[wasm_run::main("frontend-only")] 11 | #[derive(StructOpt, Debug)] 12 | enum Cli {} 13 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [] 4 | targets = ["wasm32-unknown-unknown"] 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ![Rust](https://github.com/IMI-eRnD-Be/wasm-run/workflows/main/badge.svg) 2 | //! [![Latest Version](https://img.shields.io/crates/v/wasm-run.svg)](https://crates.io/crates/wasm-run) 3 | //! [![Docs.rs](https://docs.rs/wasm-run/badge.svg)](https://docs.rs/wasm-run) 4 | //! [![LOC](https://tokei.rs/b1/github/IMI-eRnD-Be/wasm-run)](https://github.com/IMI-eRnD-Be/wasm-run) 5 | //! [![Dependency Status](https://deps.rs/repo/github/IMI-eRnD-Be/wasm-run/status.svg)](https://deps.rs/repo/github/IMI-eRnD-Be/wasm-run) 6 | //! ![License](https://img.shields.io/crates/l/wasm-run) 7 | //! 8 | //! # Synopsis 9 | //! 10 | //! Build tool that replaces `cargo run` to build WASM projects. Just like webpack, `wasm-run` 11 | //! offers a great deal of customization. 12 | //! 13 | //! To build your WASM project you normally need an external tool like `wasm-bindgen`, `wasm-pack` 14 | //! or `cargo-wasm`. `wasm-run` takes a different approach: it's a library that you install as a 15 | //! dependency to your project. Because of that you don't need any external tool, the 16 | //! tooling is built as part of your dependencies, which makes the CI easier to set up and reduce 17 | //! the hassle for new comers to start working on the project. 18 | //! 19 | //! To build your project for production you can use the command `cargo run -- build`. You can also 20 | //! run a development server that rebuilds automatically when the code changes: 21 | //! `cargo run -- serve`. It doesn't rebuild everything, only the backend if the backend changed or 22 | //! the frontend if the frontend changed. 23 | //! 24 | //! **Please note that there is a space between `--` and `build` and between `--` and `serve`!** 25 | //! 26 | //! One of the main advantage of this library is that it provides greater customization: you can 27 | //! set a few hooks during the build process in order to customize the build directory or use a 28 | //! template to generate your index.html, download some CSS, ... you name it. I personally use it 29 | //! to reduce the amount of files by bundling the CSS and the JS into the `index.html` so I had 30 | //! only two files (`index.html`, `app_bg.wasm`). 31 | //! 32 | //! # Examples 33 | //! 34 | //! There are 3 basic examples to help you get started quickly: 35 | //! 36 | //! - a ["frontend-only"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/frontend-only) 37 | //! example for a frontend only app that rebuilds the app when a file change is detected; 38 | //! - a ["backend-and-frontend"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/backend-and-frontend) 39 | //! example using the web framework Rocket (backend) which uses Rocket itself to serve the file 40 | //! during the development (any file change is also detected and it rebuilds and restart 41 | //! automatically). 42 | //! - a ["custom-cli-command"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/custom-cli-command) 43 | //! example that adds a custom CLI command named `build-docker-image` which build the backend, 44 | //! the frontend and package the whole thing in a container image. 45 | //! 46 | //! # Usage 47 | //! 48 | //! All the details about the hooks can be found on the macro [`main`]. 49 | //! 50 | //! # Additional Information 51 | //! 52 | //! * You can use this library to build examples in the `examples/` directory of your project. 53 | //! `cargo run --example your_example -- serve`. But you will need to specify the name of the 54 | //! WASM crate in your project and it must be present in the workspace. Please check the 55 | //! ["run-an-example"](https://github.com/IMI-eRnD-Be/wasm-run/blob/main/examples/run-an-example.rs) 56 | //! example. 57 | //! * If you want to use your own backend you will need to disable the `dev-server` feature 58 | //! by disabling the default features. You can use the `full-restart` feature to force the 59 | //! backend to also be recompiled when a file changes (otherwise only the frontend is 60 | //! re-compiled). You will also need to specify `run_server` to the macro arguments to run your 61 | //! backend. 62 | //! * You can add commands to the CLI by adding variants in the `enum`. 63 | //! * You can add parameters to the `Build` and `Serve` commands by overriding them. Please check 64 | //! the documentation on the macro `main`. 65 | //! * If you run `cargo run -- serve --profiling`, the WASM will be optimized. 66 | //! 67 | //! # Features 68 | //! 69 | //! * `prebuilt-wasm-opt`: if you disable the default features and enable this feature, a binary 70 | //! of wasm-opt will be downloaded from GitHub and used to optimize the WASM. By default, 71 | //! wasm-opt is compiled among the dependencies (`binaryen`). This is useful if you run into 72 | //! troubles for building `binaryen-sys`. (`binaryen` cannot be built on Netlify at the 73 | //! moment.) 74 | //! * `sass`: support for SASS and SCSS. All SASS and SCSS files found in the directories 75 | //! `styles/`, `assets/`, `sass/` and `css/` will be automatically transpiled to CSS and placed 76 | //! in the build directory. This can be configured by overriding: 77 | //! [`BuildArgs::build_sass_from_dir`], [`BuildArgs::sass_lookup_directories`], 78 | //! [`BuildArgs::sass_options`] or completely overriden in the [`Hooks::post_build`] hook. 79 | //! `sass-rs` is re-exported in the prelude of `wasm-run` for this purpose. 80 | //! * `full-restart`: when this feature is active, the command is entirely restarted when changes 81 | //! are detected when serving files for development (`cargo run -- serve`). This is useful with 82 | //! custom `serve` command that uses a custom backend and if you need to detect changes in the 83 | //! backend code itself. 84 | 85 | #![warn(missing_docs)] 86 | 87 | #[cfg(feature = "prebuilt-wasm-opt")] 88 | mod prebuilt_wasm_opt; 89 | 90 | use anyhow::{anyhow, bail, Context, Result}; 91 | use cargo_metadata::{Metadata, MetadataCommand, Package}; 92 | use downcast_rs::*; 93 | use fs_extra::dir; 94 | use notify::RecommendedWatcher; 95 | use once_cell::sync::OnceCell; 96 | use std::collections::{HashMap, HashSet}; 97 | use std::fs; 98 | use std::io::BufReader; 99 | use std::iter; 100 | use std::iter::FromIterator; 101 | use std::path::{Path, PathBuf}; 102 | #[cfg(feature = "dev-server")] 103 | use std::pin::Pin; 104 | use std::process::{Child, ChildStdout, Command, Stdio}; 105 | use std::sync::mpsc; 106 | use std::time; 107 | use structopt::StructOpt; 108 | #[cfg(feature = "dev-server")] 109 | use tide::Server; 110 | 111 | pub use wasm_run_proc_macro::*; 112 | 113 | #[doc(hidden)] 114 | pub use structopt; 115 | 116 | const DEFAULT_INDEX: &str = r#""#; 117 | 118 | static METADATA: OnceCell = OnceCell::new(); 119 | static DEFAULT_BUILD_PATH: OnceCell = OnceCell::new(); 120 | static FRONTEND_PACKAGE: OnceCell<&Package> = OnceCell::new(); 121 | static BACKEND_PACKAGE: OnceCell> = OnceCell::new(); 122 | static HOOKS: OnceCell = OnceCell::new(); 123 | 124 | #[derive(Debug, PartialEq, Clone, Copy)] 125 | /// A build profile for the WASM. 126 | pub enum BuildProfile { 127 | /// Development profile (no `--release`, no optimization). 128 | Dev, 129 | /// Release profile (`--profile`, `-O2 -Os`). 130 | Release, 131 | /// Release profile (`--profile`, `-O2 --debuginfo`). 132 | Profiling, 133 | } 134 | 135 | /// This function is called early before any command starts. This is not part of the public API. 136 | #[doc(hidden)] 137 | pub fn wasm_run_init( 138 | pkg_name: &str, 139 | backend_pkg_name: Option<&str>, 140 | default_build_path: Option PathBuf>>, 141 | hooks: Hooks, 142 | ) -> Result<(&'static Metadata, &'static Package)> { 143 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 144 | 145 | let metadata = MetadataCommand::new() 146 | .exec() 147 | .context("this binary is not meant to be ran outside of its workspace")?; 148 | 149 | METADATA 150 | .set(metadata) 151 | .expect("the cell is initially empty; qed"); 152 | 153 | let metadata = METADATA.get().unwrap(); 154 | 155 | let frontend_package = METADATA 156 | .get() 157 | .unwrap() 158 | .packages 159 | .iter() 160 | .find(|x| x.name == pkg_name) 161 | .expect("the frontend package existence has been checked during compile time; qed"); 162 | 163 | FRONTEND_PACKAGE 164 | .set(frontend_package) 165 | .expect("the cell is initially empty; qed"); 166 | 167 | let frontend_package = FRONTEND_PACKAGE.get().unwrap(); 168 | 169 | if let Some(name) = backend_pkg_name { 170 | let backend_package = METADATA 171 | .get() 172 | .unwrap() 173 | .packages 174 | .iter() 175 | .find(|x| x.name == name) 176 | .expect("the backend package existence has been checked during compile time; qed"); 177 | 178 | BACKEND_PACKAGE 179 | .set(Some(backend_package)) 180 | .expect("the cell is initially empty; qed"); 181 | } else { 182 | BACKEND_PACKAGE 183 | .set(None) 184 | .expect("the cell is initially empty; qed"); 185 | } 186 | 187 | DEFAULT_BUILD_PATH 188 | .set(if let Some(default_build_path) = default_build_path { 189 | default_build_path(metadata, frontend_package) 190 | } else { 191 | metadata.workspace_root.join("build") 192 | }) 193 | .expect("the cell is initially empty; qed"); 194 | 195 | if HOOKS.set(hooks).is_err() { 196 | panic!("the cell is initially empty; qed"); 197 | } 198 | 199 | Ok((metadata, frontend_package)) 200 | } 201 | 202 | /// Build arguments. 203 | #[derive(StructOpt, Debug)] 204 | pub struct DefaultBuildArgs { 205 | /// Build directory output. 206 | #[structopt(long)] 207 | pub build_path: Option, 208 | 209 | /// Create a profiling build. Enable optimizations and debug info. 210 | #[structopt(long)] 211 | pub profiling: bool, 212 | } 213 | 214 | /// A trait that allows overriding the `build` command. 215 | pub trait BuildArgs: Downcast { 216 | /// Build directory output. 217 | fn build_path(&self) -> &PathBuf; 218 | 219 | /// Default path for the build/public directory. 220 | fn default_build_path(&self) -> &PathBuf { 221 | DEFAULT_BUILD_PATH 222 | .get() 223 | .expect("default_build_path has been initialized on startup; qed") 224 | } 225 | 226 | /// Path to the `target` directory. 227 | fn target_path(&self) -> &PathBuf { 228 | &self.metadata().target_directory 229 | } 230 | 231 | /// Metadata of the project. 232 | fn metadata(&self) -> &Metadata { 233 | METADATA 234 | .get() 235 | .expect("metadata has been initialized on startup; qed") 236 | } 237 | 238 | /// Package metadata. 239 | fn frontend_package(&self) -> &Package { 240 | FRONTEND_PACKAGE 241 | .get() 242 | .expect("frontend_package has been initialized on startup; qed") 243 | } 244 | 245 | /// Backend frontend_package metadata. 246 | fn backend_package(&self) -> Option<&Package> { 247 | BACKEND_PACKAGE 248 | .get() 249 | .expect("frontend_package has been initialized on startup; qed") 250 | .to_owned() 251 | } 252 | 253 | /// Create a profiling build. Enable optimizations and debug info. 254 | fn profiling(&self) -> bool; 255 | 256 | /// Transpile SASS and SCSS files to CSS in the build directory. 257 | #[cfg(feature = "sass")] 258 | fn build_sass_from_dir( 259 | &self, 260 | input_dir: &std::path::Path, 261 | options: sass_rs::Options, 262 | ) -> Result<()> { 263 | use walkdir::{DirEntry, WalkDir}; 264 | 265 | let build_path = self.build_path(); 266 | 267 | fn is_sass(entry: &DirEntry) -> bool { 268 | matches!( 269 | entry.path().extension().map(|x| x.to_str()).flatten(), 270 | Some("sass") | Some("scss") 271 | ) 272 | } 273 | 274 | fn should_ignore(entry: &DirEntry) -> bool { 275 | entry 276 | .file_name() 277 | .to_str() 278 | .map(|x| x.starts_with("_")) 279 | .unwrap_or(false) 280 | } 281 | 282 | log::info!("Building SASS from {:?}", input_dir); 283 | 284 | let walker = WalkDir::new(&input_dir).into_iter(); 285 | for entry in walker 286 | .filter_map(|x| match x { 287 | Ok(x) => Some(x), 288 | Err(err) => { 289 | log::warn!( 290 | "Could not walk into directory `{}`: {}", 291 | input_dir.display(), 292 | err, 293 | ); 294 | None 295 | } 296 | }) 297 | .filter(|x| x.path().is_file() && is_sass(x) && !should_ignore(x)) 298 | { 299 | let file_path = entry.path(); 300 | let css_path = build_path 301 | .join(file_path.strip_prefix(&input_dir).unwrap()) 302 | .with_extension("css"); 303 | 304 | match sass_rs::compile_file(file_path, options.clone()) { 305 | Ok(css) => { 306 | let _ = fs::create_dir_all(css_path.parent().unwrap()); 307 | fs::write(&css_path, css).with_context(|| { 308 | format!("could not write CSS to file `{}`", css_path.display()) 309 | })?; 310 | } 311 | Err(err) => bail!( 312 | "could not convert SASS file `{}` to `{}`: {}", 313 | file_path.display(), 314 | css_path.display(), 315 | err, 316 | ), 317 | } 318 | } 319 | 320 | Ok(()) 321 | } 322 | 323 | /// Returns a list of directories to lookup to transpile SASS and SCSS files to CSS. 324 | #[cfg(feature = "sass")] 325 | fn sass_lookup_directories(&self, _profile: BuildProfile) -> Vec { 326 | const STYLE_CANDIDATES: &[&str] = &["assets", "styles", "css", "sass"]; 327 | 328 | let package_path = self.frontend_package().manifest_path.parent().unwrap(); 329 | 330 | STYLE_CANDIDATES 331 | .iter() 332 | .map(|x| package_path.join(x)) 333 | .filter(|x| x.exists()) 334 | .collect() 335 | } 336 | 337 | /// Default profile to transpile SASS and SCSS files to CSS. 338 | #[cfg(feature = "sass")] 339 | fn sass_options(&self, profile: BuildProfile) -> sass_rs::Options { 340 | sass_rs::Options { 341 | output_style: match profile { 342 | BuildProfile::Release | BuildProfile::Profiling => sass_rs::OutputStyle::Compressed, 343 | _ => sass_rs::OutputStyle::Nested, 344 | }, 345 | ..sass_rs::Options::default() 346 | } 347 | } 348 | 349 | /// Run the `build` command. 350 | fn run(self) -> Result 351 | where 352 | Self: Sized + 'static, 353 | { 354 | let hooks = HOOKS.get().expect("wasm_run_init() has not been called"); 355 | build(BuildProfile::Release, &self, hooks)?; 356 | Ok(self.build_path().to_owned()) 357 | } 358 | } 359 | 360 | impl_downcast!(BuildArgs); 361 | 362 | impl BuildArgs for DefaultBuildArgs { 363 | fn build_path(&self) -> &PathBuf { 364 | self.build_path 365 | .as_ref() 366 | .unwrap_or_else(|| self.default_build_path()) 367 | } 368 | 369 | fn profiling(&self) -> bool { 370 | self.profiling 371 | } 372 | } 373 | 374 | /// Serve arguments. 375 | #[derive(StructOpt, Debug)] 376 | pub struct DefaultServeArgs { 377 | /// Activate HTTP logs. 378 | #[structopt(long)] 379 | pub log: bool, 380 | 381 | /// IP address to bind. 382 | /// 383 | /// Use 0.0.0.0 to expose the server to your network. 384 | #[structopt(long, short = "h", default_value = "127.0.0.1")] 385 | pub ip: String, 386 | 387 | /// Port number. 388 | #[structopt(long, short = "p", default_value = "3000")] 389 | pub port: u16, 390 | 391 | /// Build arguments. 392 | #[structopt(flatten)] 393 | pub build_args: DefaultBuildArgs, 394 | } 395 | 396 | /// A trait that allows overriding the `serve` command. 397 | pub trait ServeArgs: Downcast + Send { 398 | /// Activate HTTP logs. 399 | #[cfg(feature = "dev-server")] 400 | fn log(&self) -> bool; 401 | 402 | /// IP address to bind. 403 | /// 404 | /// Use 0.0.0.0 to expose the server to your network. 405 | #[cfg(feature = "dev-server")] 406 | fn ip(&self) -> &str; 407 | 408 | /// Port number. 409 | #[cfg(feature = "dev-server")] 410 | fn port(&self) -> u16; 411 | 412 | /// Build arguments. 413 | fn build_args(&self) -> &dyn BuildArgs; 414 | 415 | /// Run the `serve` command. 416 | fn run(self) -> Result<()> 417 | where 418 | Self: Sync + Sized + 'static, 419 | { 420 | let hooks = HOOKS.get().expect("wasm_run_init() has not been called"); 421 | // NOTE: the first step for serving is to call `build` a first time. The build directory 422 | // must be present before we start watching files there. 423 | build(BuildProfile::Dev, self.build_args(), hooks)?; 424 | #[cfg(feature = "dev-server")] 425 | { 426 | async_std::task::block_on(async { 427 | let t1 = async_std::task::spawn(serve_frontend(&self, hooks)?); 428 | let t2 = async_std::task::spawn_blocking(move || watch_frontend(&self, hooks)); 429 | futures::try_join!(t1, t2)?; 430 | Err(anyhow!("server and watcher unexpectedly exited")) 431 | }) 432 | } 433 | #[cfg(not(feature = "dev-server"))] 434 | { 435 | use std::sync::Arc; 436 | use std::thread; 437 | 438 | if self.build_args().backend_package().is_none() { 439 | bail!("missing backend crate name"); 440 | } 441 | 442 | let args = Arc::new(self); 443 | let t1 = { 444 | let args = Arc::clone(&args); 445 | thread::spawn(move || watch_frontend(&*args, hooks)) 446 | }; 447 | let t2 = thread::spawn(move || watch_backend(&*args, hooks)); 448 | let _ = t1.join(); 449 | let _ = t2.join(); 450 | 451 | Err(anyhow!("server and watcher unexpectedly exited")) 452 | } 453 | } 454 | } 455 | 456 | impl_downcast!(ServeArgs); 457 | 458 | impl ServeArgs for DefaultServeArgs { 459 | #[cfg(feature = "dev-server")] 460 | fn log(&self) -> bool { 461 | self.log 462 | } 463 | 464 | #[cfg(feature = "dev-server")] 465 | fn ip(&self) -> &str { 466 | &self.ip 467 | } 468 | 469 | #[cfg(feature = "dev-server")] 470 | fn port(&self) -> u16 { 471 | self.port 472 | } 473 | 474 | fn build_args(&self) -> &dyn BuildArgs { 475 | &self.build_args 476 | } 477 | } 478 | 479 | /// Hooks. 480 | /// 481 | /// Check the code of [`Hooks::default()`] implementation to see what they do by default. 482 | /// 483 | /// If you don't provide your own hook, the default code will be executed. But if you do provide a 484 | /// hook, the code will be *replaced*. 485 | pub struct Hooks { 486 | /// This hook will be run before the WASM is compiled. It does nothing by default. 487 | /// You can tweak the command-line arguments of the build command here or create additional 488 | /// files in the build directory. 489 | pub pre_build: 490 | Box Result<()> + Send + Sync>, 491 | 492 | /// This hook will be run after the WASM is compiled and optimized. 493 | /// By default it copies the static files to the build directory. 494 | #[allow(clippy::type_complexity)] 495 | pub post_build: 496 | Box) -> Result<()> + Send + Sync>, 497 | 498 | /// This hook will be run before running the HTTP server. 499 | /// By default it will add routes to the files in the build directory. 500 | #[cfg(feature = "dev-server")] 501 | #[allow(clippy::type_complexity)] 502 | pub serve: Box) -> Result<()> + Send + Sync>, 503 | 504 | /// This hook will be run before starting to watch for changes in files. 505 | /// By default it will add all the `src/` directories and `Cargo.toml` files of all the crates 506 | /// in the workspace plus the `static/` directory if it exists in the frontend crate. 507 | pub frontend_watch: 508 | Box Result<()> + Send + Sync>, 509 | 510 | /// This hook will be run before starting to watch for changes in files. 511 | /// By default it will add the backend crate directory and all its dependencies. But it 512 | /// excludes the target directory. 513 | pub backend_watch: 514 | Box Result<()> + Send + Sync>, 515 | 516 | /// This hook will be run before (re-)starting the backend. 517 | /// You can tweak the cargo command that is run here: adding/removing environment variables or 518 | /// adding arguments. 519 | /// By default it will do `cargo run -p `. 520 | pub backend_command: Box Result<()> + Send + Sync>, 521 | } 522 | 523 | impl Default for Hooks { 524 | fn default() -> Self { 525 | Self { 526 | backend_command: Box::new(|args, command| { 527 | command.args(&[ 528 | "run", 529 | "-p", 530 | &args 531 | .build_args() 532 | .backend_package() 533 | .context("missing backend crate name")? 534 | .name, 535 | ]); 536 | Ok(()) 537 | }), 538 | backend_watch: Box::new(|args, watcher| { 539 | use notify::{RecursiveMode, Watcher}; 540 | 541 | let metadata = args.build_args().metadata(); 542 | let backend = args 543 | .build_args() 544 | .backend_package() 545 | .context("missing backend crate name")?; 546 | let packages: HashMap<_, _> = metadata 547 | .packages 548 | .iter() 549 | .map(|x| (x.name.as_str(), x)) 550 | .collect(); 551 | let members: HashSet<_> = HashSet::from_iter(&metadata.workspace_members); 552 | 553 | backend 554 | .dependencies 555 | .iter() 556 | .map(|x| packages.get(x.name.as_str()).unwrap()) 557 | .filter(|x| members.contains(&x.id)) 558 | .map(|x| x.manifest_path.parent().unwrap()) 559 | .chain(iter::once(backend.manifest_path.parent().unwrap())) 560 | .try_for_each(|x| watcher.watch(x, RecursiveMode::Recursive))?; 561 | 562 | Ok(()) 563 | }), 564 | frontend_watch: Box::new(|args, watcher| { 565 | use notify::{RecursiveMode, Watcher}; 566 | 567 | let metadata = args.build_args().metadata(); 568 | let frontend = args.build_args().frontend_package(); 569 | let packages: HashMap<_, _> = metadata 570 | .packages 571 | .iter() 572 | .map(|x| (x.name.as_str(), x)) 573 | .collect(); 574 | let members: HashSet<_> = HashSet::from_iter(&metadata.workspace_members); 575 | 576 | frontend 577 | .dependencies 578 | .iter() 579 | .filter_map(|x| packages.get(x.name.as_str())) 580 | .filter(|x| members.contains(&x.id)) 581 | .map(|x| x.manifest_path.parent().unwrap()) 582 | .chain(iter::once(frontend.manifest_path.parent().unwrap())) 583 | .try_for_each(|x| watcher.watch(x, RecursiveMode::Recursive))?; 584 | 585 | Ok(()) 586 | }), 587 | pre_build: Box::new(|_, _, _| Ok(())), 588 | post_build: Box::new( 589 | |args, #[allow(unused_variables)] profile, wasm_js, wasm_bin| { 590 | let build_path = args.build_path(); 591 | let wasm_js_path = build_path.join("app.js"); 592 | let wasm_bin_path = build_path.join("app_bg.wasm"); 593 | 594 | fs::write(&wasm_js_path, wasm_js).with_context(|| { 595 | format!("could not write JS file to `{}`", wasm_js_path.display()) 596 | })?; 597 | fs::write(&wasm_bin_path, wasm_bin).with_context(|| { 598 | format!("could not write WASM file to `{}`", wasm_bin_path.display()) 599 | })?; 600 | 601 | let index_path = build_path.join("index.html"); 602 | let static_dir = args 603 | .frontend_package() 604 | .manifest_path 605 | .parent() 606 | .unwrap() 607 | .join("static"); 608 | 609 | if index_path.exists() { 610 | fs::copy("index.html", &index_path).context(format!( 611 | "could not copy index.html to `{}`", 612 | index_path.display() 613 | ))?; 614 | } else if static_dir.exists() { 615 | dir::copy( 616 | &static_dir, 617 | &build_path, 618 | &dir::CopyOptions { 619 | content_only: true, 620 | ..dir::CopyOptions::new() 621 | }, 622 | ) 623 | .with_context(|| { 624 | format!( 625 | "could not copy content of directory static: `{}` to `{}`", 626 | static_dir.display(), 627 | build_path.display() 628 | ) 629 | })?; 630 | } else { 631 | fs::write(&index_path, DEFAULT_INDEX).with_context(|| { 632 | format!( 633 | "could not write default index.html to `{}`", 634 | index_path.display() 635 | ) 636 | })?; 637 | } 638 | 639 | #[cfg(feature = "sass")] 640 | { 641 | let options = args.sass_options(profile); 642 | for style_path in args.sass_lookup_directories(profile) { 643 | args.build_sass_from_dir(&style_path, options.clone())?; 644 | } 645 | } 646 | 647 | Ok(()) 648 | }, 649 | ), 650 | #[cfg(feature = "dev-server")] 651 | serve: Box::new(|args, server| { 652 | use tide::{Body, Request, Response}; 653 | 654 | let build_path = args.build_args().build_path().to_owned(); 655 | let index_path = build_path.join("index.html"); 656 | 657 | server.at("/").serve_dir(args.build_args().build_path())?; 658 | server.at("/").get(move |_| { 659 | let index_path = index_path.clone(); 660 | async move { Ok(Response::from(Body::from_file(index_path).await?)) } 661 | }); 662 | server.at("/*path").get(move |req: Request<()>| { 663 | let build_path = build_path.clone(); 664 | async move { 665 | match Body::from_file(build_path.join(req.param("path").unwrap())).await { 666 | Ok(body) => Ok(Response::from(body)), 667 | Err(_) => Ok(Response::from( 668 | Body::from_file(build_path.join("index.html")).await?, 669 | )), 670 | } 671 | } 672 | }); 673 | 674 | Ok(()) 675 | }), 676 | } 677 | } 678 | } 679 | 680 | fn build(mut profile: BuildProfile, args: &dyn BuildArgs, hooks: &Hooks) -> Result<()> { 681 | use wasm_bindgen_cli_support::Bindgen; 682 | 683 | if args.profiling() { 684 | profile = BuildProfile::Profiling; 685 | } 686 | 687 | let frontend_package = args.frontend_package(); 688 | 689 | let build_path = args.build_path(); 690 | let _ = fs::remove_dir_all(build_path); 691 | fs::create_dir_all(build_path).with_context(|| { 692 | format!( 693 | "could not create build directory `{}`", 694 | build_path.display() 695 | ) 696 | })?; 697 | 698 | let mut command = Command::new("cargo"); 699 | 700 | command 701 | .args(&[ 702 | "build", 703 | "--lib", 704 | "--target", 705 | "wasm32-unknown-unknown", 706 | "--manifest-path", 707 | ]) 708 | .arg(&frontend_package.manifest_path) 709 | .args(match profile { 710 | BuildProfile::Profiling => &["--release"] as &[&str], 711 | BuildProfile::Release => &["--release"], 712 | BuildProfile::Dev => &[], 713 | }); 714 | 715 | log::info!("Running pre-build hook"); 716 | (hooks.pre_build)(args, profile, &mut command)?; 717 | 718 | log::info!("Building frontend"); 719 | let status = command.status().context("could not start build process")?; 720 | 721 | if !status.success() { 722 | if let Some(code) = status.code() { 723 | bail!("build process exit with code {}", code); 724 | } else { 725 | bail!("build process has been terminated by a signal"); 726 | } 727 | } 728 | 729 | let wasm_path = args 730 | .target_path() 731 | .join("wasm32-unknown-unknown") 732 | .join(match profile { 733 | BuildProfile::Profiling => "release", 734 | BuildProfile::Release => "release", 735 | BuildProfile::Dev => "debug", 736 | }) 737 | .join(frontend_package.name.replace("-", "_")) 738 | .with_extension("wasm"); 739 | 740 | let mut output = Bindgen::new() 741 | .input_path(wasm_path) 742 | .out_name("app") 743 | .web(true) 744 | .expect("fails only if multiple modes specified; qed") 745 | .debug(!matches!(profile, BuildProfile::Release)) 746 | .generate_output() 747 | .context("could not generate WASM bindgen file")?; 748 | 749 | let wasm_js = output.js().to_owned(); 750 | let wasm_bin = output.wasm_mut().emit_wasm(); 751 | 752 | let wasm_bin = match profile { 753 | BuildProfile::Profiling => wasm_opt(wasm_bin, 0, 2, true, args.target_path())?, 754 | BuildProfile::Release => wasm_opt(wasm_bin, 1, 2, false, args.target_path())?, 755 | BuildProfile::Dev => wasm_bin, 756 | }; 757 | 758 | log::info!("Running post-build hook"); 759 | (hooks.post_build)(args, profile, wasm_js, wasm_bin)?; 760 | 761 | Ok(()) 762 | } 763 | 764 | #[cfg(feature = "dev-server")] 765 | fn serve_frontend( 766 | args: &dyn ServeArgs, 767 | hooks: &Hooks, 768 | ) -> Result> + Send + 'static>>> { 769 | use futures::TryFutureExt; 770 | 771 | if args.log() { 772 | tide::log::start(); 773 | } 774 | let mut app = tide::new(); 775 | 776 | (hooks.serve)(args, &mut app)?; 777 | 778 | log::info!( 779 | "Development server started: http://{}:{}", 780 | args.ip(), 781 | args.port() 782 | ); 783 | 784 | Ok(Box::pin( 785 | app.listen(format!("{}:{}", args.ip(), args.port())) 786 | .map_err(Into::into), 787 | )) 788 | } 789 | 790 | #[cfg(not(feature = "dev-server"))] 791 | fn watch_backend(args: &dyn ServeArgs, hooks: &Hooks) -> Result<()> { 792 | let (tx, rx) = mpsc::channel(); 793 | 794 | let mut watcher: RecommendedWatcher = notify::Watcher::new(tx, time::Duration::from_secs(2)) 795 | .context("could not initialize watcher")?; 796 | 797 | (hooks.backend_watch)(args, &mut watcher)?; 798 | 799 | struct BackgroundProcess(std::process::Child); 800 | 801 | impl Drop for BackgroundProcess { 802 | fn drop(&mut self) { 803 | // TODO: cleaner exit on Unix 804 | let _ = self.0.kill(); 805 | let _ = self.0.wait(); 806 | } 807 | } 808 | 809 | let run_server = || -> Result { 810 | let mut command = Command::new("cargo"); 811 | (hooks.backend_command)(args, &mut command)?; 812 | Ok(command.spawn().map(BackgroundProcess)?) 813 | }; 814 | 815 | let mut process_guard = Some(run_server()?); 816 | 817 | watch_loop(args, rx, || { 818 | drop(process_guard.take()); 819 | process_guard.replace(run_server()?); 820 | Ok(()) 821 | }); 822 | } 823 | 824 | fn watch_frontend(args: &dyn ServeArgs, hooks: &Hooks) -> Result<()> { 825 | let (tx, rx) = mpsc::channel(); 826 | 827 | let mut watcher: RecommendedWatcher = notify::Watcher::new(tx, time::Duration::from_secs(2)) 828 | .context("could not initialize watcher")?; 829 | 830 | (hooks.frontend_watch)(args, &mut watcher)?; 831 | 832 | let build_args = args.build_args(); 833 | 834 | watch_loop(args, rx, || build(BuildProfile::Dev, build_args, hooks)); 835 | } 836 | 837 | fn watch_loop( 838 | args: &dyn ServeArgs, 839 | rx: mpsc::Receiver, 840 | mut callback: impl FnMut() -> Result<()>, 841 | ) -> ! { 842 | loop { 843 | use notify::DebouncedEvent::*; 844 | 845 | let message = rx.recv(); 846 | match &message { 847 | Ok(Create(path)) | Ok(Write(path)) | Ok(Remove(path)) | Ok(Rename(_, path)) 848 | if !path.starts_with(args.build_args().build_path()) 849 | && !path.starts_with(args.build_args().target_path()) 850 | && !path 851 | .file_name() 852 | .and_then(|x| x.to_str()) 853 | .map(|x| x.starts_with('.')) 854 | .unwrap_or(false) => 855 | { 856 | if let Err(err) = callback() { 857 | log::error!("{}", err); 858 | } 859 | } 860 | Ok(_) => {} 861 | Err(e) => log::error!("Watch error: {}", e), 862 | } 863 | } 864 | } 865 | 866 | #[allow(unused_variables, unreachable_code)] 867 | fn wasm_opt( 868 | binary: Vec, 869 | shrink_level: u32, 870 | optimization_level: u32, 871 | debug_info: bool, 872 | target_path: impl AsRef, 873 | ) -> Result> { 874 | #[cfg(feature = "binaryen")] 875 | return match binaryen::Module::read(&binary) { 876 | Ok(mut module) => { 877 | module.optimize(&binaryen::CodegenConfig { 878 | shrink_level, 879 | optimization_level, 880 | debug_info, 881 | }); 882 | Ok(module.write()) 883 | } 884 | Err(()) => bail!("could not load WASM module"), 885 | }; 886 | 887 | #[cfg(feature = "prebuilt-wasm-opt")] 888 | return { 889 | let wasm_opt = prebuilt_wasm_opt::install_wasm_opt(target_path)?; 890 | 891 | let mut command = Command::new(&wasm_opt); 892 | command 893 | .stderr(Stdio::inherit()) 894 | .args(&["-o", "-", "-O"]) 895 | .args(&["-ol", &optimization_level.to_string()]) 896 | .args(&["-s", &shrink_level.to_string()]); 897 | if debug_info { 898 | command.arg("-g"); 899 | } 900 | 901 | #[cfg(target_os = "macos")] 902 | { 903 | command.env("DYLD_LIBRARY_PATH", wasm_opt.parent().unwrap()); 904 | } 905 | 906 | #[cfg(windows)] 907 | let delete_guard = { 908 | use std::io::Write; 909 | 910 | let tmp = tempfile::NamedTempFile::new()?; 911 | tmp.as_file().write_all(&binary)?; 912 | command.arg(tmp.path()); 913 | tmp 914 | }; 915 | 916 | #[cfg(unix)] 917 | { 918 | use std::io::{Seek, SeekFrom, Write}; 919 | 920 | let mut file = tempfile::tempfile()?; 921 | file.write_all(&binary)?; 922 | file.seek(SeekFrom::Start(0))?; 923 | command.stdin(file); 924 | } 925 | 926 | let output = command.output()?; 927 | if !output.status.success() { 928 | bail!("command `wasm-opt` failed."); 929 | } 930 | Ok(output.stdout) 931 | }; 932 | 933 | log::warn!("No optimization has been done on the WASM"); 934 | Ok(binary) 935 | } 936 | 937 | /// An extension for [`Package`] and for [`Metadata`] to run a cargo command a bit more easily. 938 | /// Ideal for scripting. 939 | pub trait PackageExt { 940 | /// Run the cargo command in the package's directory if ran on a [`Package`] or in the 941 | /// workspace root if ran on a [`Metadata`]. 942 | fn cargo(&self, builder: impl FnOnce(&mut Command)) -> Result; 943 | } 944 | 945 | impl PackageExt for Package { 946 | fn cargo(&self, builder: impl FnOnce(&mut Command)) -> Result { 947 | let mut command = Command::new("cargo"); 948 | command 949 | .current_dir(self.manifest_path.parent().unwrap()) 950 | .stdout(Stdio::piped()); 951 | 952 | builder(&mut command); 953 | 954 | Ok(CargoChild(command.spawn()?)) 955 | } 956 | } 957 | 958 | impl PackageExt for Metadata { 959 | fn cargo(&self, builder: impl FnOnce(&mut Command)) -> Result { 960 | let mut command = Command::new("cargo"); 961 | command 962 | .current_dir(&self.workspace_root) 963 | .stdout(Stdio::piped()); 964 | 965 | builder(&mut command); 966 | 967 | Ok(CargoChild(command.spawn()?)) 968 | } 969 | } 970 | 971 | /// A cargo child process. 972 | /// 973 | /// The child process is killed and waited if the instance is dropped. 974 | pub struct CargoChild(Child); 975 | 976 | impl CargoChild { 977 | /// Wait for the child process to finish and return an `Err(_)` if it didn't ended 978 | /// successfully. 979 | pub fn wait_success(&mut self) -> Result<()> { 980 | let status = self.0.wait()?; 981 | 982 | if let Some(code) = status.code() { 983 | if !status.success() { 984 | bail!("cargo exited with status: {}", code) 985 | } 986 | } 987 | 988 | if !status.success() { 989 | bail!("cargo exited with error") 990 | } 991 | 992 | Ok(()) 993 | } 994 | 995 | /// Creates an iterator of Message from a Read outputting a stream of JSON messages. For usage 996 | /// information, look at the top-level documentation of [`cargo_metadata`]. 997 | pub fn iter(&mut self) -> cargo_metadata::MessageIter> { 998 | let reader = BufReader::new(self.0.stdout.take().unwrap()); 999 | cargo_metadata::Message::parse_stream(reader) 1000 | } 1001 | } 1002 | 1003 | impl Drop for CargoChild { 1004 | fn drop(&mut self) { 1005 | let _ = self.0.kill(); 1006 | let _ = self.0.wait(); 1007 | } 1008 | } 1009 | 1010 | /// The wasm-run Prelude 1011 | /// 1012 | /// The purpose of this module is to alleviate imports of many common types: 1013 | /// 1014 | /// ``` 1015 | /// # #![allow(unused_imports)] 1016 | /// use wasm_run::prelude::*; 1017 | /// ``` 1018 | pub mod prelude { 1019 | pub use wasm_run_proc_macro::*; 1020 | 1021 | pub use anyhow; 1022 | #[cfg(feature = "dev-server")] 1023 | pub use async_std; 1024 | pub use cargo_metadata; 1025 | pub use cargo_metadata::{Message, Metadata, Package}; 1026 | pub use fs_extra; 1027 | #[cfg(feature = "dev-server")] 1028 | pub use futures; 1029 | pub use notify; 1030 | pub use notify::RecommendedWatcher; 1031 | #[cfg(feature = "sass")] 1032 | pub use sass_rs; 1033 | #[cfg(feature = "dev-server")] 1034 | pub use tide; 1035 | #[cfg(feature = "dev-server")] 1036 | pub use tide::Server; 1037 | 1038 | pub use super::{ 1039 | BuildArgs, BuildProfile, CargoChild, DefaultBuildArgs, DefaultServeArgs, Hooks, PackageExt, 1040 | ServeArgs, 1041 | }; 1042 | } 1043 | -------------------------------------------------------------------------------- /src/prebuilt_wasm_opt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use binary_install::Cache; 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub(crate) fn install_wasm_opt(target_path: impl AsRef) -> Result { 6 | let cache = Cache::at(target_path.as_ref()); 7 | 8 | let url = format!( 9 | "https://github.com/WebAssembly/binaryen/releases/download/version_{version}/binaryen-version_{version}-{arch}-{os}.tar.gz", 10 | version = "97", 11 | arch = platforms::TARGET_ARCH, 12 | os = platforms::TARGET_OS, 13 | ); 14 | 15 | #[cfg(target_os = "macos")] 16 | let binaries = &["wasm-opt", "libbinaryen"]; 17 | #[cfg(not(target_os = "macos"))] 18 | let binaries = &["wasm-opt"]; 19 | 20 | eprintln!("Downloading wasm-opt..."); 21 | Ok(cache 22 | .download(true, "wasm-opt", binaries, &url) 23 | .map_err(|err| err.compat()) 24 | .with_context(|| format!("could not download binaryen: {}", url))? 25 | .expect("install is permitted; qed") 26 | .binary("wasm-opt") 27 | .map_err(|err| err.compat())?) 28 | } 29 | -------------------------------------------------------------------------------- /tests/examples.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::process::Command; 3 | 4 | fn run_cargo(path: &Path, args: &[&str]) { 5 | let output = Command::new("cargo") 6 | // NOTE: this variable forces cargo to use the same toolchain but for the Rocket example 7 | // we need nightly. 8 | .env_remove("RUSTUP_TOOLCHAIN") 9 | .current_dir(path) 10 | .args(args) 11 | .output() 12 | .unwrap(); 13 | let stdout = String::from_utf8_lossy(&output.stdout); 14 | let stderr = String::from_utf8_lossy(&output.stderr); 15 | 16 | println!("stdout:\n{}\n", stdout); 17 | eprintln!("stderr:\n{}\n", stderr); 18 | assert!(output.status.success()); 19 | } 20 | 21 | #[test] 22 | fn build_example_crates() { 23 | let examples = Path::new("examples"); 24 | run_cargo( 25 | examples, 26 | &["run", "--example", "run-an-example", "--", "build"], 27 | ); 28 | run_cargo( 29 | &examples.join("frontend-only"), 30 | &["build", "--bin", "frontend-only"], 31 | ); 32 | run_cargo(&examples.join("backend-and-frontend"), &["build"]); 33 | #[cfg(target_os = "linux")] 34 | run_cargo( 35 | &examples.join("custom-cli-command"), 36 | &["run", "-p", "run", "--", "build-container-image"], 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /tests/test-binaryen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-binaryen" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../..", features = ["binaryen", "dev-server"], default-features = false } 15 | structopt = "0.3" 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /tests/test-binaryen/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | fn log(message: &str); 9 | } 10 | 11 | #[wasm_bindgen(start)] 12 | pub fn run_app() -> Result<(), JsValue> { 13 | log("Hello World!"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-binaryen/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[wasm_run::main] 4 | #[derive(StructOpt, Debug)] 5 | enum Cli {} 6 | -------------------------------------------------------------------------------- /tests/test-cargo-helper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-cargo-helper" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../.." } 15 | structopt = "0.3" 16 | -------------------------------------------------------------------------------- /tests/test-cargo-helper/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | fn log(message: &str); 9 | } 10 | 11 | #[wasm_bindgen(start)] 12 | pub fn run_app() -> Result<(), JsValue> { 13 | log("Hello World!"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-cargo-helper/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | use wasm_run::prelude::*; 3 | 4 | #[wasm_run::main(other_cli_commands)] 5 | #[derive(StructOpt, Debug)] 6 | enum Cli { 7 | Test, 8 | } 9 | 10 | fn other_cli_commands(cli: Cli, metadata: &Metadata, package: &Package) -> anyhow::Result<()> { 11 | match cli { 12 | Cli::Test => { 13 | let read_messages = |cargo: &mut CargoChild| { 14 | for message in cargo.iter() { 15 | match message.unwrap() { 16 | Message::CompilerMessage(msg) => { 17 | println!("{:?}", msg); 18 | } 19 | Message::CompilerArtifact(artifact) => { 20 | println!("{:?}", artifact); 21 | } 22 | Message::BuildScriptExecuted(script) => { 23 | println!("{:?}", script); 24 | } 25 | Message::BuildFinished(finished) => { 26 | println!("{:?}", finished); 27 | } 28 | _ => (), // Unknown message 29 | } 30 | } 31 | }; 32 | 33 | let mut cargo = package.cargo(|command| { 34 | command.args(&["build", "--message-format=json"]); 35 | })?; 36 | 37 | read_messages(&mut cargo); 38 | cargo.wait_success()?; 39 | 40 | let mut cargo = metadata.cargo(|command| { 41 | command.args(&["build", "--message-format=json"]); 42 | })?; 43 | 44 | read_messages(&mut cargo); 45 | cargo.wait_success()?; 46 | 47 | let build_path = Cli::build()?; 48 | 49 | if !build_path.exists() { 50 | anyhow::bail!("build path must exist"); 51 | } 52 | 53 | std::fs::remove_dir_all(build_path)?; 54 | 55 | let build_path = Cli::build_with_args(&["--profiling"])?; 56 | 57 | if !build_path.exists() { 58 | anyhow::bail!("build path must exist"); 59 | } 60 | 61 | Ok(()) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/test-crate-name-vs-pkg-name/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-crate-name-vs-pkg-name" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../.." } 15 | structopt = "0.3" 16 | -------------------------------------------------------------------------------- /tests/test-crate-name-vs-pkg-name/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | fn log(message: &str); 9 | } 10 | 11 | #[wasm_bindgen(start)] 12 | pub fn run_app() -> Result<(), JsValue> { 13 | log("Hello World!"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-crate-name-vs-pkg-name/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[wasm_run::main] 4 | #[derive(StructOpt, Debug)] 5 | enum Cli {} 6 | -------------------------------------------------------------------------------- /tests/test-crates.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | fn run_crate(path: &Path, args: &[&str]) { 6 | let output = Command::new("cargo") 7 | .current_dir(path) 8 | .args(&["run"]) 9 | .arg("--") 10 | .args(args) 11 | .output() 12 | .unwrap(); 13 | let stdout = String::from_utf8_lossy(&output.stdout); 14 | let stderr = String::from_utf8_lossy(&output.stderr); 15 | 16 | println!("stdout:\n{}\n", stdout); 17 | eprintln!("stderr:\n{}\n", stderr); 18 | assert!( 19 | output.status.success(), 20 | "build failed: {} args: {:?}", 21 | path.display(), 22 | args 23 | ); 24 | } 25 | 26 | #[test] 27 | fn run_test_crates() { 28 | let tests = Path::new("tests"); 29 | 30 | let crate_path = tests.join("test-crate-name-vs-pkg-name"); 31 | let build_path = Path::new("build"); 32 | let _ = fs::remove_dir_all(build_path); 33 | run_crate(&crate_path, &["build"]); 34 | assert!( 35 | build_path.exists(), 36 | "test for `crate-name-vs-pkg-name` failed" 37 | ); 38 | 39 | let crate_path = tests.join("test-cargo-helper"); 40 | run_crate(&crate_path, &["test"]); 41 | 42 | let crate_path = tests.join("test-default-build-path"); 43 | let build_path = crate_path.join("public"); 44 | let _ = fs::remove_dir_all(&build_path); 45 | run_crate(&crate_path, &["build"]); 46 | assert!(build_path.exists(), "test for `default_build_path` failed"); 47 | assert!(build_path.join("fancy.css").exists()); 48 | 49 | let crate_path = tests.join("test-binaryen"); 50 | let build_path = crate_path.join("build"); 51 | let _ = fs::remove_dir_all(&build_path); 52 | run_crate(&crate_path, &["build"]); 53 | assert!(build_path.exists(), "test for `binaryen` failed"); 54 | 55 | let crate_path = tests.join("test-no-serve"); 56 | let build_path = crate_path.join("build"); 57 | let _ = fs::remove_dir_all(&build_path); 58 | run_crate(&crate_path, &["build"]); 59 | assert!(build_path.exists(), "test for `no-serve` failed"); 60 | 61 | let crate_path = tests.join("test-sass"); 62 | let build_path = crate_path.join("build"); 63 | let _ = fs::remove_dir_all(&build_path); 64 | run_crate(&crate_path, &["build"]); 65 | assert!(build_path.exists(), "test for `test-sass` failed"); 66 | assert!(build_path.join("test1.css").exists()); 67 | assert!(build_path.join("test2.css").exists()); 68 | assert!(!build_path.join("_test3.css").exists()); 69 | assert!(build_path.join("test4.css").exists()); 70 | assert!(build_path.join("subdirectory").join("test5.css").exists()); 71 | assert!(!build_path.join("subdirectory").join("_test6.css").exists()); 72 | assert!(build_path.join("subdirectory").join("test7.css").exists()); 73 | } 74 | -------------------------------------------------------------------------------- /tests/test-default-build-path/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-default-build-path" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../.." } 15 | structopt = "0.3" 16 | -------------------------------------------------------------------------------- /tests/test-default-build-path/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | fn log(message: &str); 9 | } 10 | 11 | #[wasm_bindgen(start)] 12 | pub fn run_app() -> Result<(), JsValue> { 13 | log("Hello World!"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-default-build-path/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use structopt::StructOpt; 3 | use wasm_run::prelude::*; 4 | 5 | #[wasm_run::main(default_build_path)] 6 | #[derive(StructOpt, Debug)] 7 | enum Cli {} 8 | 9 | fn default_build_path(_metadata: &Metadata, package: &Package) -> PathBuf { 10 | package.manifest_path.parent().unwrap().join("public") 11 | } 12 | -------------------------------------------------------------------------------- /tests/test-default-build-path/static/fancy.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | -------------------------------------------------------------------------------- /tests/test-default-build-path/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/test-no-serve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-no-serve" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../..", default-features = false } 15 | structopt = "0.3" 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /tests/test-no-serve/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | fn log(message: &str); 9 | } 10 | 11 | #[wasm_bindgen(start)] 12 | pub fn run_app() -> Result<(), JsValue> { 13 | log("Hello World!"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-no-serve/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | use wasm_run::prelude::*; 3 | 4 | #[wasm_run::main()] 5 | #[derive(StructOpt, Debug)] 6 | enum Cli {} 7 | -------------------------------------------------------------------------------- /tests/test-sass/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-crate-name-vs-pkg-name" 3 | version = "0.1.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [target.'cfg(target_arch = "wasm32")'.dependencies] 11 | wasm-bindgen = "^0.2" 12 | 13 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 14 | wasm-run = { path = "../..", features = ["sass"] } 15 | structopt = "0.3" 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /tests/test-sass/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | extern "C" { 7 | #[wasm_bindgen(js_namespace = console)] 8 | fn log(message: &str); 9 | } 10 | 11 | #[wasm_bindgen(start)] 12 | pub fn run_app() -> Result<(), JsValue> { 13 | log("Hello World!"); 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-sass/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[wasm_run::main] 4 | #[derive(StructOpt, Debug)] 5 | enum Cli {} 6 | -------------------------------------------------------------------------------- /tests/test-sass/styles/_test3.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | -------------------------------------------------------------------------------- /tests/test-sass/styles/subdirectory/_test6.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | -------------------------------------------------------------------------------- /tests/test-sass/styles/subdirectory/test5.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | -------------------------------------------------------------------------------- /tests/test-sass/styles/subdirectory/test7.scss: -------------------------------------------------------------------------------- 1 | @import 'test6'; 2 | -------------------------------------------------------------------------------- /tests/test-sass/styles/test1.scss: -------------------------------------------------------------------------------- 1 | $font-stack: Helvetica, sans-serif; 2 | $primary-color: #333; 3 | 4 | body { 5 | font: 100% $font-stack; 6 | color: $primary-color; 7 | } 8 | -------------------------------------------------------------------------------- /tests/test-sass/styles/test2.sass: -------------------------------------------------------------------------------- 1 | $font-stack: Helvetica, sans-serif 2 | $primary-color: #333 3 | 4 | body 5 | font: 100% $font-stack 6 | color: $primary-color 7 | -------------------------------------------------------------------------------- /tests/test-sass/styles/test4.scss: -------------------------------------------------------------------------------- 1 | @import 'test3'; 2 | -------------------------------------------------------------------------------- /wasm-run-proc-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-run-proc-macro" 3 | version = "0.8.0" 4 | authors = ["Cecile Tonglet "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | repository = "https://github.com/IMI-eRnD-Be/wasm-run" 9 | homepage = "https://github.com/IMI-eRnD-Be/wasm-run" 10 | documentation = "https://docs.rs/wasm-run" 11 | include = ["src/**/*.rs", "README.md", "LICENSE.Apache-2.0", "LICENSE.MIT"] 12 | keywords = ["wasm", "cargo"] 13 | categories = ["development-tools::build-utils"] 14 | description = "Build tool that replaces `cargo run` to build WASM projects" 15 | 16 | [lib] 17 | # NOTE: cargo-readme needs this key for some reason 18 | path = "src/lib.rs" 19 | proc-macro = true 20 | 21 | [features] 22 | serve = [] 23 | 24 | [dependencies] 25 | cargo_metadata = "0.12.1" 26 | proc-macro2 = "1.0.24" 27 | quote = "1.0.7" 28 | syn = { version = "1.0.53", features = ["full"] } 29 | 30 | [dev-dependencies] 31 | wasm-run = { path = ".." } 32 | structopt = "0.3" 33 | -------------------------------------------------------------------------------- /wasm-run-proc-macro/LICENSE.Apache-2.0: -------------------------------------------------------------------------------- 1 | ../LICENSE.Apache-2.0 -------------------------------------------------------------------------------- /wasm-run-proc-macro/LICENSE.MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE.MIT -------------------------------------------------------------------------------- /wasm-run-proc-macro/README.md: -------------------------------------------------------------------------------- 1 | # wasm-run-proc-macro 2 | 3 | See the crate `wasm-run` for documentation. 4 | 5 | License: MIT OR Apache-2.0 6 | -------------------------------------------------------------------------------- /wasm-run-proc-macro/src/attr_parser.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Error, ParseStream, Result}; 2 | use syn::{Ident, LitStr, Path, Token}; 3 | 4 | pub struct Attr { 5 | pub other_cli_commands: Option, 6 | pub pre_build: Option, 7 | pub post_build: Option, 8 | #[cfg(feature = "dev-server")] 9 | pub serve: Option, 10 | pub frontend_watch: Option, 11 | pub frontend_pkg_name: Option, 12 | #[cfg(not(feature = "dev-server"))] 13 | pub backend_watch: Option, 14 | pub backend_pkg_name: Option, 15 | pub default_build_path: Option, 16 | pub build_args: Option, 17 | pub serve_args: Option, 18 | } 19 | 20 | impl Attr { 21 | pub fn parse(input: ParseStream) -> Result { 22 | let frontend_pkg_name = input.parse().ok(); 23 | 24 | if frontend_pkg_name.is_some() && !input.is_empty() { 25 | input.parse::()?; 26 | } 27 | 28 | let backend_pkg_name = input.parse().ok(); 29 | 30 | if backend_pkg_name.is_some() && !input.is_empty() { 31 | input.parse::()?; 32 | } 33 | 34 | let mut other_cli_commands = None; 35 | let mut pre_build = None; 36 | let mut post_build = None; 37 | #[cfg(feature = "dev-server")] 38 | let mut serve = None; 39 | let mut frontend_watch = None; 40 | #[cfg(not(feature = "dev-server"))] 41 | let mut backend_watch = None; 42 | let mut default_build_path = None; 43 | let mut build_args = None; 44 | let mut serve_args = None; 45 | 46 | while !input.is_empty() { 47 | let ident: Ident = input.parse()?; 48 | let path: Path = if input.parse::().is_ok() { 49 | input.parse()? 50 | } else { 51 | ident.clone().into() 52 | }; 53 | 54 | match ident.to_string().as_str() { 55 | "other_cli_commands" => other_cli_commands = Some(path), 56 | "pre_build" => pre_build = Some(path), 57 | "post_build" => post_build = Some(path), 58 | #[cfg(feature = "dev-server")] 59 | "serve" => serve = Some(path), 60 | #[cfg(not(feature = "dev-server"))] 61 | "backend_watch" => backend_watch = Some(path), 62 | "frontend_watch" => frontend_watch = Some(path), 63 | "default_build_path" => default_build_path = Some(path), 64 | "build_args" => build_args = Some(path), 65 | "serve_args" => serve_args = Some(path), 66 | _ => return Err(Error::new(ident.span(), "invalid argument")), 67 | } 68 | 69 | let _comma_token: Token![,] = match input.parse() { 70 | Ok(x) => x, 71 | Err(_) if input.is_empty() => break, 72 | Err(err) => return Err(err), 73 | }; 74 | } 75 | 76 | Ok(Self { 77 | other_cli_commands, 78 | pre_build, 79 | post_build, 80 | #[cfg(feature = "dev-server")] 81 | serve, 82 | frontend_watch, 83 | frontend_pkg_name, 84 | #[cfg(not(feature = "dev-server"))] 85 | backend_watch, 86 | backend_pkg_name, 87 | default_build_path, 88 | build_args, 89 | serve_args, 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /wasm-run-proc-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! See the crate `wasm-run` for documentation. 2 | 3 | mod attr_parser; 4 | mod main_generator; 5 | 6 | use cargo_metadata::MetadataCommand; 7 | use proc_macro::TokenStream; 8 | use syn::{parse_macro_input, ItemEnum}; 9 | 10 | /// Makes an entrypoint to your binary (not WASM). 11 | /// 12 | /// ```ignore 13 | /// #[wasm_run::main( 14 | /// pre_build = my_pre_build_function, 15 | /// post_build = my_post_build_function, 16 | /// build_args = my_build_args_struct, 17 | /// serve_args = my_serve_args_struct, 18 | /// // ... see below for all the available arguments 19 | /// )] 20 | /// enum Cli {} 21 | /// ``` 22 | /// 23 | /// It requires to be used with `structopt` on an `enum`. Please consult the documentation of 24 | /// `structopt` if you don't know how to make an `enum` with it. 25 | /// 26 | /// There are a number of named arguments you can provide to the macro: 27 | /// - `other_cli_commands`: a function that is called if you have added new commands to the 28 | /// `enum`; 29 | /// - `pre_build`: a function that is called when the build has not yet started (you can tweak 30 | /// the command-line arguments of the build command); 31 | /// - `post_build`: a function that is called when the build is finished (after the optimization 32 | /// with `wasm-opt`); 33 | /// - `frontend_watch`: a function that is called when the watcher is being initialized (allowing 34 | /// you to add extra things to watch for example); 35 | /// - `backend_watch`: a function that is called when the watcher is being initialized (allowing 36 | /// you to add extra things to watch for example); 37 | /// - `serve`: (only if built with the `serve` feature): a function that is called when the HTTP 38 | /// serve is getting configured; 39 | /// - `default_build_path`: a function that is called that provides the default directory path 40 | /// when the user didn't provide it through the command-line arguments (the default is 41 | /// `workspace root/build`); 42 | /// - `build_args`: allow you to override the `build` command when providing a custom argument 43 | /// (the default is `DefaultBuildArgs`); 44 | /// - `serve_args`: allow you to override the `serve` command when providing a custom argument 45 | /// (the default is `DefaultServeArgs`). 46 | /// 47 | /// You can also change the frontend package that is built by providing its name in the first 48 | /// positional argument: 49 | /// 50 | /// ```ignore 51 | /// #[wasm_run::main("my-frontend-package")] 52 | /// enum Cli {} 53 | /// ``` 54 | /// 55 | /// And the backend package in the second positional argument: 56 | /// 57 | /// ```ignore 58 | /// #[wasm_run::main("my-frontend-package", "my-backend-package")] 59 | /// enum Cli {} 60 | /// ``` 61 | /// 62 | /// # Examples 63 | /// 64 | /// See the [`examples/`](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/custom-cli-command) 65 | /// directory. 66 | #[proc_macro_attribute] 67 | pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { 68 | let item = parse_macro_input!(item as ItemEnum); 69 | let attr = parse_macro_input!(attr with attr_parser::Attr::parse); 70 | let metadata = MetadataCommand::new() 71 | .exec() 72 | .expect("could not get metadata"); 73 | 74 | main_generator::generate(item, attr, &metadata) 75 | .unwrap_or_else(|err| err.to_compile_error()) 76 | .into() 77 | } 78 | -------------------------------------------------------------------------------- /wasm-run-proc-macro/src/main_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::attr_parser::Attr; 2 | use cargo_metadata::Metadata; 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, quote_spanned}; 5 | use syn::spanned::Spanned; 6 | use syn::{Error, ItemEnum}; 7 | 8 | pub fn generate(item: ItemEnum, attr: Attr, metadata: &Metadata) -> syn::Result { 9 | let ident = &item.ident; 10 | let Attr { 11 | other_cli_commands, 12 | pre_build, 13 | post_build, 14 | #[cfg(feature = "dev-server")] 15 | serve, 16 | frontend_watch, 17 | frontend_pkg_name, 18 | backend_watch, 19 | backend_pkg_name, 20 | default_build_path, 21 | build_args, 22 | serve_args, 23 | } = attr; 24 | 25 | if let Some(serve_args) = serve_args.as_ref() { 26 | if build_args.is_none() { 27 | return Err(Error::new( 28 | serve_args.span(), 29 | "if you use a custom ServeArgs, you must use a custom BuildArgs", 30 | )); 31 | } 32 | } 33 | 34 | let build_ty = if let Some(ty) = build_args { 35 | quote! { #ty } 36 | } else { 37 | quote! { ::wasm_run::DefaultBuildArgs } 38 | }; 39 | 40 | let serve_ty = if let Some(ty) = serve_args { 41 | quote! { #ty } 42 | } else { 43 | quote! { ::wasm_run::DefaultServeArgs } 44 | }; 45 | 46 | let span = other_cli_commands.span(); 47 | let other_cli_commands = other_cli_commands 48 | .map(|x| { 49 | quote_spanned! {span=> 50 | WasmRunCliCommand::Other(cli) => #x(cli, metadata, package)?, 51 | } 52 | }) 53 | .unwrap_or_else(|| { 54 | if !item.variants.is_empty() { 55 | quote! { 56 | cli => compile_error!( 57 | "missing `other_cli_commands` to handle all the variants", 58 | ), 59 | } 60 | } else { 61 | quote! { 62 | WasmRunCliCommand::Other(x) => match x {}, 63 | } 64 | } 65 | }); 66 | 67 | let pre_build = pre_build.map(|path| { 68 | quote_spanned! {path.span()=> 69 | pre_build: Box::new(|args, profile, command| { 70 | let args = args.downcast_ref::<#build_ty>() 71 | .expect("invalid type for `Build` command: the type in the command enum \ 72 | must be the same than the type returned by `build_args()` \ 73 | in the implementation of the trait `ServeArgs`"); 74 | #path(args, profile, command) 75 | }), 76 | } 77 | }); 78 | 79 | let post_build = post_build.map(|path| { 80 | quote_spanned! {path.span()=> 81 | post_build: Box::new(|args, profile, wasm_js, wasm_bin| { 82 | let args = args.downcast_ref::<#build_ty>() 83 | .expect("invalid type for `Build` command: the type in the command enum \ 84 | must be the same than the type returned by `build_args()` \ 85 | in the implementation of the trait `ServeArgs`"); 86 | #path(args, profile, wasm_js, wasm_bin) 87 | }), 88 | } 89 | }); 90 | 91 | #[cfg(feature = "dev-server")] 92 | let serve = serve.map(|path| { 93 | quote_spanned! {path.span()=> 94 | serve: Box::new(|args, app| { 95 | let args = args.downcast_ref::<#serve_ty>().unwrap(); 96 | #path(args, app) 97 | }), 98 | } 99 | }); 100 | #[cfg(not(feature = "dev-server"))] 101 | let serve = quote! {}; 102 | 103 | let frontend_watch = frontend_watch.map(|path| { 104 | quote_spanned! {path.span()=> 105 | frontend_watch: Box::new(|args, watcher| { 106 | let args = args.downcast_ref::<#serve_ty>().unwrap(); 107 | #path(args, watcher) 108 | }), 109 | } 110 | }); 111 | 112 | #[cfg(not(feature = "dev-server"))] 113 | let backend_watch = backend_watch.map(|path| { 114 | quote_spanned! {path.span()=> 115 | backend_watch: Box::new(|args, watcher| { 116 | let args = args.downcast_ref::<#serve_ty>().unwrap(); 117 | #path(args, watcher) 118 | }), 119 | } 120 | }); 121 | 122 | if let Some(pkg_name) = frontend_pkg_name.as_ref() { 123 | let span = pkg_name.span(); 124 | let pkg_name = pkg_name.value(); 125 | if !metadata.packages.iter().any(|x| x.name == pkg_name) { 126 | return Err(Error::new( 127 | span, 128 | format!("package `{}` not found", pkg_name), 129 | )); 130 | } 131 | } 132 | 133 | let frontend_pkg_name = frontend_pkg_name.map(|x| quote! { #x }).unwrap_or_else(|| { 134 | let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap(); 135 | quote! { #pkg_name } 136 | }); 137 | 138 | if let Some(pkg_name) = backend_pkg_name.as_ref() { 139 | let span = pkg_name.span(); 140 | let pkg_name = pkg_name.value(); 141 | if !metadata.packages.iter().any(|x| x.name == pkg_name) { 142 | return Err(Error::new( 143 | span, 144 | format!("package `{}` not found", pkg_name), 145 | )); 146 | } 147 | } 148 | 149 | let backend_pkg_name = backend_pkg_name 150 | .map(|x| quote! { Some(#x) }) 151 | .unwrap_or_else(|| { 152 | quote! { None } 153 | }); 154 | 155 | let default_build_path = if let Some(path) = default_build_path { 156 | quote_spanned! {path.span()=> 157 | Some(Box::new(|metadata, package| { 158 | #path(metadata, package) 159 | })) 160 | } 161 | } else { 162 | quote! { None } 163 | }; 164 | 165 | Ok(quote! { 166 | #item 167 | 168 | impl #ident { 169 | fn build() -> ::wasm_run::prelude::anyhow::Result<::std::path::PathBuf> 170 | { 171 | use ::wasm_run::BuildArgs; 172 | let build_args = #build_ty::from_iter_safe(&[#frontend_pkg_name])?; 173 | build_args.run() 174 | } 175 | 176 | fn build_with_args(iter: I) 177 | -> ::wasm_run::prelude::anyhow::Result<::std::path::PathBuf> 178 | where 179 | I: ::std::iter::IntoIterator, 180 | I::Item: ::std::convert::Into<::std::ffi::OsString> + Clone, 181 | { 182 | use ::wasm_run::BuildArgs; 183 | let iter = ::std::iter::once(::std::ffi::OsString::from(#frontend_pkg_name)) 184 | .chain(iter.into_iter().map(|x| x.into())); 185 | let build_args = #build_ty::from_iter_safe(iter)?; 186 | build_args.run() 187 | } 188 | } 189 | 190 | fn main() -> ::wasm_run::prelude::anyhow::Result<()> { 191 | use ::std::path::PathBuf; 192 | use ::wasm_run::structopt::StructOpt; 193 | use ::wasm_run::prelude::*; 194 | 195 | #[derive(::wasm_run::structopt::StructOpt)] 196 | struct WasmRunCli { 197 | #[structopt(subcommand)] 198 | command: Option, 199 | } 200 | 201 | #[derive(::wasm_run::structopt::StructOpt)] 202 | enum WasmRunCliCommand { 203 | Build(#build_ty), 204 | Serve(#serve_ty), 205 | #[structopt(flatten)] 206 | Other(#ident), 207 | } 208 | 209 | let cli = WasmRunCli::from_args(); 210 | 211 | #[allow(clippy::needless_update)] 212 | let hooks = Hooks { 213 | #pre_build 214 | #post_build 215 | #serve 216 | #frontend_watch 217 | #backend_watch 218 | .. Hooks::default() 219 | }; 220 | 221 | let (metadata, package) = ::wasm_run::wasm_run_init( 222 | #frontend_pkg_name, 223 | #backend_pkg_name, 224 | #default_build_path, 225 | hooks, 226 | )?; 227 | 228 | if let Some(cli) = cli.command { 229 | match cli { 230 | WasmRunCliCommand::Build(args) => { 231 | args.run()?; 232 | }, 233 | WasmRunCliCommand::Serve(args) => args.run()?, 234 | #other_cli_commands 235 | } 236 | } else { 237 | #serve_ty::from_args().run()?; 238 | } 239 | 240 | Ok(()) 241 | } 242 | }) 243 | } 244 | --------------------------------------------------------------------------------