From 14f415a5610fd065966aede20365649595c59104 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Wed, 22 Jun 2022 20:22:54 +1000 Subject: [PATCH] feat: removed `.perseus/` (#151) * feat: made the engine code functional in the user's code No support for serving yet and there's still Wasm binary bloat. * feat: added support for serving Still a lot of Wasm binary bloat. * feat: integrated client-side code This is untested as yet, I'll update the CLI first. * feat: updated the cli and brought everything together This is still untested because of a Cargo dependency unification issue. * fix: fixed all issues by changing the cargo resolver This is literally dark magic. * fix: fixed `HydrateNode`/`DomNode` issues and reformed original structure * fix: fixed small errors with cli * fix: fixed some macro and example errors * style: appeased `clippy` * chore: updated bonnie checking script * chore: deleted `.perseus/`! * feat: added convenience macros Also changed the `run_dflt_engine` and `run_client` APIs to make them all take functions that return apps, rather than apps directly, which makes the Actix Web integration able to work normally (by making everything else share its quirks). * feat: added more convenience macros * refactor: broke out macros under a new features This feature is the default though. * feat: added dflt engine system for export-only apps * test: updated all examples Except `fetching`, need to merge from `main`. * test: updated `fetching` example * style: appeased clippy * fix: fixed unused code warnings from macros Just made the user's functions `pub` to stop the compiler whining. * feat: added `#[main_export]` for apps not using a server * feat: added support for custom cargo/wasm-pack args to cli * fix: fixed actix dflt server return type * test: made all examples work with all integrations There's now an `EXAMPLE_INTEGRATION` environment variable that controls this, which is set to `warp` by default in a new `.env` file, which Bonnie can read. * fix: fixed i18n translator misreference * feat: superseded `autoserde` macro This also adds a `build_paths` macro to make that work with the new systems. * test: fixed testing script * chore: updated `bonnie.toml` for new layout * ci: removed `ci-prep` calls and added `wasm32-unknown-unknown` target for `check` op * ci: fixed remnant `ci-prep` invocation * fix: fixed global build state server/client division * fix: added missing build paths annotation * test: removed unnecessary test from plugins example This made it uncompilable because of the silly way I've set that up. I'll fix that as I rewrite the docs for v0.4.0. * chore: updated `bn test` command with all core examples * feat: added `should_revalidate` macro * fix: fixed imports in headers example * fix: fixed static content paths in new layout * fix: fixed imports in state generation example BREAKING CHANGE: Changed multiple APIs for functional plugin actions related to the builder (they all take the new EngineError type now) Restructured exports related to engine functionality (this will get progressively worse as this PR develops!) Removed the HOST and PORT environment variables for configuring the server (these are replaced with PERSEUS_HOST and PERSEUS_PORT) Substantially refactored exports from Perseus Divided client-side and server-side exports (many functions will now need to be target-gated) Replaced #[autoserde(...)] macro with macros for each state function (#[build_state], #[build_paths], etc.) The #[build_paths] macro must now be applied to all build paths functions (for client/server functionality division) #[perseus::main] now takes an argument as the default server to use (server integrations should now be imported and used) Made state functions automatically target-gated as #[cfg(not(feature = "wasm32"))] The #[should_revalidate] macro must now be applied to all revalidation determination functions (for client/server functionality division) --- .env | 1 + .github/workflows/cd.yml | 2 - .github/workflows/ci.yml | 4 +- Cargo.toml | 16 +- bonnie.toml | 37 ++- examples/comprehensive/tiny/.gitignore | 3 +- examples/comprehensive/tiny/Cargo.toml | 16 ++ examples/comprehensive/tiny/index.html | 11 - examples/comprehensive/tiny/src/lib.rs | 4 +- examples/core/basic/.gitignore | 1 + examples/core/basic/.perseus/.gitignore | 2 - examples/core/basic/.perseus/Cargo.toml | 29 --- .../core/basic/.perseus/builder/Cargo.toml | 36 --- .../builder/src/bin/export_error_page.rs | 93 -------- .../basic/.perseus/builder/src/bin/tinker.rs | 24 -- .../core/basic/.perseus/server/Cargo.toml | 34 --- .../core/basic/.perseus/server/src/main.rs | 171 -------------- examples/core/basic/.perseus/src/lib.rs | 67 ------ examples/core/basic/Cargo.toml | 20 +- examples/core/basic/index.html | 10 - examples/core/basic/src/lib.rs | 25 +- examples/core/basic/src/templates/index.rs | 2 +- examples/core/freezing_and_thawing/.gitignore | 2 +- examples/core/freezing_and_thawing/Cargo.toml | 17 +- examples/core/freezing_and_thawing/index.html | 10 - .../freezing_and_thawing/src/global_state.rs | 2 +- examples/core/freezing_and_thawing/src/lib.rs | 2 +- .../src/templates/index.rs | 2 +- examples/core/global_state/.gitignore | 2 +- examples/core/global_state/Cargo.toml | 17 +- examples/core/global_state/index.html | 10 - .../core/global_state/src/global_state.rs | 2 +- examples/core/global_state/src/lib.rs | 2 +- examples/core/i18n/.gitignore | 5 +- examples/core/i18n/Cargo.toml | 17 +- examples/core/i18n/index.html | 11 - examples/core/i18n/src/lib.rs | 2 +- examples/core/i18n/src/templates/post.rs | 3 +- examples/core/idb_freezing/.gitignore | 2 +- examples/core/idb_freezing/Cargo.toml | 17 +- examples/core/idb_freezing/index.html | 10 - .../core/idb_freezing/src/global_state.rs | 2 +- examples/core/idb_freezing/src/lib.rs | 2 +- .../core/idb_freezing/src/templates/about.rs | 10 +- .../core/idb_freezing/src/templates/index.rs | 18 +- examples/core/index_view/.gitignore | 3 +- examples/core/index_view/Cargo.toml | 17 +- examples/core/index_view/src/lib.rs | 2 +- examples/core/plugins/.gitignore | 2 +- examples/core/plugins/Cargo.toml | 17 +- examples/core/plugins/index.html | 10 - examples/core/plugins/src/lib.rs | 2 +- examples/core/plugins/src/plugin.rs | 10 +- examples/core/plugins/tests/main.rs | 2 - examples/core/router_state/.gitignore | 2 +- examples/core/router_state/Cargo.toml | 16 ++ examples/core/router_state/index.html | 10 - examples/core/router_state/src/lib.rs | 2 +- examples/core/rx_state/.gitignore | 2 +- examples/core/rx_state/Cargo.toml | 17 +- examples/core/rx_state/index.html | 10 - examples/core/rx_state/src/lib.rs | 2 +- examples/core/rx_state/src/templates/index.rs | 2 +- examples/core/set_headers/.gitignore | 2 +- examples/core/set_headers/Cargo.toml | 17 +- examples/core/set_headers/index.html | 10 - examples/core/set_headers/src/lib.rs | 2 +- .../core/set_headers/src/templates/index.rs | 15 +- examples/core/state_generation/.gitignore | 2 +- examples/core/state_generation/Cargo.toml | 19 +- examples/core/state_generation/index.html | 10 - examples/core/state_generation/src/lib.rs | 2 +- .../src/templates/amalgamation.rs | 10 +- .../src/templates/build_paths.rs | 3 +- .../src/templates/build_state.rs | 2 +- .../src/templates/incremental_generation.rs | 3 +- .../src/templates/request_state.rs | 6 +- .../src/templates/revalidation.rs | 3 +- ...revalidation_and_incremental_generation.rs | 13 +- examples/core/static_content/.gitignore | 2 +- examples/core/static_content/Cargo.toml | 17 +- examples/core/static_content/index.html | 12 - examples/core/static_content/src/lib.rs | 2 +- examples/core/unreactive/.gitignore | 2 +- examples/core/unreactive/Cargo.toml | 17 +- examples/core/unreactive/index.html | 10 - examples/core/unreactive/src/lib.rs | 2 +- .../core/unreactive/src/templates/index.rs | 2 +- examples/demos/auth/.gitignore | 2 +- examples/demos/auth/Cargo.toml | 16 ++ examples/demos/auth/src/global_state.rs | 4 +- examples/demos/auth/src/lib.rs | 2 +- examples/demos/auth/src/templates/index.rs | 2 + examples/demos/fetching/.gitignore | 2 +- examples/demos/fetching/Cargo.toml | 17 +- examples/demos/fetching/src/lib.rs | 2 +- .../demos/fetching/src/templates/index.rs | 9 +- packages/perseus-actix-web/Cargo.toml | 4 + packages/perseus-actix-web/src/dflt_server.rs | 36 +++ packages/perseus-actix-web/src/lib.rs | 4 + packages/perseus-axum/Cargo.toml | 4 + packages/perseus-axum/src/dflt_server.rs | 27 +++ packages/perseus-axum/src/lib.rs | 4 + packages/perseus-cli/Cargo.toml | 3 - packages/perseus-cli/build.rs | 105 --------- packages/perseus-cli/src/bin/main.rs | 57 +---- packages/perseus-cli/src/build.rs | 63 ++--- packages/perseus-cli/src/cmd.rs | 9 +- packages/perseus-cli/src/deploy.rs | 25 +- packages/perseus-cli/src/eject.rs | 52 ----- packages/perseus-cli/src/errors.rs | 107 +-------- packages/perseus-cli/src/export.rs | 36 ++- packages/perseus-cli/src/export_error_page.rs | 9 +- packages/perseus-cli/src/extraction.rs | 30 --- packages/perseus-cli/src/lib.rs | 25 +- packages/perseus-cli/src/parse.rs | 64 +---- packages/perseus-cli/src/prepare.rs | 220 +---------------- packages/perseus-cli/src/serve.rs | 60 ++--- packages/perseus-cli/src/serve_exported.rs | 2 +- packages/perseus-cli/src/snoop.rs | 18 +- packages/perseus-cli/src/tinker.rs | 10 +- packages/perseus-integration/Cargo.toml | 18 ++ packages/perseus-integration/README.md | 5 + packages/perseus-integration/src/lib.rs | 6 + packages/perseus-macro/src/entrypoint.rs | 188 ++++++++++++++- packages/perseus-macro/src/head.rs | 8 + packages/perseus-macro/src/lib.rs | 168 ++++++++++--- .../src/{autoserde.rs => state_fns.rs} | 119 ++++++---- packages/perseus-macro/src/template.rs | 2 +- packages/perseus-macro/src/template_rx.rs | 16 +- packages/perseus-warp/Cargo.toml | 4 + packages/perseus-warp/src/dflt_server.rs | 24 ++ packages/perseus-warp/src/lib.rs | 4 + packages/perseus/Cargo.toml | 43 ++-- packages/perseus/src/client.rs | 59 +++++ .../perseus/src/engine}/build.rs | 47 ++-- packages/perseus/src/engine/dflt_engine.rs | 104 +++++++++ .../perseus/src/engine}/export.rs | 166 ++++++------- .../perseus/src/engine/export_error_page.rs | 64 +++++ packages/perseus/src/engine/get_op.rs | 50 ++++ packages/perseus/src/engine/mod.rs | 19 ++ packages/perseus/src/engine/serve.rs | 103 ++++++++ packages/perseus/src/engine/tinker.rs | 16 ++ packages/perseus/src/error_pages.rs | 29 ++- packages/perseus/src/errors.rs | 47 ++++ packages/perseus/src/export.rs | 4 +- packages/perseus/src/i18n/mod.rs | 4 + .../perseus/src/i18n/translations_manager.rs | 33 +++ packages/perseus/src/init.rs | 211 ++++++++++++----- packages/perseus/src/lib.rs | 47 +++- packages/perseus/src/macros.rs | 7 +- .../perseus/src/{server => }/page_data.rs | 0 packages/perseus/src/plugins/functional.rs | 44 ++-- packages/perseus/src/router/app_route.rs | 104 ++++++--- packages/perseus/src/router/mod.rs | 2 + .../perseus/src/router/router_component.rs | 31 ++- packages/perseus/src/server/html_shell.rs | 6 +- packages/perseus/src/server/mod.rs | 2 - packages/perseus/src/server/render.rs | 2 +- packages/perseus/src/shell.rs | 4 +- packages/perseus/src/state/global_state.rs | 23 +- packages/perseus/src/state/mod.rs | 14 +- packages/perseus/src/stores/immutable.rs | 19 +- packages/perseus/src/stores/mutable.rs | 14 ++ packages/perseus/src/template/core.rs | 221 +++++++++++------- packages/perseus/src/template/mod.rs | 4 + packages/perseus/src/template/render_ctx.rs | 1 - packages/perseus/src/utils/mod.rs | 6 +- packages/perseus/src/utils/path_prefix.rs | 11 +- scripts/test.rs | 14 +- 170 files changed, 2304 insertions(+), 1951 deletions(-) create mode 100644 .env delete mode 100644 examples/comprehensive/tiny/index.html create mode 100644 examples/core/basic/.gitignore delete mode 100644 examples/core/basic/.perseus/.gitignore delete mode 100644 examples/core/basic/.perseus/Cargo.toml delete mode 100644 examples/core/basic/.perseus/builder/Cargo.toml delete mode 100644 examples/core/basic/.perseus/builder/src/bin/export_error_page.rs delete mode 100644 examples/core/basic/.perseus/builder/src/bin/tinker.rs delete mode 100644 examples/core/basic/.perseus/server/Cargo.toml delete mode 100644 examples/core/basic/.perseus/server/src/main.rs delete mode 100644 examples/core/basic/.perseus/src/lib.rs delete mode 100644 examples/core/basic/index.html delete mode 100644 examples/core/freezing_and_thawing/index.html delete mode 100644 examples/core/global_state/index.html delete mode 100644 examples/core/i18n/index.html delete mode 100644 examples/core/idb_freezing/index.html delete mode 100644 examples/core/plugins/index.html delete mode 100644 examples/core/router_state/index.html delete mode 100644 examples/core/rx_state/index.html delete mode 100644 examples/core/set_headers/index.html delete mode 100644 examples/core/state_generation/index.html delete mode 100644 examples/core/static_content/index.html delete mode 100644 examples/core/unreactive/index.html create mode 100644 packages/perseus-actix-web/src/dflt_server.rs create mode 100644 packages/perseus-axum/src/dflt_server.rs delete mode 100644 packages/perseus-cli/build.rs delete mode 100644 packages/perseus-cli/src/eject.rs delete mode 100644 packages/perseus-cli/src/extraction.rs create mode 100644 packages/perseus-integration/Cargo.toml create mode 100644 packages/perseus-integration/README.md create mode 100644 packages/perseus-integration/src/lib.rs rename packages/perseus-macro/src/{autoserde.rs => state_fns.rs} (65%) create mode 100644 packages/perseus-warp/src/dflt_server.rs create mode 100644 packages/perseus/src/client.rs rename {examples/core/basic/.perseus/builder/src/bin => packages/perseus/src/engine}/build.rs (64%) create mode 100644 packages/perseus/src/engine/dflt_engine.rs rename {examples/core/basic/.perseus/builder/src/bin => packages/perseus/src/engine}/export.rs (51%) create mode 100644 packages/perseus/src/engine/export_error_page.rs create mode 100644 packages/perseus/src/engine/get_op.rs create mode 100644 packages/perseus/src/engine/mod.rs create mode 100644 packages/perseus/src/engine/serve.rs create mode 100644 packages/perseus/src/engine/tinker.rs rename packages/perseus/src/{server => }/page_data.rs (100%) diff --git a/.env b/.env new file mode 100644 index 0000000000..0cf3b0bb60 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +EXAMPLE_INTEGRATION=warp diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 08d091ddc0..84bbb1a3ac 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -24,7 +24,6 @@ jobs: steps: - uses: actions/checkout@v2 - run: cargo install bonnie - - run: bonnie ci-prep - name: Build run: cargo build --release working-directory: packages/perseus-cli @@ -45,7 +44,6 @@ jobs: - name: Install musl toolchain run: rustup target add x86_64-unknown-linux-musl - run: cargo install bonnie - - run: bonnie ci-prep - name: Build run: cargo build --release --target x86_64-unknown-linux-musl working-directory: packages/perseus-cli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a38ba66ddd..1777c63d89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - run: cargo install bonnie - - run: bonnie ci-prep + - run: rustup target add wasm32-unknown-unknown - name: Run checks run: bonnie check test: @@ -20,7 +20,6 @@ jobs: steps: - uses: actions/checkout@v2 - run: cargo install bonnie - - run: bonnie ci-prep - name: Run traditional tests run: cargo test --all # We now have a separate job for each example's E2E testing because they all take a while, we may as well run them in parallel @@ -64,6 +63,5 @@ jobs: - run: sudo apt install firefox firefox-geckodriver - name: Run Firefox WebDriver run: geckodriver & - - run: bonnie ci-prep - name: Run E2E tests for example ${{ matrix.name }} in category ${{ matrix.type }} run: bonnie test example-all-integrations ${{ matrix.type }} ${{ matrix.name }} --headless diff --git a/Cargo.toml b/Cargo.toml index b577e95e14..88df8baa9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,17 @@ members = [ "website", # We have the CLI subcrates as workspace members so we can actively develop on them # They also can't be a workspace until nested workspaces are supported - "examples/core/basic/.perseus", - "examples/core/basic/.perseus/server", - "examples/core/basic/.perseus/builder" + # "examples/core/basic/.perseus", + # "examples/core/basic/.perseus/server", + # "examples/core/basic/.perseus/builder" ] +resolver = "2" + +[patch.crates-io] +sycamore = { git = "https://github.com/arctic-hen7/sycamore" } +sycamore-router = { git = "https://github.com/arctic-hen7/sycamore" } +sycamore-router-macro = { git = "https://github.com/arctic-hen7/sycamore" } +sycamore-macro = { git = "https://github.com/arctic-hen7/sycamore" } +sycamore-core = { git = "https://github.com/arctic-hen7/sycamore" } +sycamore-reactive = { git = "https://github.com/arctic-hen7/sycamore" } +sycamore-web = { git = "https://github.com/arctic-hen7/sycamore" } diff --git a/bonnie.toml b/bonnie.toml index 2952fee551..5b9de39fc1 100644 --- a/bonnie.toml +++ b/bonnie.toml @@ -1,18 +1,13 @@ version="0.3.2" +env_files = [ ".env" ] [scripts] setup.cmd.generic = [ - "mkdir -p examples/core/basic/.perseus/dist", - "mkdir -p examples/core/basic/.perseus/dist/static", - "mkdir -p examples/core/basic/.perseus/dist/exported", "cargo build", "npm i --prefix ./website", "echo \"\n\nThe Perseus repository is ready for local development! Type 'bonnie help' to see the available commands you can run here. Also, please ensure that you have 'npx' available and that you've installed 'tailwindcss', `concurrently`, `serve` and 'browser-sync' ('npm i -g tailwindcss concurrently serve browser-sync') if you'll be working with the website or running `bonnie dev export-serve ...`.\"" ] setup.cmd.targets.windows = [ - "New-Item -Force -ItemType directory -Path examples\\core\\basic\\.perseus\\dist", - "New-Item -Force -ItemType directory -Path examples\\core\\basic\\.perseus\\dist\\static", - "New-Item -Force -ItemType directory -Path examples\\core\\basic\\.perseus\\dist\\exported", "cargo build", "npm i --prefix ./website", "Write-Host \"\n\nThe Perseus repository is ready for local development! Type 'bonnie help' to see the available commands you can run here. Also, please ensure that you have 'npx' available and that you've installed 'tailwindcss', `concurrently`, `serve` and 'browser-sync' ('npm i -g tailwindcss concurrently serve browser-sync') if you'll be working with the website or running `bonnie dev export-serve ...`.\"" @@ -40,17 +35,19 @@ dev.subcommands.export-serve-deploy-relative.cmd.targets.windows = [ dev.subcommands.export-serve-deploy-relative.args = [ "category", "example" ] dev.subcommands.export-serve-deploy-relative.desc = "deploys (exported) and serves the given example at a relative local path" +# TODO Make this not set the integration feature unless a certain file in the example calls for it dev.subcommands.example.cmd.generic = [ "cd packages/perseus-cli", # Point this live version of the CLI at the given example - "TEST_EXAMPLE=../../examples/%category/%example cargo run -- %%" + "TEST_EXAMPLE=../../examples/%category/%example PERSEUS_CARGO_ARGS=\"--features \"perseus-integration/%EXAMPLE_INTEGRATION\"\" cargo run -- %%" ] dev.subcommands.example.cmd.targets.windows = [ "cd packages\\perseus-cli", # Point this live version of the CLI at the given example - "powershell -Command { $env:TEST_EXAMPLE=\"..\\..\\examples\\%category\\%example\"; cargo run -- %% }" + "powershell -Command { $env:TEST_EXAMPLE=\"..\\..\\examples\\%category\\%example\"; $end:PERSEUS_CARGO_ARGS=\"--features \"perseus-integration/%EXAMPLE_INTEGRATION\"\"; cargo run -- %% }" ] dev.subcommands.example.args = [ "category", "example" ] +dev.subcommands.example.env_vars = [ "EXAMPLE_INTEGRATION" ] # This will be set automatically to Warp by `.env` unless overridden dev.subcommands.example.desc = "runs the given example using a live version of the cli" site.cmd = "concurrently \"bonnie site export\" \"bonnie site build-tailwind\"" @@ -143,20 +140,13 @@ site.subcommands.run.desc = "runs the website without watching for changes" check.cmd = [ "cargo check --all", "cargo fmt --all -- --check", - "cargo clippy --all" + "cargo clippy --all", + # We also have to check the `perseus` package in particular on Wasm (the examples are handled by the E2E tests) + "cd packages/perseus", + "cargo check --target wasm32-unknown-unknown" ] check.desc = "checks code for formatting errors and the like" -ci-prep.cmd.generic = [ - "mkdir -p examples/core/basic/.perseus/dist", - "mkdir -p examples/core/basic/.perseus/dist/static", -] -ci-prep.cmd.targets.windows = [ - "New-Item -Force -ItemType directory -Path examples\\core\\basic\\.perseus\\dist", - "New-Item -Force -ItemType directory -Path examples\\core\\basic\\.perseus\\static", -] -ci-prep.desc = "creates empty directories to preserve the file structure that testing expects" - test.cmd = [ "cargo test", # This will ignore Wasm tests # Run tests for each example @@ -169,7 +159,10 @@ test.cmd = [ "bonnie test example-all-integrations core global_state --headless", "bonnie test example-all-integrations core idb_freezing --headless", "bonnie test example-all-integrations core router_state --headless", - "bonnie test example-all-integrations core rx_state --headless" + "bonnie test example-all-integrations core rx_state --headless", + "bonnie test example-all-integrations core index_view --headless", + "bonnie test example-all-integrations core set_headers --headless", + "bonnie test example-all-integrations core static_content --headless" ] test.desc = "runs all tests headlessly (assumes geckodriver running in background)" test.subcommands.core.cmd = "cargo test" @@ -204,7 +197,7 @@ release.desc = "creates a new project release and pushes it to github (cargo ver # --- COMMANDS FOLLOWING THIS POINT ARE LINUX-ONLY --- -replace-versions.cmd = "find . \\( \\( -name \"*Cargo.toml\" -or -name \"*Cargo.toml.example\" -or \\( -name \"*.md\" -not -name \"*.proj.md\" \\) \\) -not -name \"CHANGELOG.md\" -not -path \"./target/*\" -not -path \"./website/*\" -not -path \"*.perseus*\" -or \\( -name \"*Cargo.toml\" -path \"./examples/core/basic/.perseus/*\" -not -path \"./examples/core/basic/.perseus/dist/*\" \\) \\) -exec sed -i -e 's/%old_version/%new_version/g' {} \\;" +replace-versions.cmd = "find . \\( \\( -name \"*Cargo.toml\" -or -name \"*Cargo.toml.example\" -or \\( -name \"*.md\" -not -name \"*.proj.md\" \\) \\) -not -name \"CHANGELOG.md\" -not -path \"./target/*\" -not -path \"./website/*\" \\) -exec sed -i -e 's/%old_version/%new_version/g' {} \\;" replace-versions.args = [ "old_version", "new_version" ] replace-versions.desc = "replaces an old version number with a new one in all relevant files (Linux only)" @@ -217,7 +210,7 @@ publish.cmd = [ "cd ../perseus", "cargo publish %%", "cd ../perseus-cli", - "cargo publish --allow-dirty %%", # Without this flag, `.perseus` will be a problem because it's not in Git + "cargo publish %%", # We delay this so that `crates.io` can have time to host the core "cd ../perseus-actix-web", "cargo publish %%", diff --git a/examples/comprehensive/tiny/.gitignore b/examples/comprehensive/tiny/.gitignore index 19f4c83304..849ddff3b7 100644 --- a/examples/comprehensive/tiny/.gitignore +++ b/examples/comprehensive/tiny/.gitignore @@ -1,2 +1 @@ - -.perseus/ \ No newline at end of file +dist/ diff --git a/examples/comprehensive/tiny/Cargo.toml b/examples/comprehensive/tiny/Cargo.toml index 3ebc52a5d2..8d6ffa0cda 100644 --- a/examples/comprehensive/tiny/Cargo.toml +++ b/examples/comprehensive/tiny/Cargo.toml @@ -8,3 +8,19 @@ edition = "2021" [dependencies] perseus = { path = "../../../packages/perseus" } sycamore = "=0.8.0-beta.6" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-tiny" +path = "src/lib.rs" diff --git a/examples/comprehensive/tiny/index.html b/examples/comprehensive/tiny/index.html deleted file mode 100644 index 2f972c2e1d..0000000000 --- a/examples/comprehensive/tiny/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Perseus Example – Tiny - - -
- - diff --git a/examples/comprehensive/tiny/src/lib.rs b/examples/comprehensive/tiny/src/lib.rs index 5d57269803..f242810145 100644 --- a/examples/comprehensive/tiny/src/lib.rs +++ b/examples/comprehensive/tiny/src/lib.rs @@ -1,7 +1,7 @@ -use perseus::{Html, PerseusApp, Template, ErrorPages}; +use perseus::{ErrorPages, Html, PerseusApp, Template}; use sycamore::view; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(|| { diff --git a/examples/core/basic/.gitignore b/examples/core/basic/.gitignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/examples/core/basic/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/examples/core/basic/.perseus/.gitignore b/examples/core/basic/.perseus/.gitignore deleted file mode 100644 index 5076b767e0..0000000000 --- a/examples/core/basic/.perseus/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -target/ diff --git a/examples/core/basic/.perseus/Cargo.toml b/examples/core/basic/.perseus/Cargo.toml deleted file mode 100644 index c6bcedfaf9..0000000000 --- a/examples/core/basic/.perseus/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -# This crate defines the user's app in terms that Wasm can understand, making development significantly simpler. -# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! - -[package] -name = "perseus-engine" -version = "0.4.0-beta.1" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# We alias here because the package name will change based on whatever's in the user's manifest -app = { package = "perseus-example-basic", path = "../" } - -perseus = { path = "../../../../packages/perseus" } -sycamore = { version = "=0.8.0-beta.6", features = ["ssr"] } -sycamore-router = "=0.8.0-beta.6" -web-sys = { version = "0.3", features = ["Event", "Headers", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window"] } -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -console_error_panic_hook = "0.1.6" - -# This section is needed for Wasm Pack (which we use instead of Trunk for flexibility) -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -# This changes a few things to support running as a standalone server binary (this is set by `perseus deploy`, do NOT invoke this manually!) -standalone = [] diff --git a/examples/core/basic/.perseus/builder/Cargo.toml b/examples/core/basic/.perseus/builder/Cargo.toml deleted file mode 100644 index d490582e1f..0000000000 --- a/examples/core/basic/.perseus/builder/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -# This crate defines the build process for Perseus -# This used to be part of the root crate in the engine, but it was moved out of there so feature gating could be different across the server, builder, and client -# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! - -[package] -name = "perseus-engine-builder" -version = "0.4.0-beta.1" -edition = "2021" -default-run = "perseus-builder" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -perseus-engine = { path = "../" } -perseus = { path = "../../../../../packages/perseus", features = [ "tinker-plugins", "server-side" ] } -futures = "0.3" -fs_extra = "1" -tokio = { version = "1", features = [ "macros", "rt-multi-thread" ] } -fmterr = "0.1" - -# We define a binary for building, serving, and doing both -[[bin]] -name = "perseus-builder" -path = "src/bin/build.rs" - -[[bin]] -name = "perseus-exporter" -path = "src/bin/export.rs" - -[[bin]] -name = "perseus-tinker" # Yes, the noun is 'tinker', not 'tinkerer' -path = "src/bin/tinker.rs" - -[[bin]] -name = "perseus-error-page-exporter" -path = "src/bin/export_error_page.rs" diff --git a/examples/core/basic/.perseus/builder/src/bin/export_error_page.rs b/examples/core/basic/.perseus/builder/src/bin/export_error_page.rs deleted file mode 100644 index 9e36393ce1..0000000000 --- a/examples/core/basic/.perseus/builder/src/bin/export_error_page.rs +++ /dev/null @@ -1,93 +0,0 @@ -use fmterr::fmt_err; -use perseus::{internal::serve::build_error_page, PerseusApp, PluginAction, SsrNode}; -use perseus_engine as app; -use std::{env, fs}; - -#[tokio::main] -async fn main() { - let exit_code = real_main().await; - std::process::exit(exit_code) -} - -async fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - env::set_current_dir("../").unwrap(); - let app = app::main::(); - - let plugins = app.get_plugins(); - - let error_pages = app.get_error_pages(); - // Prepare the HTML shell - let index_view_str = app.get_index_view_str(); - let root_id = app.get_root(); - let immutable_store = app.get_immutable_store(); - // We assume the app has already been built before running this (so the render config must be available) - // It doesn't matter if the type parameters here are wrong, this function doesn't use them - let html_shell = - PerseusApp::get_html_shell(index_view_str, &root_id, &immutable_store, &plugins).await; - // Get the error code to build from the arguments to this executable - let args = env::args().collect::>(); - let err_code_to_build_for = match args.get(1) { - Some(arg) => match arg.parse::() { - Ok(err_code) => err_code, - Err(_) => { - eprintln!("You must provide a valid number as an HTTP error code."); - return 1; - } - }, - None => { - eprintln!("You must provide an HTTP error code to export an error page for."); - return 1; - } - }; - // Get the output to write to from the second argument - let output = match args.get(2) { - Some(output) => output, - None => { - eprintln!("You must provide an output location for the exported error page."); - return 1; - } - }; - plugins - .functional_actions - .export_error_page_actions - .before_export_error_page - .run( - (err_code_to_build_for, output.to_string()), - plugins.get_plugin_data(), - ); - - // Build that error page as the server does - let err_page_str = build_error_page( - "", - err_code_to_build_for, - "", - None, - &error_pages, - &html_shell, - ); - - // Write that to the mandatory second argument (the output location) - // We'll move out of `.perseus/` first though - env::set_current_dir("../").unwrap(); - match fs::write(&output, err_page_str) { - Ok(_) => (), - Err(err) => { - eprintln!("{}", fmt_err(&err)); - plugins - .functional_actions - .export_error_page_actions - .after_failed_write - .run((err, output.to_string()), plugins.get_plugin_data()); - return 1; - } - }; - - plugins - .functional_actions - .export_error_page_actions - .after_successful_export_error_page - .run((), plugins.get_plugin_data()); - println!("Static exporting successfully completed!"); - 0 -} diff --git a/examples/core/basic/.perseus/builder/src/bin/tinker.rs b/examples/core/basic/.perseus/builder/src/bin/tinker.rs deleted file mode 100644 index ff981e2f85..0000000000 --- a/examples/core/basic/.perseus/builder/src/bin/tinker.rs +++ /dev/null @@ -1,24 +0,0 @@ -use perseus::{plugins::PluginAction, SsrNode}; -use perseus_engine as app; - -fn main() { - let exit_code = real_main(); - std::process::exit(exit_code) -} - -fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - std::env::set_current_dir("../").unwrap(); - - let plugins = app::main::().get_plugins(); - // Run all the tinker actions - // Note: this is deliberately synchronous, tinker actions that need a multithreaded async runtime should probably - // be making their own engines! - plugins - .functional_actions - .tinker - .run((), plugins.get_plugin_data()); - - println!("Tinkering complete!"); - 0 -} diff --git a/examples/core/basic/.perseus/server/Cargo.toml b/examples/core/basic/.perseus/server/Cargo.toml deleted file mode 100644 index cf27923545..0000000000 --- a/examples/core/basic/.perseus/server/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -# This crate defines the user's app in terms that Wasm can understand, making development significantly simpler. -# IMPORTANT: spacing matters in this file for runtime replacements, do NOT change it! - -[package] -name = "perseus-engine-server" -version = "0.4.0-beta.1" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -perseus = { path = "../../../../../packages/perseus", features = [ "server-side" ] } -perseus-actix-web = { path = "../../../../../packages/perseus-actix-web", optional = true } -perseus-warp = { path = "../../../../../packages/perseus-warp", optional = true } -perseus-axum = { path = "../../../../../packages/perseus-axum", optional = true } -perseus-engine = { path = "../" } -actix-web = { version = "=4.0.0-rc.3", optional = true } -actix-http = { version = "=3.0.0-rc.2", optional = true } # Without this, Actix can introduce breaking changes in a dependency tree -# actix-router = { version = "=0.5.0-rc.3", optional = true } -futures = "0.3" -warp = { package = "warp-fix-171", version = "0.3", optional = true } -tokio = { version = "1", optional = true, features = [ "macros", "rt-multi-thread" ] } # We don't need this for Actix Web -axum = { version = "0.5", optional = true } - -# This binary can use any of the server integrations -[features] -integration-actix-web = [ "perseus-actix-web", "actix-web", "actix-http" ] -integration-warp = [ "perseus-warp", "warp", "tokio" ] -integration-axum = [ "perseus-axum", "axum", "tokio" ] - -default = [ "integration-warp" ] - -# This makes the binary work on its own, and is enabled by `perseus deploy` (do NOT invoke this manually!) -standalone = [ "perseus/standalone", "perseus-engine/standalone" ] diff --git a/examples/core/basic/.perseus/server/src/main.rs b/examples/core/basic/.perseus/server/src/main.rs deleted file mode 100644 index 51913506ac..0000000000 --- a/examples/core/basic/.perseus/server/src/main.rs +++ /dev/null @@ -1,171 +0,0 @@ -use futures::executor::block_on; -use perseus::internal::i18n::TranslationsManager; -use perseus::internal::serve::{ServerOptions, ServerProps}; -use perseus::plugins::PluginAction; -use perseus::stores::MutableStore; -use perseus::PerseusApp; -use perseus::SsrNode; -use perseus_engine as app; -use std::env; -use std::fs; - -// This server executable can be run in two modes: -// dev: inside `.perseus/server/src/main.rs`, works with that file structure -// prod: as a standalone executable with a `dist/` directory as a sibling - -// Integration: Actix Web -#[cfg(feature = "integration-actix-web")] -#[actix_web::main] -async fn main() -> std::io::Result<()> { - println!("WARNING: The Actix Web integration uses a beta version of Actix Web, and is considered unstable. It is not recommended for production usage."); - - use actix_web::{App, HttpServer}; - use perseus_actix_web::configurer; - - let is_standalone = get_standalone_and_act(); - let (host, port) = get_host_and_port(); - - HttpServer::new(move || App::new().configure(block_on(configurer(get_props(is_standalone))))) - .bind((host, port))? - .run() - .await -} - -// Integration: Warp -#[cfg(feature = "integration-warp")] -#[tokio::main] -async fn main() { - use perseus_warp::perseus_routes; - use std::net::SocketAddr; - - let is_standalone = get_standalone_and_act(); - let props = get_props(is_standalone); - let (host, port) = get_host_and_port(); - let addr: SocketAddr = format!("{}:{}", host, port) - .parse() - .expect("Invalid address provided to bind to."); - let routes = block_on(perseus_routes(props)); - warp::serve(routes).run(addr).await; -} - -// Integration: Axum -#[cfg(feature = "integration-axum")] -#[tokio::main] -async fn main() { - use perseus_axum::get_router; - use std::net::SocketAddr; - - let is_standalone = get_standalone_and_act(); - let props = get_props(is_standalone); - let (host, port) = get_host_and_port(); - let addr: SocketAddr = format!("{}:{}", host, port) - .parse() - .expect("Invalid address provided to bind to."); - let app = block_on(get_router(props)); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); -} - -/// Determines whether or not we're operating in standalone mode, and acts accordingly. This MUST be executed in the parent thread, as it switches the current directory. -fn get_standalone_and_act() -> bool { - // So we don't have to define a different `FsConfigManager` just for the server, we shift the execution context to the same level as everything else - // The server has to be a separate crate because otherwise the dependencies don't work with Wasm bundling - // If we're not running as a standalone binary, assume we're running in dev mode under `.perseus/` - if !cfg!(feature = "standalone") { - env::set_current_dir("../").unwrap(); - false - } else { - // If we are running as a standalone binary, we have no idea where we're being executed from (#63), so we should set the working directory to be the same as the binary location - let binary_loc = env::current_exe().unwrap(); - let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close to sanity - env::set_current_dir(binary_dir).unwrap(); - true - } -} - -/// Gets the host and port to serve on. -fn get_host_and_port() -> (String, u16) { - // We have to use two sets of environment variables until v0.4.0 - // TODO Remove the old environment variables in v0.4.0 - let host_old = env::var("HOST"); - let port_old = env::var("PORT"); - let host = env::var("PERSEUS_HOST"); - let port = env::var("PERSEUS_PORT"); - - let host = host.unwrap_or_else(|_| host_old.unwrap_or_else(|_| "127.0.0.1".to_string())); - let port = port - .unwrap_or_else(|_| port_old.unwrap_or_else(|_| "8080".to_string())) - .parse::() - .expect("Port must be a number."); - - (host, port) -} - -/// Gets the properties to pass to the server. -fn get_props(is_standalone: bool) -> ServerProps { - let app = app::main::(); - let plugins = app.get_plugins(); - - plugins - .functional_actions - .server_actions - .before_serve - .run((), plugins.get_plugin_data()); - - // This allows us to operate inside `.perseus/` and as a standalone binary in production - let static_dir_path = if is_standalone { - "./static" - } else { - "../static" - }; - - let immutable_store = app.get_immutable_store(); - let locales = app.get_locales(); - let app_root = app.get_root(); - let static_aliases = app.get_static_aliases(); - let templates_map = app.get_atomic_templates_map(); - let error_pages = app.get_error_pages(); - let index_view_str = app.get_index_view_str(); - // Generate the global state - let global_state_creator = app.get_global_state_creator(); - // By the time this binary is being run, the app has already been built be the CLI (hopefully!), so we can depend on access to hte render config - let index_view = block_on(PerseusApp::get_html_shell( - index_view_str, - &app_root, - &immutable_store, - &plugins, - )); - - let opts = ServerOptions { - // We don't support setting some attributes from `wasm-pack` through plugins/`PerseusApp` because that would require CLI changes as well (a job for an alternative engine) - html_shell: index_view, - js_bundle: "dist/pkg/perseus_engine.js".to_string(), - // Our crate has the same name, so this will be predictable - wasm_bundle: "dist/pkg/perseus_engine_bg.wasm".to_string(), - // This probably won't exist, but on the off chance that the user needs to support older browsers, we'll provide it anyway - wasm_js_bundle: "dist/pkg/perseus_engine_bg.wasm.js".to_string(), - templates_map, - locales, - root_id: app_root, - snippets: "dist/pkg/snippets".to_string(), - error_pages, - // The CLI supports static content in `../static` by default if it exists - // This will be available directly at `/.perseus/static` - static_dir: if fs::metadata(&static_dir_path).is_ok() { - Some(static_dir_path.to_string()) - } else { - None - }, - static_aliases, - }; - - ServerProps { - opts, - immutable_store, - mutable_store: app.get_mutable_store(), - translations_manager: block_on(app.get_translations_manager()), - global_state_creator, - } -} diff --git a/examples/core/basic/.perseus/src/lib.rs b/examples/core/basic/.perseus/src/lib.rs deleted file mode 100644 index 1e7e040ff1..0000000000 --- a/examples/core/basic/.perseus/src/lib.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![allow(clippy::unused_unit)] // rustwasm/wasm-bindgen#2774 awaiting next `wasm-bindgen` release - -// The user should use the `main` macro to create this wrapper -pub use app::__perseus_main as main; - -use perseus::{ - checkpoint, create_app_route, - internal::{ - router::{perseus_router, PerseusRouterProps}, - shell::get_render_cfg, - }, - plugins::PluginAction, - templates::TemplateNodeType, -}; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; - -/// The entrypoint into the app itself. This will be compiled to Wasm and actually executed, rendering the rest of the app. -#[wasm_bindgen] -pub fn run() -> Result<(), JsValue> { - let app = main(); - let plugins = app.get_plugins(); - - checkpoint("begin"); - // Panics should always go to the console - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - - plugins - .functional_actions - .client_actions - .start - .run((), plugins.get_plugin_data()); - checkpoint("initial_plugins_complete"); - - // Get the root we'll be injecting the router into - let root = web_sys::window() - .unwrap() - .document() - .unwrap() - .query_selector(&format!("#{}", app.get_root())) - .unwrap() - .unwrap(); - - // Create the route type we'll use for this app, based on the user's app definition - create_app_route! { - name => AppRoute, - // The render configuration is injected verbatim into the HTML shell, so it certainly should be present - render_cfg => &get_render_cfg().expect("render configuration invalid or not injected"), - // TODO avoid unnecessary allocation here (major problem!) - // The `G` parameter is ambient here for `RouteVerdict` - templates => &main::().get_templates_map(), - locales => &main::().get_locales() - } - - // Set up the properties we'll pass to the router - let router_props = PerseusRouterProps { - locales: app.get_locales(), - error_pages: app.get_error_pages(), - }; - - // This top-level context is what we sue for everything, allowing page state to be registered and stored for the lifetime of the app - sycamore::render_to( - move |cx| perseus_router::<_, AppRoute>(cx, router_props), - &root, - ); - - Ok(()) -} diff --git a/examples/core/basic/Cargo.toml b/examples/core/basic/Cargo.toml index 6791130c3f..3b15403bf5 100644 --- a/examples/core/basic/Cargo.toml +++ b/examples/core/basic/Cargo.toml @@ -11,6 +11,22 @@ sycamore = "=0.8.0-beta.6" serde = { version = "1", features = ["derive"] } serde_json = "1" -[dev-dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +# This is an internal convenience crate that exposes all integrations through features for testing +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-basic" +path = "src/lib.rs" diff --git a/examples/core/basic/index.html b/examples/core/basic/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/basic/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/basic/src/lib.rs b/examples/core/basic/src/lib.rs index ee3e6e1a79..a9bac9190f 100644 --- a/examples/core/basic/src/lib.rs +++ b/examples/core/basic/src/lib.rs @@ -3,7 +3,30 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +// pub fn get_app() -> PerseusApp { +// PerseusApp::new() +// .template(crate::templates::index::get_template) +// .template(crate::templates::about::get_template) +// .error_pages(crate::error_pages::get_error_pages) +// } + +// #[perseus::engine_main] +// async fn main() { +// use perseus::builder::{get_op, run_dflt_engine}; + +// let op = get_op().unwrap(); +// let exit_code = run_dflt_engine(op, get_app, perseus_warp::dflt_server).await; +// std::process::exit(exit_code); +// } + +// #[perseus::browser_main] +// pub fn main() -> perseus::ClientReturn { +// use perseus::run_client; + +// run_client(get_app) +// } + +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/basic/src/templates/index.rs b/examples/core/basic/src/templates/index.rs index c019174d72..10082b7f02 100644 --- a/examples/core/basic/src/templates/index.rs +++ b/examples/core/basic/src/templates/index.rs @@ -28,7 +28,7 @@ pub fn head(cx: Scope, _props: IndexPageState) -> View { } } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state( _path: String, _locale: String, diff --git a/examples/core/freezing_and_thawing/.gitignore b/examples/core/freezing_and_thawing/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/freezing_and_thawing/.gitignore +++ b/examples/core/freezing_and_thawing/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/freezing_and_thawing/Cargo.toml b/examples/core/freezing_and_thawing/Cargo.toml index a20ffd8eaf..17af13133f 100644 --- a/examples/core/freezing_and_thawing/Cargo.toml +++ b/examples/core/freezing_and_thawing/Cargo.toml @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-freezing-and-thawing" +path = "src/lib.rs" diff --git a/examples/core/freezing_and_thawing/index.html b/examples/core/freezing_and_thawing/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/freezing_and_thawing/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/freezing_and_thawing/src/global_state.rs b/examples/core/freezing_and_thawing/src/global_state.rs index 9c84a671ff..fbafee3e7d 100644 --- a/examples/core/freezing_and_thawing/src/global_state.rs +++ b/examples/core/freezing_and_thawing/src/global_state.rs @@ -9,7 +9,7 @@ pub struct AppState { pub test: String, } -#[perseus::autoserde(global_build_state)] +#[perseus::global_build_state] pub async fn get_build_state() -> RenderFnResult { Ok(AppState { test: "Hello World!".to_string(), diff --git a/examples/core/freezing_and_thawing/src/lib.rs b/examples/core/freezing_and_thawing/src/lib.rs index 418b487283..04a6fad613 100644 --- a/examples/core/freezing_and_thawing/src/lib.rs +++ b/examples/core/freezing_and_thawing/src/lib.rs @@ -4,7 +4,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/freezing_and_thawing/src/templates/index.rs b/examples/core/freezing_and_thawing/src/templates/index.rs index 612a66e65f..b45b35cba0 100644 --- a/examples/core/freezing_and_thawing/src/templates/index.rs +++ b/examples/core/freezing_and_thawing/src/templates/index.rs @@ -51,7 +51,7 @@ pub fn get_template() -> Template { .template(index_page) } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state( _path: String, _locale: String, diff --git a/examples/core/global_state/.gitignore b/examples/core/global_state/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/global_state/.gitignore +++ b/examples/core/global_state/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/global_state/Cargo.toml b/examples/core/global_state/Cargo.toml index 545991b4d7..089f927069 100644 --- a/examples/core/global_state/Cargo.toml +++ b/examples/core/global_state/Cargo.toml @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-global-state" +path = "src/lib.rs" diff --git a/examples/core/global_state/index.html b/examples/core/global_state/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/global_state/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/global_state/src/global_state.rs b/examples/core/global_state/src/global_state.rs index 9c84a671ff..fbafee3e7d 100644 --- a/examples/core/global_state/src/global_state.rs +++ b/examples/core/global_state/src/global_state.rs @@ -9,7 +9,7 @@ pub struct AppState { pub test: String, } -#[perseus::autoserde(global_build_state)] +#[perseus::global_build_state] pub async fn get_build_state() -> RenderFnResult { Ok(AppState { test: "Hello World!".to_string(), diff --git a/examples/core/global_state/src/lib.rs b/examples/core/global_state/src/lib.rs index 418b487283..04a6fad613 100644 --- a/examples/core/global_state/src/lib.rs +++ b/examples/core/global_state/src/lib.rs @@ -4,7 +4,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/i18n/.gitignore b/examples/core/i18n/.gitignore index 3b1525da45..849ddff3b7 100644 --- a/examples/core/i18n/.gitignore +++ b/examples/core/i18n/.gitignore @@ -1,4 +1 @@ -/target -Cargo.lock - -.perseus/ \ No newline at end of file +dist/ diff --git a/examples/core/i18n/Cargo.toml b/examples/core/i18n/Cargo.toml index 814bde8bce..e1b2ed9c4b 100644 --- a/examples/core/i18n/Cargo.toml +++ b/examples/core/i18n/Cargo.toml @@ -15,4 +15,19 @@ urlencoding = "2.1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-i18n" +path = "src/lib.rs" diff --git a/examples/core/i18n/index.html b/examples/core/i18n/index.html deleted file mode 100644 index cb9e980c16..0000000000 --- a/examples/core/i18n/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Perseus Example – i18n - - -
- - diff --git a/examples/core/i18n/src/lib.rs b/examples/core/i18n/src/lib.rs index 5b47905e11..3c598146f9 100644 --- a/examples/core/i18n/src/lib.rs +++ b/examples/core/i18n/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/i18n/src/templates/post.rs b/examples/core/i18n/src/templates/post.rs index 0bede897f2..df1f4e8ac7 100644 --- a/examples/core/i18n/src/templates/post.rs +++ b/examples/core/i18n/src/templates/post.rs @@ -31,7 +31,7 @@ pub fn get_template() -> Template { .template(post_page) } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_static_props( path: String, _locale: String, @@ -49,6 +49,7 @@ pub async fn get_static_props( }) // This `?` declares the default, that the server is the cause of the error } +#[perseus::build_paths] pub async fn get_static_paths() -> RenderFnResult> { Ok(vec![ "".to_string(), diff --git a/examples/core/idb_freezing/.gitignore b/examples/core/idb_freezing/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/idb_freezing/.gitignore +++ b/examples/core/idb_freezing/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/idb_freezing/Cargo.toml b/examples/core/idb_freezing/Cargo.toml index 3b63bdbd8e..5f9501a1e2 100644 --- a/examples/core/idb_freezing/Cargo.toml +++ b/examples/core/idb_freezing/Cargo.toml @@ -14,4 +14,19 @@ wasm-bindgen-futures = "0.4" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-idb-freezing" +path = "src/lib.rs" diff --git a/examples/core/idb_freezing/index.html b/examples/core/idb_freezing/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/idb_freezing/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/idb_freezing/src/global_state.rs b/examples/core/idb_freezing/src/global_state.rs index 9c84a671ff..fbafee3e7d 100644 --- a/examples/core/idb_freezing/src/global_state.rs +++ b/examples/core/idb_freezing/src/global_state.rs @@ -9,7 +9,7 @@ pub struct AppState { pub test: String, } -#[perseus::autoserde(global_build_state)] +#[perseus::global_build_state] pub async fn get_build_state() -> RenderFnResult { Ok(AppState { test: "Hello World!".to_string(), diff --git a/examples/core/idb_freezing/src/lib.rs b/examples/core/idb_freezing/src/lib.rs index 418b487283..04a6fad613 100644 --- a/examples/core/idb_freezing/src/lib.rs +++ b/examples/core/idb_freezing/src/lib.rs @@ -4,7 +4,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/idb_freezing/src/templates/about.rs b/examples/core/idb_freezing/src/templates/about.rs index 7b1bf064f6..37a11260b1 100644 --- a/examples/core/idb_freezing/src/templates/about.rs +++ b/examples/core/idb_freezing/src/templates/about.rs @@ -1,4 +1,3 @@ -use perseus::state::{Freeze, IdbFrozenStateStore}; use perseus::{Html, Template}; use sycamore::prelude::*; @@ -8,6 +7,9 @@ use crate::global_state::*; pub fn about_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a>) -> View { // This is not part of our data model let freeze_status = create_signal(cx, String::new()); + // It's faster to get this only once and rely on reactivity + // But it's unused when this runs on the server-side because of the target-gate below + #[allow(unused_variables)] let render_ctx = perseus::get_render_ctx!(cx); view! { cx, @@ -18,9 +20,11 @@ pub fn about_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a br() // We'll let the user freeze from here to demonstrate that the frozen state also navigates back to the last route - button(id = "freeze_button", on:click = move |_| + button(id = "freeze_button", on:click = move |_| { // The IndexedDB API is asynchronous, so we'll spawn a future + #[cfg(target_arch = "wasm32")] perseus::spawn_local_scoped(cx, async move { + use perseus::state::{IdbFrozenStateStore, Freeze}; // We do this here (rather than when we get the render context) so that it's updated whenever we press the button let frozen_state = render_ctx.freeze(); let idb_store = match IdbFrozenStateStore::new().await { @@ -35,7 +39,7 @@ pub fn about_page<'a, G: Html>(cx: Scope<'a>, _: (), global_state: AppStateRx<'a Err(_) => freeze_status.set("Error.".to_string()) }; }) - ) { "Freeze to IndexedDB" } + }) { "Freeze to IndexedDB" } p { (freeze_status.get()) } } } diff --git a/examples/core/idb_freezing/src/templates/index.rs b/examples/core/idb_freezing/src/templates/index.rs index 683bf7fb6f..0d0bc49891 100644 --- a/examples/core/idb_freezing/src/templates/index.rs +++ b/examples/core/idb_freezing/src/templates/index.rs @@ -1,4 +1,3 @@ -use perseus::state::{Freeze, IdbFrozenStateStore, PageThawPrefs, ThawPrefs}; use perseus::{Html, RenderFnResultWithCause, Template}; use sycamore::prelude::*; @@ -18,6 +17,9 @@ pub fn index_page<'a, G: Html>( // This is not part of our data model let freeze_status = create_signal(cx, String::new()); let thaw_status = create_signal(cx, String::new()); + // It's faster to get this only once and rely on reactivity + // But it's unused when this runs on the server-side because of the target-gate below + #[allow(unused_variables)] let render_ctx = perseus::get_render_ctx!(cx); view! { cx, @@ -31,9 +33,11 @@ pub fn index_page<'a, G: Html>( a(href = "about", id = "about-link") { "About" } br() - button(id = "freeze_button", on:click = move |_| + button(id = "freeze_button", on:click = move |_| { // The IndexedDB API is asynchronous, so we'll spawn a future + #[cfg(target_arch = "wasm32")] // The freezing types are only available in the browser perseus::spawn_local_scoped(cx, async { + use perseus::state::{IdbFrozenStateStore, Freeze, PageThawPrefs, ThawPrefs}; // We do this here (rather than when we get the render context) so that it's updated whenever we press the button let frozen_state = render_ctx.freeze(); let idb_store = match IdbFrozenStateStore::new().await { @@ -48,12 +52,14 @@ pub fn index_page<'a, G: Html>( Err(_) => freeze_status.set("Error.".to_string()) }; }) - ) { "Freeze to IndexedDB" } + }) { "Freeze to IndexedDB" } p { (freeze_status.get()) } - button(id = "thaw_button", on:click = move |_| + button(id = "thaw_button", on:click = move |_| { // The IndexedDB API is asynchronous, so we'll spawn a future + #[cfg(target_arch = "wasm32")] // The freezing types are only available in the browser perseus::spawn_local_scoped(cx, async move { + use perseus::state::{IdbFrozenStateStore, Freeze, PageThawPrefs, ThawPrefs}; let idb_store = match IdbFrozenStateStore::new().await { Ok(idb_store) => idb_store, Err(_) => { @@ -79,7 +85,7 @@ pub fn index_page<'a, G: Html>( Err(_) => thaw_status.set("Error.".to_string()) } }) - ) { "Thaw from IndexedDB" } + }) { "Thaw from IndexedDB" } p { (thaw_status.get()) } } } @@ -90,7 +96,7 @@ pub fn get_template() -> Template { .template(index_page) } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state( _path: String, _locale: String, diff --git a/examples/core/index_view/.gitignore b/examples/core/index_view/.gitignore index 19f4c83304..849ddff3b7 100644 --- a/examples/core/index_view/.gitignore +++ b/examples/core/index_view/.gitignore @@ -1,2 +1 @@ - -.perseus/ \ No newline at end of file +dist/ diff --git a/examples/core/index_view/Cargo.toml b/examples/core/index_view/Cargo.toml index 948a00e1ac..539cf85285 100644 --- a/examples/core/index_view/Cargo.toml +++ b/examples/core/index_view/Cargo.toml @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-index-view" +path = "src/lib.rs" diff --git a/examples/core/index_view/src/lib.rs b/examples/core/index_view/src/lib.rs index 4e168387b1..2056f58dba 100644 --- a/examples/core/index_view/src/lib.rs +++ b/examples/core/index_view/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp, PerseusRoot}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/plugins/.gitignore b/examples/core/plugins/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/plugins/.gitignore +++ b/examples/core/plugins/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/plugins/Cargo.toml b/examples/core/plugins/Cargo.toml index ec7431a4c2..63b2eb4d1c 100644 --- a/examples/core/plugins/Cargo.toml +++ b/examples/core/plugins/Cargo.toml @@ -14,4 +14,19 @@ toml = "0.5" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-plugins" +path = "src/lib.rs" diff --git a/examples/core/plugins/index.html b/examples/core/plugins/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/plugins/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/plugins/src/lib.rs b/examples/core/plugins/src/lib.rs index 61ca1568ec..a52807f4ca 100644 --- a/examples/core/plugins/src/lib.rs +++ b/examples/core/plugins/src/lib.rs @@ -4,7 +4,7 @@ mod templates; use perseus::{Html, PerseusApp, Plugins}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/plugins/src/plugin.rs b/examples/core/plugins/src/plugin.rs index 0c09dca076..8483148d35 100644 --- a/examples/core/plugins/src/plugin.rs +++ b/examples/core/plugins/src/plugin.rs @@ -24,13 +24,9 @@ pub fn get_test_plugin() -> Plugin { if let Some(plugin_data) = plugin_data.downcast_ref::() { let about_page_greeting = plugin_data.about_page_greeting.to_string(); // Note that this doesn't work with hydration, but a full template does (there's some difference there that causes a hydration ID overlap for some reason) - vec![Template::new("about") - .template(move |cx, _| sycamore::view! { cx, p { (about_page_greeting) } }) - .head(|cx, _| { - sycamore::view! { cx, - title { "About Page (Plugin Modified) | Perseus Example – Plugins" } - } - })] + vec![Template::new("about").template( + move |cx, _| sycamore::view! { cx, p { (about_page_greeting) } }, + )] } else { unreachable!() } diff --git a/examples/core/plugins/tests/main.rs b/examples/core/plugins/tests/main.rs index e2d03f3357..6ce8d02f3a 100644 --- a/examples/core/plugins/tests/main.rs +++ b/examples/core/plugins/tests/main.rs @@ -27,8 +27,6 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> { // Make sure the hardcoded text there exists let text = c.find(Locator::Css("p")).await?.text().await?; assert_eq!(text, "Hey from a plugin!"); - let title = c.find(Locator::Css("title")).await?.html(false).await?; - assert!(title.contains("About Page (Plugin Modified)")); // Make sure we get initial state if we refresh c.refresh().await?; wait_for_checkpoint!("initial_state_present", 0, c); diff --git a/examples/core/router_state/.gitignore b/examples/core/router_state/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/router_state/.gitignore +++ b/examples/core/router_state/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/router_state/Cargo.toml b/examples/core/router_state/Cargo.toml index 4b306884c0..1020cb1ec9 100644 --- a/examples/core/router_state/Cargo.toml +++ b/examples/core/router_state/Cargo.toml @@ -10,3 +10,19 @@ perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] } sycamore = "=0.8.0-beta.6" serde = { version = "1", features = ["derive"] } serde_json = "1" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-router-state" +path = "src/lib.rs" diff --git a/examples/core/router_state/index.html b/examples/core/router_state/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/router_state/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/router_state/src/lib.rs b/examples/core/router_state/src/lib.rs index ee3e6e1a79..172b4388b5 100644 --- a/examples/core/router_state/src/lib.rs +++ b/examples/core/router_state/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/rx_state/.gitignore b/examples/core/rx_state/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/rx_state/.gitignore +++ b/examples/core/rx_state/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/rx_state/Cargo.toml b/examples/core/rx_state/Cargo.toml index f3288f371e..54d4eb6b3c 100644 --- a/examples/core/rx_state/Cargo.toml +++ b/examples/core/rx_state/Cargo.toml @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-rx-state" +path = "src/lib.rs" diff --git a/examples/core/rx_state/index.html b/examples/core/rx_state/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/rx_state/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/rx_state/src/lib.rs b/examples/core/rx_state/src/lib.rs index ee3e6e1a79..172b4388b5 100644 --- a/examples/core/rx_state/src/lib.rs +++ b/examples/core/rx_state/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/rx_state/src/templates/index.rs b/examples/core/rx_state/src/templates/index.rs index 201ec0bc24..26104d3a1c 100644 --- a/examples/core/rx_state/src/templates/index.rs +++ b/examples/core/rx_state/src/templates/index.rs @@ -31,7 +31,7 @@ pub fn get_template() -> Template { .build_state_fn(get_build_state) } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state( _path: String, _locale: String, diff --git a/examples/core/set_headers/.gitignore b/examples/core/set_headers/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/set_headers/.gitignore +++ b/examples/core/set_headers/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/set_headers/Cargo.toml b/examples/core/set_headers/Cargo.toml index 0796c6e763..d485cffef9 100644 --- a/examples/core/set_headers/Cargo.toml +++ b/examples/core/set_headers/Cargo.toml @@ -13,5 +13,20 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } ureq = "2" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-set-headers" +path = "src/lib.rs" diff --git a/examples/core/set_headers/index.html b/examples/core/set_headers/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/set_headers/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/set_headers/src/lib.rs b/examples/core/set_headers/src/lib.rs index 7522e8240a..9a777e008a 100644 --- a/examples/core/set_headers/src/lib.rs +++ b/examples/core/set_headers/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/set_headers/src/templates/index.rs b/examples/core/set_headers/src/templates/index.rs index b435c90ceb..db75748b8a 100644 --- a/examples/core/set_headers/src/templates/index.rs +++ b/examples/core/set_headers/src/templates/index.rs @@ -1,7 +1,4 @@ -use perseus::{ - http::header::{HeaderMap, HeaderName}, - Html, RenderFnResultWithCause, Template, -}; +use perseus::{Html, RenderFnResultWithCause, Template}; use sycamore::prelude::{view, Scope, SsrNode, View}; #[perseus::make_rx(PageStateRx)] @@ -31,7 +28,7 @@ pub fn get_template() -> Template { .set_headers_fn(set_headers) } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause { Ok(PageState { greeting: "Hello World!".to_string(), @@ -40,8 +37,12 @@ pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWi // For legacy reasons, this takes an `Option`, but, if you're generating state, it will always be here // In v0.4.0, this will be updated to take just your page's state (if it has any) -#[perseus::autoserde(set_headers)] -pub fn set_headers(state: Option) -> HeaderMap { +// Unfortunately, this return type does have to be fully qualified, or you have to import it with a server-only target-gate +#[perseus::set_headers] +pub fn set_headers(state: Option) -> perseus::http::header::HeaderMap { + // These imports are only available on the server-side, which this function is automatically gated to + use perseus::http::header::{HeaderMap, HeaderName}; + let mut map = HeaderMap::new(); map.insert( HeaderName::from_lowercase(b"x-greeting").unwrap(), diff --git a/examples/core/state_generation/.gitignore b/examples/core/state_generation/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/state_generation/.gitignore +++ b/examples/core/state_generation/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/state_generation/Cargo.toml b/examples/core/state_generation/Cargo.toml index f32e742431..f1137e0ef5 100644 --- a/examples/core/state_generation/Cargo.toml +++ b/examples/core/state_generation/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "perseus-example-base" +name = "perseus-example-state-generation" version = "0.3.2" edition = "2021" @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-state-generation" +path = "src/lib.rs" diff --git a/examples/core/state_generation/index.html b/examples/core/state_generation/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/state_generation/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/state_generation/src/lib.rs b/examples/core/state_generation/src/lib.rs index 31622c0ca3..774fbe4b93 100644 --- a/examples/core/state_generation/src/lib.rs +++ b/examples/core/state_generation/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::build_state::get_template) diff --git a/examples/core/state_generation/src/templates/amalgamation.rs b/examples/core/state_generation/src/templates/amalgamation.rs index 939394dedd..b8f23b0c83 100644 --- a/examples/core/state_generation/src/templates/amalgamation.rs +++ b/examples/core/state_generation/src/templates/amalgamation.rs @@ -1,4 +1,6 @@ -use perseus::{RenderFnResultWithCause, Request, States, Template}; +use perseus::{RenderFnResultWithCause, Template}; +#[cfg(not(target_arch = "wasm32"))] +use perseus::{Request, States}; use sycamore::prelude::{view, Html, Scope, View}; #[perseus::make_rx(PageStateRx)] @@ -23,7 +25,7 @@ pub fn get_template() -> Template { .template(amalgamation_page) } -#[perseus::autoserde(amalgamate_states)] +#[perseus::amalgamate_states] pub fn amalgamate_states(states: States) -> RenderFnResultWithCause> { // We know they'll both be defined, and Perseus currently has to provide both as serialized strings let build_state = serde_json::from_str::(&states.build_state.unwrap())?; @@ -37,14 +39,14 @@ pub fn amalgamate_states(states: States) -> RenderFnResultWithCause RenderFnResultWithCause { Ok(PageState { message: "Hello from the build process!".to_string(), }) } -#[perseus::autoserde(request_state)] +#[perseus::request_state] pub async fn get_request_state( _path: String, _locale: String, diff --git a/examples/core/state_generation/src/templates/build_paths.rs b/examples/core/state_generation/src/templates/build_paths.rs index 137e1e4e92..5a959f7aa4 100644 --- a/examples/core/state_generation/src/templates/build_paths.rs +++ b/examples/core/state_generation/src/templates/build_paths.rs @@ -29,7 +29,7 @@ pub fn get_template() -> Template { } // We'll take in the path here, which will consist of the template name `build_paths` followed by the spcific path we're building for (as exported from `get_build_paths`) -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWithCause { let title = path.clone(); let content = format!( @@ -45,6 +45,7 @@ pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWit // Note that everything you export from here will be prefixed with `/` when it becomes a URL in your app // // Note also that there's almost no point in using build paths without build state, as every page would come out exactly the same (unless you differentiated them on the client...) +#[perseus::build_paths] pub async fn get_build_paths() -> RenderFnResult> { Ok(vec![ "".to_string(), diff --git a/examples/core/state_generation/src/templates/build_state.rs b/examples/core/state_generation/src/templates/build_state.rs index b8a6171f63..ae6b70ee68 100644 --- a/examples/core/state_generation/src/templates/build_state.rs +++ b/examples/core/state_generation/src/templates/build_state.rs @@ -21,7 +21,7 @@ pub fn get_template() -> Template { // We're told the path we're generating for (useless unless we're using build paths as well) and the locale (which will be `xx-XX` if we're not using i18n) // Note that this function is asynchronous, so we can do work like fetching from a server or the like here (see the `demo/fetching` example) -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause { Ok(PageState { greeting: "Hello World!".to_string(), diff --git a/examples/core/state_generation/src/templates/incremental_generation.rs b/examples/core/state_generation/src/templates/incremental_generation.rs index 03510d222c..7179732ced 100644 --- a/examples/core/state_generation/src/templates/incremental_generation.rs +++ b/examples/core/state_generation/src/templates/incremental_generation.rs @@ -34,7 +34,7 @@ pub fn get_template() -> Template { } // We'll take in the path here, which will consist of the template name `incremental_generation` followed by the spcific path we're building for (as exported from `get_build_paths`) -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWithCause { // This path is illegal, and can't be rendered // Because we're using incremental generation, we could gte literally anything as the `path` @@ -57,6 +57,7 @@ pub async fn get_build_state(path: String, _locale: String) -> RenderFnResultWit // Note that everything you export from here will be prefixed with `/` when it becomes a URL in your app // // Note also that there's almost no point in using build paths without build state, as every page would come out exactly the same (unless you differentiated them on the client...) +#[perseus::build_paths] pub async fn get_build_paths() -> RenderFnResult> { Ok(vec!["test".to_string(), "blah/test/blah".to_string()]) } diff --git a/examples/core/state_generation/src/templates/request_state.rs b/examples/core/state_generation/src/templates/request_state.rs index 3a5301bc8d..2bbd6bc35f 100644 --- a/examples/core/state_generation/src/templates/request_state.rs +++ b/examples/core/state_generation/src/templates/request_state.rs @@ -1,4 +1,6 @@ -use perseus::{RenderFnResultWithCause, Request, Template}; +#[cfg(not(target_arch = "wasm32"))] +use perseus::Request; +use perseus::{RenderFnResultWithCause, Template}; use sycamore::prelude::{view, Html, Scope, View}; #[perseus::make_rx(PageStateRx)] @@ -23,7 +25,7 @@ pub fn get_template() -> Template { .template(request_state_page) } -#[perseus::autoserde(request_state)] +#[perseus::request_state] pub async fn get_request_state( _path: String, _locale: String, diff --git a/examples/core/state_generation/src/templates/revalidation.rs b/examples/core/state_generation/src/templates/revalidation.rs index c6dc42c67c..a2a6042624 100644 --- a/examples/core/state_generation/src/templates/revalidation.rs +++ b/examples/core/state_generation/src/templates/revalidation.rs @@ -27,7 +27,7 @@ pub fn get_template() -> Template { } // This will get the system time when the app was built -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause { Ok(PageState { time: format!("{:?}", std::time::SystemTime::now()), @@ -36,6 +36,7 @@ pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWi // This will run every time `.revalidate_after()` permits the page to be revalidated // This acts as a secondary check, and can perform arbitrary logic to check if we should actually revalidate a page +#[perseus::should_revalidate] pub async fn should_revalidate() -> RenderFnResultWithCause { // For simplicity's sake, this will always say we should revalidate, but you could amke this check any condition Ok(true) diff --git a/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs b/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs index 3345a241b6..b8223cd9e8 100644 --- a/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs +++ b/examples/core/state_generation/src/templates/revalidation_and_incremental_generation.rs @@ -28,20 +28,29 @@ pub fn get_template() -> Template { // load this page. For that reason, this should NOT do long-running work, as requests will be delayed. If both this // and `revaldiate_after()` are provided, this logic will only run when `revalidate_after()` tells Perseus // that it should revalidate. - .should_revalidate_fn(|| async { Ok(true) }) + .should_revalidate_fn(should_revalidate) .build_state_fn(get_build_state) .build_paths_fn(get_build_paths) .incremental_generation() } // This will get the system time when the app was built -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state(_path: String, _locale: String) -> RenderFnResultWithCause { Ok(PageState { time: format!("{:?}", std::time::SystemTime::now()), }) } +#[perseus::build_paths] pub async fn get_build_paths() -> RenderFnResult> { Ok(vec!["test".to_string(), "blah/test/blah".to_string()]) } + +// This will run every time `.revalidate_after()` permits the page to be revalidated +// This acts as a secondary check, and can perform arbitrary logic to check if we should actually revalidate a page +#[perseus::should_revalidate] +pub async fn should_revalidate() -> RenderFnResultWithCause { + // For simplicity's sake, this will always say we should revalidate, but you could amke this check any condition + Ok(true) +} diff --git a/examples/core/static_content/.gitignore b/examples/core/static_content/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/static_content/.gitignore +++ b/examples/core/static_content/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/static_content/Cargo.toml b/examples/core/static_content/Cargo.toml index b74dd3e815..29ff46a48d 100644 --- a/examples/core/static_content/Cargo.toml +++ b/examples/core/static_content/Cargo.toml @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-static-content" +path = "src/lib.rs" diff --git a/examples/core/static_content/index.html b/examples/core/static_content/index.html deleted file mode 100644 index e70301b489..0000000000 --- a/examples/core/static_content/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - -
- - diff --git a/examples/core/static_content/src/lib.rs b/examples/core/static_content/src/lib.rs index 301b1ca4d3..0b68f29874 100644 --- a/examples/core/static_content/src/lib.rs +++ b/examples/core/static_content/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/unreactive/.gitignore b/examples/core/unreactive/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/core/unreactive/.gitignore +++ b/examples/core/unreactive/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/core/unreactive/Cargo.toml b/examples/core/unreactive/Cargo.toml index b264b51e6d..9528ce1c7d 100644 --- a/examples/core/unreactive/Cargo.toml +++ b/examples/core/unreactive/Cargo.toml @@ -13,4 +13,19 @@ serde_json = "1" [dev-dependencies] fantoccini = "0.17" -tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-unreactive" +path = "src/lib.rs" diff --git a/examples/core/unreactive/index.html b/examples/core/unreactive/index.html deleted file mode 100644 index edc8a66246..0000000000 --- a/examples/core/unreactive/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
- - diff --git a/examples/core/unreactive/src/lib.rs b/examples/core/unreactive/src/lib.rs index ee3e6e1a79..172b4388b5 100644 --- a/examples/core/unreactive/src/lib.rs +++ b/examples/core/unreactive/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/core/unreactive/src/templates/index.rs b/examples/core/unreactive/src/templates/index.rs index a2516d1de9..f129567d95 100644 --- a/examples/core/unreactive/src/templates/index.rs +++ b/examples/core/unreactive/src/templates/index.rs @@ -34,7 +34,7 @@ pub fn head(cx: Scope, _props: IndexPageState) -> View { } } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state( _path: String, _locale: String, diff --git a/examples/demos/auth/.gitignore b/examples/demos/auth/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/demos/auth/.gitignore +++ b/examples/demos/auth/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/demos/auth/Cargo.toml b/examples/demos/auth/Cargo.toml index 2a5856e49f..92fd6d3d16 100644 --- a/examples/demos/auth/Cargo.toml +++ b/examples/demos/auth/Cargo.toml @@ -11,5 +11,21 @@ perseus = { path = "../../../packages/perseus", features = [] } sycamore = "=0.8.0-beta.6" serde = { version = "1", features = ["derive"] } serde_json = "1" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" # We need the `HtmlDocument` feature to be able to use cookies (which this example does) web-sys = { version = "0.3", features = [ "Storage" ] } + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-auth" +path = "src/lib.rs" diff --git a/examples/demos/auth/src/global_state.rs b/examples/demos/auth/src/global_state.rs index b0a6e55934..d5bcf92a3d 100644 --- a/examples/demos/auth/src/global_state.rs +++ b/examples/demos/auth/src/global_state.rs @@ -5,7 +5,7 @@ pub fn get_global_state_creator() -> GlobalStateCreator { GlobalStateCreator::new().build_state_fn(get_build_state) } -#[perseus::autoserde(global_build_state)] +#[perseus::global_build_state] pub async fn get_build_state() -> RenderFnResult { Ok(AppState { // We explicitly tell the first page that no login state has been checked yet @@ -44,9 +44,9 @@ pub struct AuthData { } // We implement a custom function on the reactive version of the global state here (hence the `.get()`s and `.set()`s, all the fields become `Signal`s) // There's no point in implementing it on the unreactive version, since this will only be called from within the browser, in which we have a reactive version +#[cfg(target_arch = "wasm32")] // These functions all use `web_sys`, and so won't work on the server-side impl<'a> AuthDataRx<'a> { /// Checks whether or not the user is logged in and modifies the internal state accordingly. If this has already been run, it won't do anything (aka. it will only run if it's `Server`) - #[cfg(target_arch = "wasm32")] // This just avoids an unused function warning (since we have to gate the `.update()` call) pub fn detect_state(&self) { // If we've checked the login status before, then we should assume the status hasn't changed (we'd change this in a login/logout page) if let LoginState::Yes | LoginState::No = *self.state.get() { diff --git a/examples/demos/auth/src/lib.rs b/examples/demos/auth/src/lib.rs index 418b487283..04a6fad613 100644 --- a/examples/demos/auth/src/lib.rs +++ b/examples/demos/auth/src/lib.rs @@ -4,7 +4,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/demos/auth/src/templates/index.rs b/examples/demos/auth/src/templates/index.rs index 893b06e55b..502e08dbb9 100644 --- a/examples/demos/auth/src/templates/index.rs +++ b/examples/demos/auth/src/templates/index.rs @@ -21,6 +21,7 @@ fn index_view<'a, G: Html>(cx: Scope<'a>, _: (), AppStateRx { auth }: AppStateRx view! { cx, h1 { (format!("Welcome back, {}!", &username)) } button(on:click = |_| { + #[cfg(target_arch = "wasm32")] auth.logout(); }) { "Logout" } } @@ -30,6 +31,7 @@ fn index_view<'a, G: Html>(cx: Scope<'a>, _: (), AppStateRx { auth }: AppStateRx h1 { "Welcome, stranger!" } input(bind:value = entered_username, placeholder = "Username") button(on:click = |_| { + #[cfg(target_arch = "wasm32")] auth.login(&entered_username.get()) }) { "Login" } }, diff --git a/examples/demos/fetching/.gitignore b/examples/demos/fetching/.gitignore index 9405098b45..849ddff3b7 100644 --- a/examples/demos/fetching/.gitignore +++ b/examples/demos/fetching/.gitignore @@ -1 +1 @@ -.perseus/ +dist/ diff --git a/examples/demos/fetching/Cargo.toml b/examples/demos/fetching/Cargo.toml index d95d720379..65a69fe8dc 100644 --- a/examples/demos/fetching/Cargo.toml +++ b/examples/demos/fetching/Cargo.toml @@ -10,8 +10,21 @@ perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] } sycamore = "=0.8.0-beta.6" serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwasm = "0.4" -# This makes sure `reqwest` is only included on the server-side (it won't compile at all for the browser) [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-integration = { path = "../../../packages/perseus-integration", default-features = false } reqwest = "0.11" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +reqwasm = "0.4" + +[lib] +name = "lib" +path = "src/lib.rs" +crate-type = [ "cdylib", "rlib" ] + +[[bin]] +name = "perseus-example-fetching" +path = "src/lib.rs" diff --git a/examples/demos/fetching/src/lib.rs b/examples/demos/fetching/src/lib.rs index 7522e8240a..9a777e008a 100644 --- a/examples/demos/fetching/src/lib.rs +++ b/examples/demos/fetching/src/lib.rs @@ -3,7 +3,7 @@ mod templates; use perseus::{Html, PerseusApp}; -#[perseus::main] +#[perseus::main(perseus_integration::dflt_server)] pub fn main() -> PerseusApp { PerseusApp::new() .template(crate::templates::index::get_template) diff --git a/examples/demos/fetching/src/templates/index.rs b/examples/demos/fetching/src/templates/index.rs index b36653acb4..c0da60d6f2 100644 --- a/examples/demos/fetching/src/templates/index.rs +++ b/examples/demos/fetching/src/templates/index.rs @@ -18,6 +18,8 @@ pub fn index_page<'a, G: Html>( // This will only run in the browser // `reqwasm` wraps browser-specific APIs, so we don't want it running on the server // If the browser IP has already been fetched (e.g. if we've come here for the second time in the same session), we won't bother re-fetching + #[cfg(target_arch = "wasm32")] + // Because we only have `reqwasm` on the client-side, we make sure this is only *compiled* in the browser as well if G::IS_BROWSER && browser_ip.get().is_none() { // Spawn a `Future` on this thread to fetch the data (`spawn_local` is re-exported from `wasm-bindgen-futures`) // Don't worry, this doesn't need to be sent to JavaScript for execution @@ -56,14 +58,12 @@ pub fn get_template() -> Template { .template(index_page) } -#[perseus::autoserde(build_state)] +#[perseus::build_state] pub async fn get_build_state( _path: String, _locale: String, ) -> RenderFnResultWithCause { // We'll cache the result with `try_cache_res`, which means we only make the request once, and future builds will use the cached result (speeds up development) - // Currently, target gating isn't fully sorted out in the latest version, so, because `reqwest` is only available on the server-side, we have to note that (in future, this won't be necessary) - #[cfg(not(target_arch = "wasm32"))] let body = perseus::cache_fallible_res( "ipify", || async { @@ -74,9 +74,6 @@ pub async fn get_build_state( false, ) .await?; - // To be clear, this will never ever run, we just need it in the current version to appease the compiler (soon, this will be totally unnecessary) - #[cfg(target_arch = "wasm32")] - let body = "".to_string(); Ok(IndexPageState { server_ip: body, diff --git a/packages/perseus-actix-web/Cargo.toml b/packages/perseus-actix-web/Cargo.toml index ff3020e8c9..fc2ab9deb3 100644 --- a/packages/perseus-actix-web/Cargo.toml +++ b/packages/perseus-actix-web/Cargo.toml @@ -26,3 +26,7 @@ thiserror = "1" fmterr = "0.1" futures = "0.3" sycamore = { version = "=0.8.0-beta.6", features = ["ssr"] } + +[features] +# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes +dflt-server = [ "perseus/builder" ] diff --git a/packages/perseus-actix-web/src/dflt_server.rs b/packages/perseus-actix-web/src/dflt_server.rs new file mode 100644 index 0000000000..48957c1e03 --- /dev/null +++ b/packages/perseus-actix-web/src/dflt_server.rs @@ -0,0 +1,36 @@ +use crate::configurer; +use actix_web::{App, HttpServer}; +use futures::executor::block_on; +use perseus::{ + builder::{get_host_and_port, get_props, get_standalone_and_act}, + internal::i18n::TranslationsManager, + stores::MutableStore, + PerseusAppBase, SsrNode, +}; + +/// Creates and starts the default Perseus server using Actix Web. This should be run in a `main()` function annotated with `#[tokio::main]` (which requires the `macros` and +/// `rt-multi-thread` features on the `tokio` dependency). +pub async fn dflt_server( + app: impl Fn() -> PerseusAppBase + 'static + Send + Sync + Clone, +) { + get_standalone_and_act(); + let (host, port) = get_host_and_port(); + + HttpServer::new(move || + App::new() + .configure( + block_on( + configurer( + get_props( + app() + ) + ) + ) + ) + ) + .bind((host, port)) + .expect("Couldn't bind to given address. Maybe something is already running on the selected port?") + .run() + .await + .expect("Server failed.") // TODO Improve error message here +} diff --git a/packages/perseus-actix-web/src/lib.rs b/packages/perseus-actix-web/src/lib.rs index e68f19fe89..fe70fa6211 100644 --- a/packages/perseus-actix-web/src/lib.rs +++ b/packages/perseus-actix-web/src/lib.rs @@ -10,10 +10,14 @@ documentation, and this should mostly be used as a secondary reference source. Y mod configurer; mod conv_req; +#[cfg(feature = "dflt-server")] +mod dflt_server; pub mod errors; mod initial_load; mod page_data; mod translations; pub use crate::configurer::configurer; +#[cfg(feature = "dflt-server")] +pub use dflt_server::dflt_server; pub use perseus::internal::serve::ServerOptions; diff --git a/packages/perseus-axum/Cargo.toml b/packages/perseus-axum/Cargo.toml index 16e8bfc8e0..1c21ee9592 100644 --- a/packages/perseus-axum/Cargo.toml +++ b/packages/perseus-axum/Cargo.toml @@ -26,3 +26,7 @@ fmterr = "0.1" futures = "0.3" sycamore = { version = "=0.8.0-beta.6", features = ["ssr"] } closure = "0.3" + +[features] +# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes +dflt-server = [ "perseus/builder" ] diff --git a/packages/perseus-axum/src/dflt_server.rs b/packages/perseus-axum/src/dflt_server.rs new file mode 100644 index 0000000000..84eb42ecac --- /dev/null +++ b/packages/perseus-axum/src/dflt_server.rs @@ -0,0 +1,27 @@ +use crate::get_router; +use futures::executor::block_on; +use perseus::{ + builder::{get_host_and_port, get_props, get_standalone_and_act}, + internal::i18n::TranslationsManager, + stores::MutableStore, + PerseusAppBase, SsrNode, +}; +use std::net::SocketAddr; + +/// Creates and starts the default Perseus server with Axum. This should be run in a `main` function annotated with `#[tokio::main]` (which requires the `macros` and +/// `rt-multi-thread` features on the `tokio` dependency). +pub async fn dflt_server( + app: impl Fn() -> PerseusAppBase + 'static + Send + Sync + Clone, +) { + get_standalone_and_act(); + let props = get_props(app()); + let (host, port) = get_host_and_port(); + let addr: SocketAddr = format!("{}:{}", host, port) + .parse() + .expect("Invalid address provided to bind to."); + let app = block_on(get_router(props)); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/packages/perseus-axum/src/lib.rs b/packages/perseus-axum/src/lib.rs index 3b07eba330..3133791dd3 100644 --- a/packages/perseus-axum/src/lib.rs +++ b/packages/perseus-axum/src/lib.rs @@ -9,10 +9,14 @@ documentation, and this should mostly be used as a secondary reference source. Y #![deny(missing_docs)] // This integration doesn't need to convert request types, because we can get them straight out of Axum and then just delete the bodies +#[cfg(feature = "dflt-server")] +mod dflt_server; mod initial_load; mod page_data; mod router; mod translations; pub use crate::router::get_router; +#[cfg(feature = "dflt-server")] +pub use dflt_server::dflt_server; pub use perseus::internal::serve::ServerOptions; diff --git a/packages/perseus-cli/Cargo.toml b/packages/perseus-cli/Cargo.toml index 2ff9369071..6e261e19b0 100644 --- a/packages/perseus-cli/Cargo.toml +++ b/packages/perseus-cli/Cargo.toml @@ -39,9 +39,6 @@ futures = "0.3" tokio-stream = "0.1" ureq = "2" -[build-dependencies] -fs_extra = "1" - [lib] name = "perseus_cli" diff --git a/packages/perseus-cli/build.rs b/packages/perseus-cli/build.rs deleted file mode 100644 index a8f16b5a76..0000000000 --- a/packages/perseus-cli/build.rs +++ /dev/null @@ -1,105 +0,0 @@ -// This build script copies the `examples/core/basic/.perseus/` directory into `packages/perseus-cli/` for use in compilation -// Having this as a build script rather than an external script allows the CLI to be installed with `cargo install` from any commit hash - -use std::fs; -use std::path::PathBuf; - -// All this is run relative to the `packages/perseus-cli/` directory -fn main() { - // Tell Cargo that this needs to be re-run if the direcotry storing the engine code has changed - println!("cargo:rerun-if-changed=../../examples/core/basic/.perseus"); - - let dest = PathBuf::from("."); - let engine_dir = PathBuf::from("../../examples/core/basic/.perseus"); - - // Replace the current `.perseus/` directory here with the latest version - let _ = fs::remove_dir_all(dest.join(".perseus")); // It's fine if this doesn't exist - fs_extra::dir::copy(engine_dir, &dest, &fs_extra::dir::CopyOptions::new()).unwrap(); - // Rename the manifests for appropriate usage - fs::rename( - dest.join(".perseus/Cargo.toml"), - dest.join(".perseus/Cargo.toml.old"), - ) - .unwrap(); - fs::rename( - dest.join(".perseus/server/Cargo.toml"), - dest.join(".perseus/server/Cargo.toml.old"), - ) - .unwrap(); - fs::rename( - dest.join(".perseus/builder/Cargo.toml"), - dest.join(".perseus/builder/Cargo.toml.old"), - ) - .unwrap(); - // Remove distribution artifacts so they don't clog up the final bundle - fs::remove_dir_all(dest.join(".perseus/dist")).unwrap(); - // But we need to create the basic directory structure for outputs - fs::create_dir(dest.join(".perseus/dist")).unwrap(); - fs::create_dir(dest.join(".perseus/dist/static")).unwrap(); - fs::create_dir(dest.join(".perseus/dist/exported")).unwrap(); - // Replace the example's package name with a token the CLI can use (compatible with alternative engines as well) - // We only need to do this in the root package, the others depend on it - // While we're at it, we'll update the dependencies to be tokens that can be replaced by the CLI (removing relative path references) - let updated_root_manifest = fs::read_to_string(dest.join(".perseus/Cargo.toml.old")) - .unwrap() - .replace("perseus-example-basic", "USER_PKG_NAME") - .replace("path = \"../../../../packages/perseus\"", "PERSEUS_VERSION"); - fs::write(dest.join(".perseus/Cargo.toml.old"), updated_root_manifest).unwrap(); - let updated_builder_manifest = fs::read_to_string(dest.join(".perseus/builder/Cargo.toml.old")) - .unwrap() - .replace( - "path = \"../../../../../packages/perseus\"", - "PERSEUS_VERSION", - ); - fs::write( - dest.join(".perseus/builder/Cargo.toml.old"), - updated_builder_manifest, - ) - .unwrap(); - let updated_server_manifest = fs::read_to_string(dest.join(".perseus/server/Cargo.toml.old")) - .unwrap() - .replace( - "path = \"../../../../../packages/perseus\"", - "PERSEUS_VERSION", - ) - .replace( - "path = \"../../../../../packages/perseus-actix-web\"", - "PERSEUS_ACTIX_WEB_VERSION", - ) - .replace( - "path = \"../../../../../packages/perseus-warp\"", - "PERSEUS_WARP_VERSION", - ); - fs::write( - dest.join(".perseus/server/Cargo.toml.old"), - updated_server_manifest, - ) - .unwrap(); -} - -/* -[ - # The CLI needs the `.perseus/` directory copied in for packaging (and we need to rename `Cargo.toml` to `Cargo.toml.old`) - "cd packages/perseus-cli", - "rm -rf ./.perseus", - "cp -r ../../examples/core/basic/.perseus/ .perseus/", - "mv .perseus/Cargo.toml .perseus/Cargo.toml.old", - "mv .perseus/server/Cargo.toml .perseus/server/Cargo.toml.old", - "mv .perseus/builder/Cargo.toml .perseus/builder/Cargo.toml.old", - # Remove distribution artifacts (they clog up the final bundle) - "rm -rf .perseus/dist", - "mkdir -p .perseus/dist", - "mkdir -p .perseus/dist/static", - "mkdir -p .perseus/dist/exported", - # Replace the example's package name with a token the CLI can use (compatible with alternative engines as well) - # We only need to do this in the root package, the others depend on it - "sed -i 's/perseus-example-basic/USER_PKG_NAME/' .perseus/Cargo.toml.old", - # Replace the relative path references with tokens too - "sed -i 's/path = \"\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/packages\\/perseus\"/PERSEUS_VERSION/' .perseus/Cargo.toml.old", - "sed -i 's/path = \"\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/packages\\/perseus\"/PERSEUS_VERSION/' .perseus/builder/Cargo.toml.old", - # These will need to be updated as more integrations are added - "sed -i 's/path = \"\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/packages\\/perseus\"/PERSEUS_VERSION/' .perseus/server/Cargo.toml.old", - "sed -i 's/path = \"\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/packages\\/perseus-actix-web\"/PERSEUS_ACTIX_WEB_VERSION/' .perseus/server/Cargo.toml.old", - "sed -i 's/path = \"\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/\\.\\.\\/packages\\/perseus-warp\"/PERSEUS_WARP_VERSION/' .perseus/server/Cargo.toml.old" -] -*/ diff --git a/packages/perseus-cli/src/bin/main.rs b/packages/perseus-cli/src/bin/main.rs index a0683a73f2..8868f3867e 100644 --- a/packages/perseus-cli/src/bin/main.rs +++ b/packages/perseus-cli/src/bin/main.rs @@ -4,13 +4,13 @@ use fmterr::fmt_err; use notify::{recommended_watcher, RecursiveMode, Watcher}; use perseus_cli::parse::{ExportOpts, ServeOpts, SnoopSubcommand}; use perseus_cli::{ - build, check_env, delete_artifacts, delete_bad_dir, deploy, eject, export, has_ejected, + build, check_env, delete_artifacts, deploy, export, parse::{Opts, Subcommand}, - prepare, serve, serve_exported, tinker, + serve, serve_exported, tinker, }; use perseus_cli::{ - errors::*, export_error_page, order_reload, run_reload_server, snoop_build, snoop_server, - snoop_wasm_build, + delete_dist, errors::*, export_error_page, order_reload, run_reload_server, snoop_build, + snoop_server, snoop_wasm_build, }; use std::env; use std::io::Write; @@ -37,10 +37,7 @@ async fn real_main() -> i32 { let dir = match dir { Ok(dir) => dir, Err(err) => { - eprintln!( - "{}", - fmt_err(&PrepError::CurrentDirUnavailable { source: err }) - ); + eprintln!("{}", fmt_err(&Error::CurrentDirUnavailable { source: err })); return 1; } }; @@ -50,14 +47,7 @@ async fn real_main() -> i32 { Ok(exit_code) => exit_code, // If something failed, we print the error to `stderr` and return a failure exit code Err(err) => { - let should_cause_deletion = err_should_cause_deletion(&err); eprintln!("{}", fmt_err(&err)); - // Check if the error needs us to delete a partially-formed '.perseus/' directory - if should_cause_deletion { - if let Err(err) = delete_bad_dir(dir) { - eprintln!("{}", fmt_err(&err)); - } - } 1 } } @@ -210,10 +200,6 @@ async fn core(dir: PathBuf) -> Result { } async fn core_watch(dir: PathBuf, opts: Opts) -> Result { - // If we're not cleaning up artifacts, create them if needed - if !matches!(opts.subcmd, Subcommand::Clean(_)) { - prepare(dir.clone(), &opts.engine)?; - } let exit_code = match opts.subcmd { Subcommand::Build(build_opts) => { // Delete old build artifacts @@ -253,21 +239,8 @@ async fn core_watch(dir: PathBuf, opts: Opts) -> Result { let (exit_code, _server_path) = serve(dir, test_opts)?; exit_code } - Subcommand::Clean(clean_opts) => { - if clean_opts.dist { - // The user only wants to remove distribution artifacts - // We don't delete `render_conf.json` because it's literally impossible for that to be the source of a problem right now - delete_artifacts(dir.clone(), "static")?; - delete_artifacts(dir.clone(), "pkg")?; - delete_artifacts(dir, "exported")?; - } else { - // This command deletes the `.perseus/` directory completely, which musn't happen if the user has ejected - if has_ejected(dir.clone()) && !clean_opts.force { - return Err(EjectionError::CleanAfterEject.into()); - } - // Just delete the '.perseus/' directory directly, as we'd do in a corruption - delete_bad_dir(dir)?; - } + Subcommand::Clean => { + delete_dist(dir)?; 0 } Subcommand::Deploy(deploy_opts) => { @@ -276,21 +249,11 @@ async fn core_watch(dir: PathBuf, opts: Opts) -> Result { delete_artifacts(dir.clone(), "pkg")?; deploy(dir, deploy_opts)? } - Subcommand::Eject => { - eject(dir)?; - 0 - } Subcommand::Tinker(tinker_opts) => { - // We shouldn't run arbitrary plugin code designed to alter the engine if the user has made their own changes after ejecting - if has_ejected(dir.clone()) && !tinker_opts.force { - return Err(EjectionError::TinkerAfterEject.into()); - } // Unless we've been told not to, we start with a blank slate // This will remove old tinkerings and eliminate any possible corruptions (which are very likely with tinkering!) if !tinker_opts.no_clean { - delete_bad_dir(dir.clone())?; - // Recreate the '.perseus/' directory - prepare(dir.clone(), &opts.engine)?; + delete_dist(dir.clone())?; } tinker(dir)? } @@ -300,10 +263,6 @@ async fn core_watch(dir: PathBuf, opts: Opts) -> Result { SnoopSubcommand::Serve(snoop_serve_opts) => snoop_server(dir, snoop_serve_opts)?, }, Subcommand::ExportErrorPage(opts) => export_error_page(dir, opts)?, - Subcommand::Prep => { - // The `.perseus/` directory has already been set up in the preliminaries, so we don't need to do anything here - 0 - } }; Ok(exit_code) } diff --git a/packages/perseus-cli/src/build.rs b/packages/perseus-cli/src/build.rs index a8db51fcb8..916479cab9 100644 --- a/packages/perseus-cli/src/build.rs +++ b/packages/perseus-cli/src/build.rs @@ -5,8 +5,7 @@ use crate::thread::{spawn_thread, ThreadHandle}; use console::{style, Emoji}; use indicatif::{MultiProgress, ProgressBar}; use std::env; -use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; // Emojis for stages static GENERATING: Emoji<'_, '_> = Emoji("🔨", ""); @@ -22,22 +21,22 @@ macro_rules! handle_exit_code { }; } -/// Finalizes the build by renaming some directories. -pub fn finalize(target: &Path) -> Result<(), ExecutionError> { - // Move the `pkg/` directory into `dist/pkg/` - let pkg_dir = target.join("dist/pkg"); - if pkg_dir.exists() { - if let Err(err) = fs::remove_dir_all(&pkg_dir) { - return Err(ExecutionError::MovePkgDirFailed { source: err }); - } - } - // The `fs::rename()` function will fail on Windows if the destination already exists, so this should work (we've just deleted it as per https://github.com/rust-lang/rust/issues/31301#issuecomment-177117325) - if let Err(err) = fs::rename(target.join("pkg"), target.join("dist/pkg")) { - return Err(ExecutionError::MovePkgDirFailed { source: err }); - } +// /// Finalizes the build by renaming some directories. +// pub fn finalize(target: &Path) -> Result<(), ExecutionError> { +// // Move the `pkg/` directory into `dist/pkg/` +// let pkg_dir = target.join("dist/pkg"); +// if pkg_dir.exists() { +// if let Err(err) = fs::remove_dir_all(&pkg_dir) { +// return Err(ExecutionError::MovePkgDirFailed { source: err }); +// } +// } +// // The `fs::rename()` function will fail on Windows if the destination already exists, so this should work (we've just deleted it as per https://github.com/rust-lang/rust/issues/31301#issuecomment-177117325) +// if let Err(err) = fs::rename(target.join("pkg"), target.join("dist/pkg")) { +// return Err(ExecutionError::MovePkgDirFailed { source: err }); +// } - Ok(()) -} +// Ok(()) +// } /// Actually builds the user's code, program arguments having been interpreted. This needs to know how many steps there are in total /// because the serving logic also uses it. This also takes a `MultiProgress` to interact with so it can be used truly atomically. @@ -55,8 +54,6 @@ pub fn build_internal( ), ExecutionError, > { - let target = dir.join(".perseus"); - // Static generation message let sg_msg = format!( "{} {} Generating your app", @@ -74,20 +71,22 @@ pub fn build_internal( // We make sure to add them at the top (the server spinner may have already been instantiated) let sg_spinner = spinners.insert(0, ProgressBar::new_spinner()); let sg_spinner = cfg_spinner(sg_spinner, &sg_msg); - let sg_target = target.join("builder"); // Static generation needs the `perseus-engine-builder` crate + let sg_dir = dir.clone(); let wb_spinner = spinners.insert(1, ProgressBar::new_spinner()); let wb_spinner = cfg_spinner(wb_spinner, &wb_msg); - let wb_target = target; + let wb_dir = dir; let sg_thread = spawn_thread(move || { handle_exit_code!(run_stage( vec![&format!( - "{} run {}", + "{} run {} {}", env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - if is_release { "--release" } else { "" } + if is_release { "--release" } else { "" }, + env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) )], - &sg_target, + &sg_dir, &sg_spinner, - &sg_msg + &sg_msg, + "build" )?); Ok(0) @@ -95,13 +94,15 @@ pub fn build_internal( let wb_thread = spawn_thread(move || { handle_exit_code!(run_stage( vec![&format!( - "{} build --target web {}", + "{} build --out-dir dist/pkg --out-name perseus_engine --target web {} {}", env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), - if is_release { "--release" } else { "--dev" } // If we don't supply `--dev`, another profile will be used + if is_release { "--release" } else { "--dev" }, // If we don't supply `--dev`, another profile will be used + env::var("PERSEUS_WASM_PACK_ARGS").unwrap_or_else(|_| String::new()) )], - &wb_target, + &wb_dir, &wb_spinner, - &wb_msg + &wb_msg, + "" // Not a builder command )?); Ok(0) @@ -114,7 +115,7 @@ pub fn build_internal( pub fn build(dir: PathBuf, opts: BuildOpts) -> Result { let spinners = MultiProgress::new(); - let (sg_thread, wb_thread) = build_internal(dir.clone(), &spinners, 2, opts.release)?; + let (sg_thread, wb_thread) = build_internal(dir, &spinners, 2, opts.release)?; let sg_res = sg_thread .join() .map_err(|_| ExecutionError::ThreadWaitFailed)??; @@ -131,7 +132,7 @@ pub fn build(dir: PathBuf, opts: BuildOpts) -> Result { // This waits for all the threads and lets the spinners draw to the terminal // spinners.join().map_err(|_| ErrorKind::ThreadWaitFailed)?; // And now we can run the finalization stage - finalize(&dir.join(".perseus"))?; + // finalize(&dir)?; // We've handled errors in the component threads, so the exit code is now zero Ok(0) diff --git a/packages/perseus-cli/src/cmd.rs b/packages/perseus-cli/src/cmd.rs index abffdc7f03..121d2ff116 100644 --- a/packages/perseus-cli/src/cmd.rs +++ b/packages/perseus-cli/src/cmd.rs @@ -14,6 +14,8 @@ pub static FAILURE: Emoji<'_, '_> = Emoji("❌", "failed!"); pub fn run_cmd( cmd: String, dir: &Path, + // This is only relevant for builder-related commands, but since that's most things, we may as well (it's only an env var) + op: &str, pre_dump: impl Fn(), ) -> Result<(String, String, i32), ExecutionError> { // We run the command in a shell so that NPM/Yarn binaries can be recognized (see #5) @@ -29,6 +31,7 @@ pub fn run_cmd( // This will NOT pipe output/errors to the console let output = Command::new(shell_exec) .args([shell_param, &cmd]) + .env("PERSEUS_ENGINE_OPERATION", op) .current_dir(dir) .output() .map_err(|err| ExecutionError::CmdExecFailed { cmd, source: err })?; @@ -79,12 +82,13 @@ pub fn run_stage( target: &Path, spinner: &ProgressBar, message: &str, + op: &str, ) -> Result<(String, String, i32), ExecutionError> { let mut last_output = (String::new(), String::new()); // Run the commands for cmd in cmds { // We make sure all commands run in the target directory ('.perseus/' itself) - let (stdout, stderr, exit_code) = run_cmd(cmd.to_string(), target, || { + let (stdout, stderr, exit_code) = run_cmd(cmd.to_string(), target, op, || { // This stage has failed fail_spinner(spinner, message); })?; @@ -103,7 +107,7 @@ pub fn run_stage( /// Runs a command directly, piping its output and errors to the streams of this program. This allows the user to investigate the innards of /// Perseus, or just see their own `dbg!` calls. This will return the exit code of the command, which should be passed through to this program. -pub fn run_cmd_directly(cmd: String, dir: &Path) -> Result { +pub fn run_cmd_directly(cmd: String, dir: &Path, op: &str) -> Result { // The shell configurations for Windows and Unix #[cfg(unix)] let shell_exec = "sh"; @@ -117,6 +121,7 @@ pub fn run_cmd_directly(cmd: String, dir: &Path) -> Result let output = Command::new(shell_exec) .args([shell_param, &cmd]) .current_dir(dir) + .env("PERSEUS_ENGINE_OPERATION", op) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output() diff --git a/packages/perseus-cli/src/deploy.rs b/packages/perseus-cli/src/deploy.rs index c5c7426f69..a43324d550 100644 --- a/packages/perseus-cli/src/deploy.rs +++ b/packages/perseus-cli/src/deploy.rs @@ -1,6 +1,5 @@ use crate::errors::*; use crate::export; -use crate::parse::Integration; use crate::parse::{DeployOpts, ExportOpts, ServeOpts}; use crate::serve; use fs_extra::copy_items; @@ -16,7 +15,7 @@ pub fn deploy(dir: PathBuf, opts: DeployOpts) -> Result { let exit_code = if opts.export_static { deploy_export(dir, opts.output)? } else { - deploy_full(dir, opts.output, opts.integration)? + deploy_full(dir, opts.output)? }; Ok(exit_code) @@ -24,7 +23,7 @@ pub fn deploy(dir: PathBuf, opts: DeployOpts) -> Result { /// Deploys the user's app in its entirety, with a bundled server. This can return any kind of error because deploying involves working /// with other subcommands. -fn deploy_full(dir: PathBuf, output: String, integration: Integration) -> Result { +fn deploy_full(dir: PathBuf, output: String) -> Result { // Build everything for production, not running the server let (serve_exit_code, server_path) = serve( dir.clone(), @@ -33,7 +32,6 @@ fn deploy_full(dir: PathBuf, output: String, integration: Integration) -> Result no_build: false, release: true, standalone: true, - integration, watch: false, // These have no impact if `no_run` is `true` (which it is), so we can use the defaults here host: "127.0.0.1".to_string(), @@ -72,17 +70,6 @@ fn deploy_full(dir: PathBuf, output: String, integration: Integration) -> Result } .into()); } - // Copy in the `index.html` file - let from = dir.join("index.html"); - let to = output_path.join("index.html"); - if let Err(err) = fs::copy(&from, &to) { - return Err(DeployError::MoveAssetFailed { - to: to.to_str().map(|s| s.to_string()).unwrap(), - from: from.to_str().map(|s| s.to_string()).unwrap(), - source: err, - } - .into()); - } // Copy in the `static/` directory if it exists let from = dir.join("static"); if from.exists() { @@ -107,8 +94,8 @@ fn deploy_full(dir: PathBuf, output: String, integration: Integration) -> Result .into()); } } - // Copy in the entire `.perseus/dist` directory (it must exist) - let from = dir.join(".perseus/dist"); + // Copy in the entire `dist` directory (it must exist) + let from = dir.join("dist"); if let Err(err) = copy_dir(&from, &output, &CopyOptions::new()) { return Err(DeployError::MoveDirFailed { to: output, @@ -145,9 +132,9 @@ fn deploy_export(dir: PathBuf, output: String) -> Result { if export_exit_code != 0 { return Ok(export_exit_code); } - // That subcommand produces a self-contained static site at `.perseus/dist/exported/` + // That subcommand produces a self-contained static site at `dist/exported/` // Just copy that out to the output directory - let from = dir.join(".perseus/dist/exported"); + let from = dir.join("dist/exported"); let output_path = PathBuf::from(&output); // Delete the output directory if it exists and recreate it if output_path.exists() { diff --git a/packages/perseus-cli/src/eject.rs b/packages/perseus-cli/src/eject.rs deleted file mode 100644 index 7741555980..0000000000 --- a/packages/perseus-cli/src/eject.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::errors::*; -use std::fs; -use std::path::PathBuf; - -/// Ejects the user from the Perseus CLi harness by exposing the internal subcrates to them. All this does is remove `.perseus/` from -/// the user's `.gitignore` and add a file `.ejected` to `.perseus/`. -pub fn eject(dir: PathBuf) -> Result<(), EjectionError> { - // Create a file declaring ejection so `clean` throws errors (we don't want the user to accidentally delete everything) - let ejected = dir.join(".perseus/.ejected"); - fs::write( - &ejected, - "This file signals to Perseus that you've ejected. Do NOT delete it!", - ) - .map_err(|err| EjectionError::GitignoreUpdateFailed { source: err })?; - // Now remove `.perseus/` from the user's `.gitignore` - let gitignore = dir.join(".gitignore"); - if gitignore.exists() { - let content = fs::read_to_string(&gitignore) - .map_err(|err| EjectionError::GitignoreUpdateFailed { source: err })?; - let mut new_content_vec = Vec::new(); - // Remove the line pertaining to Perseus - // We only target the one that's exactly the same as what's automatically injected, anything else can be done manually - let mut have_changed = false; - for line in content.lines() { - if line != ".perseus/" { - new_content_vec.push(line); - } else { - have_changed = true; - } - } - let new_content = new_content_vec.join("\n"); - // Make sure we've actually changed something - if !have_changed { - return Err(EjectionError::GitignoreLineNotPresent); - } - fs::write(&gitignore, new_content) - .map_err(|err| EjectionError::GitignoreUpdateFailed { source: err })?; - - Ok(()) - } else { - // The file wasn't found - Err(EjectionError::GitignoreUpdateFailed { - source: std::io::Error::from(std::io::ErrorKind::NotFound), - }) - } -} - -/// Checks if the user has ejected or not. If they have, commands like `clean` should fail unless `--force` is provided. -pub fn has_ejected(dir: PathBuf) -> bool { - let ejected = dir.join(".perseus/.ejected"); - ejected.exists() -} diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index 6aa51d2155..8ff7fe5a1d 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -5,23 +5,6 @@ use thiserror::Error; /// All errors that can be returned by the CLI. #[derive(Error, Debug)] pub enum Error { - #[error(transparent)] - PrepError(#[from] PrepError), - #[error(transparent)] - ExecutionError(#[from] ExecutionError), - #[error(transparent)] - EjectionError(#[from] EjectionError), - #[error(transparent)] - ExportError(#[from] ExportError), - #[error(transparent)] - DeployError(#[from] DeployError), - #[error(transparent)] - WatchError(#[from] WatchError), -} - -/// Errors that can occur while preparing. -#[derive(Error, Debug)] -pub enum PrepError { #[error("prerequisite command execution failed for prerequisite '{cmd}' (set '{env_var}' to another location if you've installed it elsewhere)")] PrereqNotPresent { cmd: String, @@ -34,62 +17,14 @@ pub enum PrepError { #[source] source: std::io::Error, }, - #[error("couldn't extract internal subcrates to '{target_dir:?}' (do you have the necessary permissions?)")] - ExtractionFailed { - target_dir: Option, - #[source] - source: std::io::Error, - }, - #[error("updating gitignore to ignore `.perseus/` failed (`.perseus/` has been automatically deleted)")] - GitignoreUpdateFailed { - #[source] - source: std::io::Error, - }, - #[error("couldn't update internal manifest file at '{target_dir:?}' (`.perseus/` has been automatically deleted)")] - ManifestUpdateFailed { - target_dir: Option, - #[source] - source: std::io::Error, - }, - #[error("couldn't get `Cargo.toml` for your project (have you run `cargo init` yet?)")] - GetUserManifestFailed { - #[source] - source: cargo_toml::Error, - }, - #[error( - "your project's `Cargo.toml` doesn't have a `[package]` section (package name is required)" - )] - MalformedUserManifest, - #[error("couldn't remove corrupted `.perseus/` directory as required by previous error (please delete `.perseus/` manually)")] - RemoveBadDirFailed { - #[source] - source: std::io::Error, - }, - #[error("fetching the custom engine failed")] - GetEngineFailed { - #[source] - source: ExecutionError, - }, - #[error("fetching the custom engine returned non-zero exit code ({exit_code})")] - GetEngineNonZeroExitCode { exit_code: i32 }, - #[error("couldn't remove git internals at '{target_dir:?}' for custom engine")] - RemoveEngineGitFailed { - target_dir: Option, - #[source] - source: std::io::Error, - }, -} -/// Checks if the given error should cause the CLI to delete the '.perseus/' folder so the user doesn't have something incomplete. -/// When deleting the directory, it should only be deleted if it exists, if not don't worry. If it does and deletion fails, fail like hell. -pub fn err_should_cause_deletion(err: &Error) -> bool { - matches!( - err, - Error::PrepError( - PrepError::ExtractionFailed { .. } - | PrepError::GitignoreUpdateFailed { .. } - | PrepError::ManifestUpdateFailed { .. } - ) - ) + #[error(transparent)] + ExecutionError(#[from] ExecutionError), + #[error(transparent)] + ExportError(#[from] ExportError), + #[error(transparent)] + DeployError(#[from] DeployError), + #[error(transparent)] + WatchError(#[from] WatchError), } /// Errors that can occur while attempting to execute a Perseus app with `build`/`serve` (export errors are separate). @@ -118,11 +53,6 @@ pub enum ExecutionError { #[source] source: std::io::Error, }, - #[error("couldn't move `.perseus/pkg/` to `.perseus/dist/pkg/` (run `perseus clean` if this persists)")] - MovePkgDirFailed { - #[source] - source: std::io::Error, - }, #[error("failed to wait on thread (please report this as a bug if it persists)")] ThreadWaitFailed, #[error("value in `PORT` environment variable couldn't be parsed as a number")] @@ -132,27 +62,6 @@ pub enum ExecutionError { }, } -/// Errors that can occur while ejecting or as a result of doing so. -#[derive(Error, Debug)] -pub enum EjectionError { - #[error("couldn't remove perseus subcrates from gitignore for ejection")] - GitignoreUpdateFailed { - #[source] - source: std::io::Error, - }, - #[error("line `.perseus/` to remove not found in `.gitignore`")] - GitignoreLineNotPresent, - #[error("couldn't write ejection declaration file (`.perseus/.ejected`), please try again")] - DeclarationWriteFailed { - #[source] - source: std::io::Error, - }, - #[error("can't clean after ejection unless `--force` is provided (maybe you meant to use `--dist`?)")] - CleanAfterEject, - #[error("can't tinker after ejection unless `--force` is provided (ejecting and using plugins can be problematic depending on the plugins used)")] - TinkerAfterEject, -} - /// Errors that can occur while running `perseus export`. #[derive(Error, Debug)] pub enum ExportError { diff --git a/packages/perseus-cli/src/export.rs b/packages/perseus-cli/src/export.rs index a5fd060872..86c20365b3 100644 --- a/packages/perseus-cli/src/export.rs +++ b/packages/perseus-cli/src/export.rs @@ -38,18 +38,6 @@ macro_rules! copy_file { /// Finalizes the export by copying assets. This is very different from the finalization process of normal building. pub fn finalize_export(target: &Path) -> Result<(), ExportError> { - // Move the `pkg/` directory into `dist/pkg/` as usual - let pkg_dir = target.join("dist/pkg"); - if pkg_dir.exists() { - if let Err(err) = fs::remove_dir_all(&pkg_dir) { - return Err(ExecutionError::MovePkgDirFailed { source: err }.into()); - } - } - // The `fs::rename()` function will fail on Windows if the destination already exists, so this should work (we've just deleted it as per https://github.com/rust-lang/rust/issues/31301#issuecomment-177117325) - if let Err(err) = fs::rename(target.join("pkg"), target.join("dist/pkg")) { - return Err(ExecutionError::MovePkgDirFailed { source: err }.into()); - } - // Copy files over (the directory structure should already exist from exporting the pages) copy_file!( "dist/pkg/perseus_engine.js", @@ -139,8 +127,6 @@ pub fn export_internal( ), ExportError, > { - let target = dir.join(".perseus"); - // Exporting pages message let ep_msg = format!( "{} {} Exporting your app's pages", @@ -158,20 +144,22 @@ pub fn export_internal( // We make sure to add them at the top (the server spinner may have already been instantiated) let ep_spinner = spinners.insert(0, ProgressBar::new_spinner()); let ep_spinner = cfg_spinner(ep_spinner, &ep_msg); - let ep_target = target.join("builder"); + let ep_target = dir.clone(); let wb_spinner = spinners.insert(1, ProgressBar::new_spinner()); let wb_spinner = cfg_spinner(wb_spinner, &wb_msg); - let wb_target = target; + let wb_target = dir; let ep_thread = spawn_thread(move || { handle_exit_code!(run_stage( vec![&format!( - "{} run --bin perseus-exporter {}", + "{} run {} {}", env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - if is_release { "--release" } else { "" } + if is_release { "--release" } else { "" }, + env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) )], &ep_target, &ep_spinner, - &ep_msg + &ep_msg, + "export" )?); Ok(0) @@ -179,13 +167,15 @@ pub fn export_internal( let wb_thread = spawn_thread(move || { handle_exit_code!(run_stage( vec![&format!( - "{} build --target web {}", + "{} build --out-dir dist/pkg --out-name perseus_engine --target web {} {}", env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), - if is_release { "--release" } else { "--dev" } + if is_release { "--release" } else { "--dev" }, + env::var("PERSEUS_WASM_PACK_ARGS").unwrap_or_else(|_| String::new()) )], &wb_target, &wb_spinner, - &wb_msg + &wb_msg, + "" // Not a builder command )?); Ok(0) @@ -216,7 +206,7 @@ pub fn export(dir: PathBuf, opts: ExportOpts) -> Result { } // And now we can run the finalization stage - finalize_export(&dir.join(".perseus"))?; + finalize_export(&dir)?; // We've handled errors in the component threads, so the exit code is now zero Ok(0) diff --git a/packages/perseus-cli/src/export_error_page.rs b/packages/perseus-cli/src/export_error_page.rs index 278d9412ef..62ffd23426 100644 --- a/packages/perseus-cli/src/export_error_page.rs +++ b/packages/perseus-cli/src/export_error_page.rs @@ -6,15 +6,16 @@ use std::path::PathBuf; /// Exports a single error page for the given HTTP status code to the given location. pub fn export_error_page(dir: PathBuf, opts: ExportErrorPageOpts) -> Result { - let target = dir.join(".perseus/builder"); run_cmd_directly( format!( - "{} run --bin perseus-error-page-exporter {} {}", + "{} {} run {} {}", env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), + env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()), // These are mandatory opts.code, - opts.output + opts.output, ), - &target, + &dir, + "export_error_page", ) } diff --git a/packages/perseus-cli/src/extraction.rs b/packages/perseus-cli/src/extraction.rs deleted file mode 100644 index f212633dde..0000000000 --- a/packages/perseus-cli/src/extraction.rs +++ /dev/null @@ -1,30 +0,0 @@ -// This file contains a temporary fix for the issues with recursive extraction in `include_dir` -// Tracking issue is https://github.com/Michael-F-Bryan/include_dir/issues/59 - -use include_dir::Dir; -use std::io::Write; -use std::path::Path; - -/// Extracts a directory included with `include_dir!` until issue #59 is fixed on that module (recursive extraction support). -pub fn extract_dir>(dir: Dir, path: S) -> std::io::Result<()> { - let path = path.as_ref(); - - // Create all the subdirectories in here (but not their files yet) - for dir in dir.dirs() { - std::fs::create_dir_all(path.join(dir.path()))?; - // Recurse for this directory - extract_dir(*dir, path)?; - } - - // Write all the files at the root of this directory - for file in dir.files() { - let mut fsf = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(path.join(file.path()))?; - fsf.write_all(file.contents())?; - fsf.sync_all()?; - } - - Ok(()) -} diff --git a/packages/perseus-cli/src/lib.rs b/packages/perseus-cli/src/lib.rs index fa021f5a0f..7e82ed0f82 100644 --- a/packages/perseus-cli/src/lib.rs +++ b/packages/perseus-cli/src/lib.rs @@ -18,11 +18,9 @@ the documentation you'd like to see on this front! mod build; mod cmd; mod deploy; -mod eject; pub mod errors; mod export; mod export_error_page; -mod extraction; /// Parsing utilities for arguments. pub mod parse; mod prepare; @@ -41,34 +39,35 @@ use std::path::PathBuf; pub const PERSEUS_VERSION: &str = env!("CARGO_PKG_VERSION"); pub use build::build; pub use deploy::deploy; -pub use eject::{eject, has_ejected}; pub use export::export; pub use export_error_page::export_error_page; -pub use prepare::{check_env, prepare}; +pub use prepare::check_env; pub use reload_server::{order_reload, run_reload_server}; pub use serve::serve; pub use serve_exported::serve_exported; pub use snoop::{snoop_build, snoop_server, snoop_wasm_build}; pub use tinker::tinker; -/// Deletes a corrupted '.perseus/' directory. This will be called on certain error types that would leave the user with a half-finished -/// product, which is better to delete for safety and sanity. -pub fn delete_bad_dir(dir: PathBuf) -> Result<(), PrepError> { - let mut target = dir; - target.extend([".perseus"]); - // We'll only delete the directory if it exists, otherwise we're fine +/// Deletes the entire `dist/` directory. Nicely, because there are no Cargo artifacts in there, +/// running this won't slow down future runs at all. +pub fn delete_dist(dir: PathBuf) -> Result<(), ExecutionError> { + let target = dir.join("dist"); if target.exists() { if let Err(err) = fs::remove_dir_all(&target) { - return Err(PrepError::RemoveBadDirFailed { source: err }); + return Err(ExecutionError::RemoveArtifactsFailed { + target: target.to_str().map(|s| s.to_string()), + source: err, + }); } } + Ok(()) } -/// Deletes build artifacts in `.perseus/dist/static` or `.perseus/dist/pkg` and replaces the directory. +/// Deletes build artifacts in `dist/static` or `dist/pkg` and replaces the directory. pub fn delete_artifacts(dir: PathBuf, dir_to_remove: &str) -> Result<(), ExecutionError> { let mut target = dir; - target.extend([".perseus", "dist", dir_to_remove]); + target.extend(["dist", dir_to_remove]); // We'll only delete the directory if it exists, otherwise we're fine if target.exists() { if let Err(err) = fs::remove_dir_all(&target) { diff --git a/packages/perseus-cli/src/parse.rs b/packages/perseus-cli/src/parse.rs index 6c67f7cf5d..e614da27db 100644 --- a/packages/perseus-cli/src/parse.rs +++ b/packages/perseus-cli/src/parse.rs @@ -10,43 +10,10 @@ use clap::Parser; #[clap(version = PERSEUS_VERSION)] // #[clap(setting = AppSettings::ColoredHelp)] pub struct Opts { - /// The URL of a Git repository to clone to provide a custom engine. If this is set to `default`, the normal Perseus engine (packaged with the CLI) will be used. A branch name can be added at - /// the end of this after `@` to specify a custom branch/version - #[clap(short, long, default_value = "default")] - pub engine: String, #[clap(subcommand)] pub subcmd: Subcommand, } -#[derive(Parser, PartialEq, Eq, Clone)] -pub enum Integration { - ActixWeb, - Warp, - Axum, -} -// We use an `enum` for this so we don't get errors from Cargo about non-existent feature flags, overly verbose but fails quickly -impl std::str::FromStr for Integration { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "actix-web" => Ok(Self::ActixWeb), - "warp" => Ok(Self::Warp), - "axum" => Ok(Self::Axum), - _ => Err("invalid integration name".into()), - } - } -} -impl ToString for Integration { - fn to_string(&self) -> String { - match self { - Self::ActixWeb => "actix-web".to_string(), - Self::Warp => "warp".to_string(), - Self::Axum => "axum".to_string(), - } - } -} - #[derive(Parser)] pub enum Subcommand { Build(BuildOpts), @@ -55,12 +22,9 @@ pub enum Subcommand { Serve(ServeOpts), /// Serves your app as `perseus serve` does, but puts it in testing mode Test(ServeOpts), - Clean(CleanOpts), - /// Ejects you from the CLI harness, enabling you to work with the internals of Perseus - Eject, + /// Removes build artifacts in the `dist/` directory + Clean, Deploy(DeployOpts), - /// Prepares the `.perseus/` directory (done automatically by `build` and `serve`) - Prep, Tinker(TinkerOpts), /// Runs one of the underlying commands that builds your app, allowing you to see more detailed logs #[clap(subcommand)] @@ -115,9 +79,6 @@ pub struct ServeOpts { /// Make the final binary standalone (this is used in `perseus deploy` only, don't manually invoke it unless you have a good reason!) #[clap(long)] pub standalone: bool, - /// The server integration to use - #[clap(short, long, default_value = "warp")] - pub integration: Integration, /// Watch the files in your working directory for changes (exluding `target/` and `.perseus/`) #[clap(short, long)] pub watch: bool, @@ -128,16 +89,6 @@ pub struct ServeOpts { #[clap(long, default_value = "8080")] pub port: u16, } -/// Removes `.perseus/` entirely for updates or to fix corruptions -#[derive(Parser)] -pub struct CleanOpts { - /// Only remove the `.perseus/dist/` folder (use if you've ejected) - #[clap(short, long)] - pub dist: bool, - /// Remove the directory, even if you've ejected (this will permanently destroy any changes you've made to `.perseus/`!) - #[clap(short, long)] - pub force: bool, -} /// Packages your app for deployment #[derive(Parser)] pub struct DeployOpts { @@ -147,19 +98,13 @@ pub struct DeployOpts { /// Export you app to purely static files (see `export`) #[clap(short, long)] pub export_static: bool, - /// The server integration to use (only affects non-exported deployments) - #[clap(short, long, default_value = "warp")] - pub integration: Integration, } /// Runs the `tinker` action of plugins, which lets them modify the Perseus engine #[derive(Parser)] pub struct TinkerOpts { - /// Don't remove and recreate the `.perseus/` directory + /// Don't remove and recreate the `dist/` directory #[clap(long)] pub no_clean: bool, - /// Force this command to run, even if you've ejected (this may result in some or all of your changes being removed, it depends on the plugins you're using) - #[clap(long)] - pub force: bool, } #[derive(Parser)] @@ -181,9 +126,6 @@ pub struct SnoopWasmOpts { #[derive(Parser)] pub struct SnoopServeOpts { - /// The server integration to use - #[clap(short, long, default_value = "warp")] - pub integration: Integration, /// Where to host your exported app #[clap(long, default_value = "127.0.0.1")] pub host: String, diff --git a/packages/perseus-cli/src/prepare.rs b/packages/perseus-cli/src/prepare.rs index ac57099636..f5b6cdda03 100644 --- a/packages/perseus-cli/src/prepare.rs +++ b/packages/perseus-cli/src/prepare.rs @@ -1,229 +1,13 @@ -use crate::cmd::run_cmd_directly; use crate::errors::*; -use crate::extraction::extract_dir; #[allow(unused_imports)] -use crate::PERSEUS_VERSION; use cargo_toml::Manifest; -use include_dir::{include_dir, Dir}; use std::env; -use std::fs; -use std::fs::OpenOptions; -use std::io::Write; -use std::path::PathBuf; use std::process::Command; -// This literally includes the entire subcrate in the program, allowing more efficient development. -// This MUST be copied in from `../../examples/cli/.perseus/` every time the CLI is tested (use the Bonnie script). -const SUBCRATES: Dir = include_dir!("./.perseus"); - -/// Prepares the user's project by copying in the `.perseus/` subcrates. We use these subcrates to do all the building/serving, we just -/// have to execute the right commands in the CLI. We can essentially treat the subcrates themselves as a blackbox of just a folder. -pub fn prepare(dir: PathBuf, engine_url: &str) -> Result<(), PrepError> { - // The location in the target directory at which we'll put the subcrates - let target = dir.join(".perseus"); - - if target.exists() { - // We don't care if it's corrupted etc., it just has to exist - // If the user wants to clean it, they can do that - // Besides, we want them to be able to customize stuff - Ok(()) - } else { - // Create the directory first - if let Err(err) = fs::create_dir(&target) { - return Err(PrepError::ExtractionFailed { - target_dir: target.to_str().map(|s| s.to_string()), - source: err, - }); - } - // Check if we're using the bundled engine or a custom one - if engine_url == "default" { - // Write the stored directory to the target location - // Notably, this function will not do anything or tell us if the directory already exists... - if let Err(err) = extract_dir(SUBCRATES, &target) { - return Err(PrepError::ExtractionFailed { - target_dir: target.to_str().map(|s| s.to_string()), - source: err, - }); - } - } else { - // We're using a non-standard engine, which we'll download using Git - // All other steps of integration with the user's package after this are the same - let url_parts = engine_url.split('@').collect::>(); - let engine_url = url_parts[0]; - // A custom branch can be specified after a `@`, or we'll use `stable` - let engine_branch = url_parts.get(1).unwrap_or(&"stable"); - let cmd = format!( - // We'll only clone the production branch, and only the top level, we don't need the whole shebang - "{} clone --single-branch --branch {branch} --depth 1 {repo} {output}", - env::var("PERSEUS_GIT_PATH").unwrap_or_else(|_| "git".to_string()), - branch = engine_branch, - repo = engine_url, - output = target.to_string_lossy() - ); - println!("Fetching custom engine with command: '{}'.", &cmd); - // Tell the user what command we're running so that they can debug it - let exit_code = run_cmd_directly( - cmd, - &dir, // We'll run this in the current directory and output into `.perseus/` - ) - .map_err(|err| PrepError::GetEngineFailed { source: err })?; - if exit_code != 0 { - return Err(PrepError::GetEngineNonZeroExitCode { exit_code }); - } - // Now delete the Git internals - let git_target = target.join(".git"); - if let Err(err) = fs::remove_dir_all(&git_target) { - return Err(PrepError::RemoveEngineGitFailed { - target_dir: git_target.to_str().map(|s| s.to_string()), - source: err, - }); - } - } - - // Prepare for transformations on the manifest files - // We have to store `Cargo.toml` as `Cargo.toml.old` for packaging - let root_manifest_pkg = target.join("Cargo.toml.old"); - let root_manifest = target.join("Cargo.toml"); - let server_manifest_pkg = target.join("server/Cargo.toml.old"); - let server_manifest = target.join("server/Cargo.toml"); - let builder_manifest_pkg = target.join("builder/Cargo.toml.old"); - let builder_manifest = target.join("builder/Cargo.toml"); - let root_manifest_contents = fs::read_to_string(&root_manifest_pkg).map_err(|err| { - PrepError::ManifestUpdateFailed { - target_dir: root_manifest_pkg.to_str().map(|s| s.to_string()), - source: err, - } - })?; - let server_manifest_contents = fs::read_to_string(&server_manifest_pkg).map_err(|err| { - PrepError::ManifestUpdateFailed { - target_dir: server_manifest_pkg.to_str().map(|s| s.to_string()), - source: err, - } - })?; - let builder_manifest_contents = - fs::read_to_string(&builder_manifest_pkg).map_err(|err| { - PrepError::ManifestUpdateFailed { - target_dir: builder_manifest_pkg.to_str().map(|s| s.to_string()), - source: err, - } - })?; - // Get the name of the user's crate (which the subcrates depend on) - // We assume they're running this in a folder with a Cargo.toml... - let user_manifest = Manifest::from_path("./Cargo.toml") - .map_err(|err| PrepError::GetUserManifestFailed { source: err })?; - let user_crate_name = user_manifest.package; - let user_crate_name = match user_crate_name { - Some(package) => package.name, - None => return Err(PrepError::MalformedUserManifest), - }; - // Update the name of the user's crate (Cargo needs more than just a path and an alias) - // We don't need to do that in the server manifest because it uses the root code (which re-exports the `PerseusApp`) - // We used to add a workspace here, but that means size optimizations apply to both the client and the server, so that's not done anymore - // Now, we use an empty workspace to make sure we don't include the engine in any user workspaces - // We use a token here that's set by the build script - let updated_root_manifest = - root_manifest_contents.replace("USER_PKG_NAME", &user_crate_name) + "\n[workspace]"; - let updated_server_manifest = server_manifest_contents + "\n[workspace]"; - let updated_builder_manifest = builder_manifest_contents + "\n[workspace]"; - - // We also need to set the Perseus version - // In production, we'll use the full version, but in development we'll use relative path references from the examples - // The tokens here are set by the build script once again - // Production - #[cfg(not(debug_assertions))] - let updated_root_manifest = updated_root_manifest.replace( - "PERSEUS_VERSION", - &format!("version = \"{}\"", PERSEUS_VERSION), - ); - #[cfg(not(debug_assertions))] - let updated_server_manifest = updated_server_manifest - .replace( - "PERSEUS_VERSION", - &format!("version = \"{}\"", PERSEUS_VERSION), - ) - .replace( - "PERSEUS_ACTIX_WEB_VERSION", - &format!("version = \"{}\"", PERSEUS_VERSION), - ) - .replace( - "PERSEUS_WARP_VERSION", - &format!("version = \"{}\"", PERSEUS_VERSION), - ); - #[cfg(not(debug_assertions))] - let updated_builder_manifest = updated_builder_manifest.replace( - "PERSEUS_VERSION", - &format!("version = \"{}\"", PERSEUS_VERSION), - ); - // Development - #[cfg(debug_assertions)] - let updated_root_manifest = updated_root_manifest - .replace("PERSEUS_VERSION", "path = \"../../../../packages/perseus\""); - #[cfg(debug_assertions)] - let updated_server_manifest = updated_server_manifest - .replace( - "PERSEUS_VERSION", - "path = \"../../../../../packages/perseus\"", - ) - .replace( - "PERSEUS_ACTIX_WEB_VERSION", - "path = \"../../../../../packages/perseus-actix-web\"", - ) - .replace( - "PERSEUS_WARP_VERSION", - "path = \"../../../../../packages/perseus-warp\"", - ); - #[cfg(debug_assertions)] - let updated_builder_manifest = updated_builder_manifest.replace( - "PERSEUS_VERSION", - "path = \"../../../../../packages/perseus\"", - ); - - // Write the updated manifests back - if let Err(err) = fs::write(&root_manifest, updated_root_manifest) { - return Err(PrepError::ManifestUpdateFailed { - target_dir: root_manifest.to_str().map(|s| s.to_string()), - source: err, - }); - } - if let Err(err) = fs::write(&server_manifest, updated_server_manifest) { - return Err(PrepError::ManifestUpdateFailed { - target_dir: server_manifest.to_str().map(|s| s.to_string()), - source: err, - }); - } - if let Err(err) = fs::write(&builder_manifest, updated_builder_manifest) { - return Err(PrepError::ManifestUpdateFailed { - target_dir: builder_manifest.to_str().map(|s| s.to_string()), - source: err, - }); - } - - // If we aren't already gitignoring the subcrates, update .gitignore to do so - if let Ok(contents) = fs::read_to_string(".gitignore") { - if contents.contains(".perseus/") { - return Ok(()); - } - } - let file = OpenOptions::new() - .append(true) - .create(true) // If it doesn't exist, create it - .open(".gitignore"); - let mut file = match file { - Ok(file) => file, - Err(err) => return Err(PrepError::GitignoreUpdateFailed { source: err }), - }; - // Check for errors with appending to the file - if let Err(err) = file.write_all(b"\n.perseus/") { - return Err(PrepError::GitignoreUpdateFailed { source: err }); - } - Ok(()) - } -} - /// Checks if the user has the necessary prerequisites on their system (i.e. `cargo` and `wasm-pack`). These can all be checked /// by just trying to run their binaries and looking for errors. If the user has other paths for these, they can define them under the /// environment variables `PERSEUS_CARGO_PATH` and `PERSEUS_WASM_PACK_PATH`. -pub fn check_env() -> Result<(), PrepError> { +pub fn check_env() -> Result<(), Error> { // We'll loop through each prerequisite executable to check their existence // If the spawn returns an error, it's considered not present, success means presence let prereq_execs = vec![ @@ -243,7 +27,7 @@ pub fn check_env() -> Result<(), PrepError> { let res = Command::new(&exec.0).output(); // Any errors are interpreted as meaning that the user doesn't have the prerequisite installed properly. if let Err(err) = res { - return Err(PrepError::PrereqNotPresent { + return Err(Error::PrereqNotPresent { cmd: exec.1.to_string(), env_var: exec.2.to_string(), source: err, diff --git a/packages/perseus-cli/src/serve.rs b/packages/perseus-cli/src/serve.rs index 6476d71fb3..5df7db896e 100644 --- a/packages/perseus-cli/src/serve.rs +++ b/packages/perseus-cli/src/serve.rs @@ -1,6 +1,6 @@ -use crate::build::{build_internal, finalize}; +use crate::build::build_internal; use crate::cmd::{cfg_spinner, run_stage}; -use crate::parse::{Integration, ServeOpts}; +use crate::parse::ServeOpts; use crate::thread::{spawn_thread, ThreadHandle}; use crate::{errors::*, order_reload}; use console::{style, Emoji}; @@ -36,24 +36,14 @@ fn build_server( did_build: bool, exec: Arc>, is_release: bool, - is_standalone: bool, - integration: Integration, ) -> Result< ThreadHandle Result, Result>, ExecutionError, > { - // If we're using the Actix Web integration, warn that it's unstable - // A similar warning is emitted for snooping on it - // TODO Remove this once Actix Web v4.0.0 goes stable - if integration == Integration::ActixWeb { - println!("WARNING: The Actix Web integration uses a beta version of Actix Web, and is considered unstable. It is not recommended for production usage.") - } - let num_steps = match did_build { true => 4, false => 2, }; - let target = dir.join(".perseus/server"); // Server building message let sb_msg = format!( @@ -68,27 +58,21 @@ fn build_server( // We deliberately insert the spinner at the end of the list let sb_spinner = spinners.insert(num_steps - 1, ProgressBar::new_spinner()); let sb_spinner = cfg_spinner(sb_spinner, &sb_msg); - let sb_target = target; + let sb_target = dir; let sb_thread = spawn_thread(move || { let (stdout, _stderr) = handle_exit_code!(run_stage( - vec![&format!( - // This sets Cargo to tell us everything, including the executable path to the server - "{} build --message-format json --features integration-{} {} --no-default-features {}", - env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), - // Enable the appropriate integration - integration.to_string(), - // We'll also handle whether or not it's standalone because that goes under the `--features` flag - if is_standalone { - "--features standalone" - } else { - "" - }, - if is_release { "--release" } else { "" }, - )], - &sb_target, - &sb_spinner, - &sb_msg - )?); + vec![&format!( + // This sets Cargo to tell us everything, including the executable path to the server + "{} build --message-format json {} {}", + env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), + if is_release { "--release" } else { "" }, + env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) + )], + &sb_target, + &sb_spinner, + &sb_msg, + "" // The server will be built if we build for the server-side (builder and server are currently one for Cargo) + )?); let msgs: Vec<&str> = stdout.trim().split('\n').collect(); // If we got to here, the exit code was 0 and everything should've worked @@ -134,7 +118,6 @@ fn run_server( dir: PathBuf, did_build: bool, ) -> Result { - let target = dir.join(".perseus/server"); let num_steps = match did_build { true => 4, false => 2, @@ -152,7 +135,9 @@ fn run_server( // Manually run the generated binary (invoking in the right directory context for good measure if it ever needs it in future) let child = Command::new(&server_exec_path) - .current_dir(target) + .current_dir(&dir) + // This needs to be provided in development, but not in production + .env("PERSEUS_ENGINE_OPERATION", "serve") // We should be able to access outputs in case there's an error .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -213,8 +198,6 @@ pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result<(i32, Option), Exe did_build, Arc::clone(&exec), opts.release, - opts.standalone, - opts.integration, )?; // Only build if the user hasn't set `--no-build`, handling non-zero exit codes if did_build { @@ -239,11 +222,6 @@ pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result<(i32, Option), Exe return Ok((sb_res, None)); } - // And now we can run the finalization stage (only if `--no-build` wasn't specified) - if did_build { - finalize(&dir.join(".perseus"))?; - } - // Order any connected browsers to reload order_reload(); @@ -254,7 +232,7 @@ pub fn serve(dir: PathBuf, opts: ServeOpts) -> Result<(i32, Option), Exe } else { // The user doesn't want to run the server, so we'll give them the executable path instead let exec_str = (*exec.lock().unwrap()).to_string(); - println!("Not running server because `--no-run` was provided. You can run it manually by running the following executable in `.perseus/server/`.\n{}", &exec_str); + println!("Not running server because `--no-run` was provided. You can run it manually by running the following executable from the root of the project.\n{}", &exec_str); Ok((0, Some(exec_str))) } } diff --git a/packages/perseus-cli/src/serve_exported.rs b/packages/perseus-cli/src/serve_exported.rs index 7fe6a78e40..0aaeedf2c9 100644 --- a/packages/perseus-cli/src/serve_exported.rs +++ b/packages/perseus-cli/src/serve_exported.rs @@ -7,7 +7,7 @@ static SERVING: Emoji<'_, '_> = Emoji("🛰️ ", ""); /// Serves an exported app, assuming it's already been exported. pub async fn serve_exported(dir: PathBuf, host: String, port: u16) { - let dir = dir.join(".perseus/dist/exported"); + let dir = dir.join("dist/exported"); // We actually don't have to worry about HTML file extensions at all let files = warp::any().and(warp::fs::dir(dir)); // Parse `localhost` into `127.0.0.1` (picky Rust `std`) diff --git a/packages/perseus-cli/src/snoop.rs b/packages/perseus-cli/src/snoop.rs index e41e483be0..2703610d7a 100644 --- a/packages/perseus-cli/src/snoop.rs +++ b/packages/perseus-cli/src/snoop.rs @@ -7,22 +7,21 @@ use std::path::PathBuf; /// Runs static generation processes directly so the user can see detailed logs. This is commonly used for allowing users to see `dbg!` and /// the like in their builder functions. pub fn snoop_build(dir: PathBuf) -> Result { - let target = dir.join(".perseus/builder"); run_cmd_directly( format!( "{} run", env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()) ), - &target, + &dir, + "build", ) } /// Runs the commands to build the user's app to Wasm directly so they can see detailed logs. pub fn snoop_wasm_build(dir: PathBuf, opts: SnoopWasmOpts) -> Result { - let target = dir.join(".perseus"); run_cmd_directly( format!( - "{} build --target web {}", + "{} build --out-dir dist/pkg --out-name perseus_engine --target web {}", env::var("PERSEUS_WASM_PACK_PATH").unwrap_or_else(|_| "wasm-pack".to_string()), if opts.profiling { "--profiling" @@ -30,7 +29,8 @@ pub fn snoop_wasm_build(dir: PathBuf, opts: SnoopWasmOpts) -> Result Result Result, Result>, Error, > { - let target = dir.join(".perseus/builder"); - // Tinkering message let tk_msg = format!( "{} {} Running plugin tinkers", @@ -43,16 +41,18 @@ pub fn tinker_internal( // We make sure to add them at the top (other spinners may have already been instantiated) let tk_spinner = spinners.insert(0, ProgressBar::new_spinner()); let tk_spinner = cfg_spinner(tk_spinner, &tk_msg); - let tk_target = target; + let tk_target = dir; let tk_thread = spawn_thread(move || { handle_exit_code!(run_stage( vec![&format!( - "{} run --bin perseus-tinker", + "{} run {}", env::var("PERSEUS_CARGO_PATH").unwrap_or_else(|_| "cargo".to_string()), + env::var("PERSEUS_CARGO_ARGS").unwrap_or_else(|_| String::new()) )], &tk_target, &tk_spinner, - &tk_msg + &tk_msg, + "tinker" )?); Ok(0) diff --git a/packages/perseus-integration/Cargo.toml b/packages/perseus-integration/Cargo.toml new file mode 100644 index 0000000000..8d8e006eb4 --- /dev/null +++ b/packages/perseus-integration/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "perseus-integration" +version = "0.4.0-beta.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +perseus-actix-web = { path = "../perseus-actix-web", features = [ "dflt-server" ], optional = true } +perseus-warp = { path = "../perseus-warp", features = [ "dflt-server" ], optional = true } +perseus-axum = { path = "../perseus-axum", features = [ "dflt-server" ], optional = true } + +[features] +default = [ "warp" ] + +actix-web = [ "perseus-actix-web" ] +warp = [ "perseus-warp" ] +axum = [ "perseus-axum" ] diff --git a/packages/perseus-integration/README.md b/packages/perseus-integration/README.md new file mode 100644 index 0000000000..6b2ad02f3c --- /dev/null +++ b/packages/perseus-integration/README.md @@ -0,0 +1,5 @@ +# THIS CRATE IS FOR TESTING PURPOSES ONLY! + +It merely collates all the currently supported integrations and re-exposes their default servers through feature flags, enabling each of the examples to bring in just one dependency and then support all integrations through feature flags on this crate, which are specified by the CI testing framework. + +In other words, this is an internal convenience package used for testing. diff --git a/packages/perseus-integration/src/lib.rs b/packages/perseus-integration/src/lib.rs new file mode 100644 index 0000000000..2c2893d479 --- /dev/null +++ b/packages/perseus-integration/src/lib.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "actix-web")] +pub use perseus_actix_web::dflt_server; +#[cfg(feature = "axum")] +pub use perseus_axum::dflt_server; +#[cfg(feature = "warp")] +pub use perseus_warp::dflt_server; diff --git a/packages/perseus-macro/src/entrypoint.rs b/packages/perseus-macro/src/entrypoint.rs index 7e7b555e75..76d36653ec 100644 --- a/packages/perseus-macro/src/entrypoint.rs +++ b/packages/perseus-macro/src/entrypoint.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Block, Generics, Item, ItemFn, Result, ReturnType, Type}; +use syn::{Attribute, Block, Generics, Item, ItemFn, Path, Result, ReturnType, Type}; /// A function that can be made into a Perseus app's entrypoint. /// @@ -13,7 +13,7 @@ pub struct MainFn { pub attrs: Vec, /// The return type of the function. pub return_type: Box, - /// Any generics the function takes (shouldn't be any, but it could in theory). + /// Any generics the function takes. pub generics: Generics, } impl Parse for MainFn { @@ -81,7 +81,80 @@ impl Parse for MainFn { } } -pub fn main_impl(input: MainFn) -> TokenStream { +/// An async function that can be made into a Perseus app's entrypoint. (Specifically, the engine entrypoint.) +pub struct EngineMainFn { + /// The body of the function. + pub block: Box, + /// Any attributes the function uses. + pub attrs: Vec, + /// Any generics the function takes (shouldn't be any, but it could in theory). + pub generics: Generics, +} +impl Parse for EngineMainFn { + fn parse(input: ParseStream) -> Result { + let parsed: Item = input.parse()?; + + match parsed { + Item::Fn(func) => { + let ItemFn { + attrs, sig, block, .. + } = func; + // Validate each part of this function to make sure it fulfills the requirements + // Must not be async + if sig.asyncness.is_none() { + return Err(syn::Error::new_spanned( + sig.asyncness, + "the engine entrypoint must be async", + )); + } + // Can't be const + if sig.constness.is_some() { + return Err(syn::Error::new_spanned( + sig.constness, + "the entrypoint can't be a const function", + )); + } + // Can't be external + if sig.abi.is_some() { + return Err(syn::Error::new_spanned( + sig.abi, + "the entrypoint can't be an external function", + )); + } + // Must return something (type checked by the existence of the wrapper code) + match sig.output { + ReturnType::Default => (), + ReturnType::Type(_, _) => { + return Err(syn::Error::new_spanned( + sig, + "the engine entrypoint must have no return value", + )) + } + }; + // Must accept no arguments + let inputs = sig.inputs; + if !inputs.is_empty() { + return Err(syn::Error::new_spanned( + inputs, + "the entrypoint can't take any arguments", + )); + } + + Ok(Self { + block, + attrs, + generics: sig.generics, + }) + } + item => Err(syn::Error::new_spanned( + item, + "only funtions can be used as entrypoints", + )), + } + } +} + +pub fn main_impl(input: MainFn, server_fn: Path) -> TokenStream { let MainFn { block, generics, @@ -89,15 +162,108 @@ pub fn main_impl(input: MainFn) -> TokenStream { return_type, } = input; - // We wrap the user's function to noramlize the name for the engine + // We split the user's function out into one for the browser and one for the engine (all based around the default engine) let output = quote! { - pub fn __perseus_main() -> #return_type { - // The user's function - #(#attrs)* - fn fn_internal #generics() -> #return_type { - #block - } - fn_internal() + // The engine-specific `main` function + #[cfg(not(target_arch = "wasm32"))] + #[tokio::main] + async fn main() { + // Get the operation we're supposed to run (serve, build, export, etc.) from an environment variable + let op = ::perseus::builder::get_op().unwrap(); + let exit_code = ::perseus::builder::run_dflt_engine(op, __perseus_simple_main, #server_fn).await; + std::process::exit(exit_code); + } + + // The browser-specific `main` function + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn main() -> ::perseus::ClientReturn { + ::perseus::run_client(__perseus_simple_main) + } + + // The user's function (which gets the `PerseusApp`) + #(#attrs)* + #[doc(hidden)] + pub fn __perseus_simple_main #generics() -> #return_type { + #block + } + }; + + output +} + +pub fn main_export_impl(input: MainFn) -> TokenStream { + let MainFn { + block, + generics, + attrs, + return_type, + } = input; + + // We split the user's function out into one for the browser and one for the engine (all based around the default engine) + let output = quote! { + // The engine-specific `main` function + #[cfg(not(target_arch = "wasm32"))] + #[tokio::main] + async fn main() { + // Get the operation we're supposed to run (serve, build, export, etc.) from an environment variable + let op = ::perseus::builder::get_op().unwrap(); + let exit_code = ::perseus::builder::run_dflt_engine_export_only(op, __perseus_simple_main).await; + std::process::exit(exit_code); + } + + // The browser-specific `main` function + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn main() -> ::perseus::ClientReturn { + ::perseus::run_client(__perseus_simple_main) + } + + // The user's function (which gets the `PerseusApp`) + #(#attrs)* + #[doc(hidden)] + pub fn __perseus_simple_main #generics() -> #return_type { + #block + } + }; + + output +} + +pub fn browser_main_impl(input: MainFn) -> TokenStream { + let MainFn { + block, + attrs, + return_type, + .. + } = input; + + // We split the user's function out into one for the browser and one for the engine (all based around the default engine) + let output = quote! { + // The browser-specific `main` function + // This absolutely MUST be called `main`, otherwise the hardcodes Wasm importer will fail (and then interactivity is gone completely with a really weird error message) + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen::prelude::wasm_bindgen] + #(#attrs)* + pub fn main() -> #return_type { + #block + } + }; + + output +} + +pub fn engine_main_impl(input: EngineMainFn) -> TokenStream { + let EngineMainFn { block, attrs, .. } = input; + + // We split the user's function out into one for the browser and one for the engine (all based around the default engine) + let output = quote! { + // The engine-specific `main` function + #[cfg(not(target_arch = "wasm32"))] + #[tokio::main] + #(#attrs)* + async fn main() { + #block } }; diff --git a/packages/perseus-macro/src/head.rs b/packages/perseus-macro/src/head.rs index 7a07c0b63f..90c85d52c8 100644 --- a/packages/perseus-macro/src/head.rs +++ b/packages/perseus-macro/src/head.rs @@ -134,6 +134,10 @@ pub fn head_impl(input: HeadFn) -> TokenStream { if arg.is_some() { // There's an argument that will be provided as a `String`, so the wrapper will deserialize it quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<::sycamore::prelude::SsrNode> { // The user's function, with Sycamore component annotations and the like preserved // We know this won't be async because Sycamore doesn't allow that @@ -151,6 +155,10 @@ pub fn head_impl(input: HeadFn) -> TokenStream { } else { // There are no arguments quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis fn #name(cx: ::sycamore::prelude::Scope, props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<::sycamore::prelude::SsrNode> { // The user's function, with Sycamore component annotations and the like preserved // We know this won't be async because Sycamore doesn't allow that diff --git a/packages/perseus-macro/src/lib.rs b/packages/perseus-macro/src/lib.rs index bab3cb9305..10d087a933 100644 --- a/packages/perseus-macro/src/lib.rs +++ b/packages/perseus-macro/src/lib.rs @@ -11,40 +11,81 @@ This is the API documentation for the `perseus-macro` package, which manages Per documentation, and this should mostly be used as a secondary reference source. You can also find full usage examples [here](https://github.com/arctic-hen7/perseus/tree/main/examples). */ -mod autoserde; mod entrypoint; mod head; mod rx_state; +mod state_fns; mod template; mod template_rx; mod test; use darling::FromMeta; use proc_macro::TokenStream; -use syn::ItemStruct; +use quote::quote; +use state_fns::StateFnType; +use syn::{ItemStruct, Path}; -/// Automatically serializes/deserializes properties for a template. Perseus handles your templates' properties as `String`s under the -/// hood for both simplicity and to avoid bundle size increases from excessive monomorphization. This macro aims to prevent the need for -/// manually serializing and deserializing everything! This takes the type of function that it's working on, which must be one of the -/// following: -/// -/// - `build_state` (serializes return type) -/// - `request_state` (serializes return type) -/// - `set_headers` (deserializes parameter) -/// - `amalgamate_states` (serializes return type, you'll still need to deserializes from `States` manually) +/// Annotates functions used for generating state at build time to support automatic serialization/deserialization of app state and +/// client/server division. This supersedes the old `autoserde` macro for build state functions. #[proc_macro_attribute] -pub fn autoserde(args: TokenStream, input: TokenStream) -> TokenStream { - let parsed = syn::parse_macro_input!(input as autoserde::AutoserdeFn); - let attr_args = syn::parse_macro_input!(args as syn::AttributeArgs); - // Parse macro arguments with `darling` - let args = match autoserde::AutoserdeArgs::from_list(&attr_args) { - Ok(v) => v, - Err(e) => { - return TokenStream::from(e.write_errors()); - } - }; +pub fn build_state(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); + + state_fns::state_fn_impl(parsed, StateFnType::BuildState).into() +} + +/// Annotates functions used for generating paths at build time to support automatic serialization/deserialization of app state and +/// client/server division. This supersedes the old `autoserde` macro for build paths functions. +#[proc_macro_attribute] +pub fn build_paths(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); + + state_fns::state_fn_impl(parsed, StateFnType::BuildPaths).into() +} + +/// Annotates functions used for generating global state at build time to support automatic serialization/deserialization of app state and +/// client/server division. This supersedes the old `autoserde` macro for global build state functions. +#[proc_macro_attribute] +pub fn global_build_state(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); - autoserde::autoserde_impl(parsed, args).into() + state_fns::state_fn_impl(parsed, StateFnType::GlobalBuildState).into() +} + +/// Annotates functions used for generating state at request time to support automatic serialization/deserialization of app state and +/// client/server division. This supersedes the old `autoserde` macro for request state functions. +#[proc_macro_attribute] +pub fn request_state(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); + + state_fns::state_fn_impl(parsed, StateFnType::RequestState).into() +} + +/// Annotates functions used for generating state at build time to support automatic serialization/deserialization of app state and +/// client/server division. This supersedes the old `autoserde` macro for build state functions. +#[proc_macro_attribute] +pub fn set_headers(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); + + state_fns::state_fn_impl(parsed, StateFnType::SetHeaders).into() +} + +/// Annotates functions used for amalgamating build-time and request-time states to support automatic serialization/deserialization of app state and +/// client/server division. This supersedes the old `autoserde` macro for state amalgamation functions. +#[proc_macro_attribute] +pub fn amalgamate_states(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); + + state_fns::state_fn_impl(parsed, StateFnType::AmalgamateStates).into() +} + +/// Annotates functions used for checking if a template should revalidate and request-time states to support automatic serialization/deserialization +/// of app state and client/server division. This supersedes the old `autoserde` macro for revalidation determination functions. +#[proc_macro_attribute] +pub fn should_revalidate(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as state_fns::StateFn); + + state_fns::state_fn_impl(parsed, StateFnType::ShouldRevalidate).into() } /// Labels a Sycamore component as a Perseus template, turning it into something that can be easily inserted into the `.template()` @@ -98,14 +139,65 @@ pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { test::test_impl(parsed, args).into() } -/// Marks the given function as the entrypoint into your app. You should only use this once in the `lib.rs` file of your project. +/// Marks the given function as the universal entrypoint into your app. This is designed for simple use-cases, and the annotated function should return +/// a `PerseusApp`. This will expand into separate `main()` functions for both the browser and engine sides. +/// +/// This should take an argument for the function that will produce your server. In most apps using this macro (which is designed for simple use-cases), +/// this will just be something like `perseus_warp::dflt_server` (with `perseus-warp` as a dependency with the `dflt-server` feature enabled). +/// +/// Note that the `dflt-engine` and `client-helpers` features must be enabled on `perseus` for this to work. (These are enabled by default.) +/// +/// Note further that you'll need to have `wasm-bindgen` as a dependency to use this. +#[proc_macro_attribute] +pub fn main(args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as entrypoint::MainFn); + let args = syn::parse_macro_input!(args as Path); + + entrypoint::main_impl(parsed, args).into() +} + +/// This is identical to `#[main]`, except it doesn't require a server integration, because it sets your app up for exporting only. This is useful for +/// apps not using server-requiring features (like incremental static generation and revalidation) that want to avoid bringing in another dependency on +/// the server-side. +#[proc_macro_attribute] +pub fn main_export(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as entrypoint::MainFn); + + entrypoint::main_export_impl(parsed).into() +} + +/// Marks the given function as the browser entrypoint into your app. This is designed for more complex apps that need to manually distinguish between +/// the engine and browser entrypoints. +/// +/// If you just want to run some simple customizations, you should probably use `perseus::run_client` to use the default client logic after you've made your +/// modifications. `perseus::ClientReturn` should be your return type no matter what. /// -/// Internally, this just normalizes the function's name so that Perseus can find it easily. +/// Note that any generics on the annotated function will not be preserved. You should put the `PerseusApp` generator in a separate function. +/// +/// Note further that you'll need to have `wasm-bindgen` as a dependency to use this. #[proc_macro_attribute] -pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream { +pub fn browser_main(_args: TokenStream, input: TokenStream) -> TokenStream { let parsed = syn::parse_macro_input!(input as entrypoint::MainFn); - entrypoint::main_impl(parsed).into() + entrypoint::browser_main_impl(parsed).into() +} + +/// Marks the given function as the engine entrypoint into your app. This is designed for more complex apps that need to manually distinguish between +/// the engine and browser entrypoints. +/// +/// If you just want to run some simple customizations, you should probably use `perseus::run_dflt_engine` with `perseus::builder::get_op` to use the default client logic +/// after you've made your modifications. You'll also want to return an exit code from this function (use `std::process:exit(..)`). +/// +/// Note that the `dflt-engine` and `client-helpers` features must be enabled on `perseus` for this to work. (These are enabled by default.) +/// +/// Note further that you'll need to have `tokio` as a dependency to use this. +/// +/// Finally, note that any generics on the annotated function will not be preserved. You should put the `PerseusApp` generator in a separate function. +#[proc_macro_attribute] +pub fn engine_main(_args: TokenStream, input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as entrypoint::EngineMainFn); + + entrypoint::engine_main_impl(parsed).into() } /// Processes the given `struct` to create a reactive version by wrapping each field in a `Signal`. This will generate a new `struct` with the given name and implement a `.make_rx()` @@ -182,3 +274,27 @@ pub fn make_rx(args: TokenStream, input: TokenStream) -> TokenStream { rx_state::make_rx_impl(parsed, name).into() } + +/// Marks the annotated code as only to be run as part of the engine (the server, the builder, the exporter, etc.). This resolves to a +/// target-gate that makes the annotated code run only on targets that are not `wasm32`. +#[proc_macro_attribute] +pub fn engine(_args: TokenStream, input: TokenStream) -> TokenStream { + let input_2: proc_macro2::TokenStream = input.into(); + quote! { + #[cfg(not(target_arch = "wasm32"))] + #input_2 + } + .into() +} + +/// Marks the annotated code as only to be run in the browser. This is the opposite of (and mutually exclusive with) `#[engine]`. This +/// resolves to a target-gate that makes the annotated code run only on targets that are `wasm32`. +#[proc_macro_attribute] +pub fn browser(_args: TokenStream, input: TokenStream) -> TokenStream { + let input_2: proc_macro2::TokenStream = input.into(); + quote! { + #[cfg(target_arch = "wasm32")] + #input_2 + } + .into() +} diff --git a/packages/perseus-macro/src/autoserde.rs b/packages/perseus-macro/src/state_fns.rs similarity index 65% rename from packages/perseus-macro/src/autoserde.rs rename to packages/perseus-macro/src/state_fns.rs index bca5617669..4734bfbf9f 100644 --- a/packages/perseus-macro/src/autoserde.rs +++ b/packages/perseus-macro/src/state_fns.rs @@ -1,4 +1,5 @@ -use darling::FromMeta; +// This file contains all the macros that supersede `autoserde` + use proc_macro2::TokenStream; use quote::quote; use syn::parse::{Parse, ParseStream}; @@ -7,24 +8,8 @@ use syn::{ Result, ReturnType, Type, Visibility, }; -/// The arguments that the `autoserde` annotation takes. -// TODO prevent the user from providing more than one of these -#[derive(Debug, FromMeta, PartialEq, Eq)] -pub struct AutoserdeArgs { - #[darling(default)] - build_state: bool, - #[darling(default)] - request_state: bool, - #[darling(default)] - set_headers: bool, - #[darling(default)] - amalgamate_states: bool, - #[darling(default)] - global_build_state: bool, -} - /// A function that can be wrapped in the Perseus test sub-harness. -pub struct AutoserdeFn { +pub struct StateFn { /// The body of the function. pub block: Box, /// The arguments that the function takes. We don't need to modify these because we wrap them with a functin that does serializing/ @@ -41,7 +26,7 @@ pub struct AutoserdeFn { /// Any generics the function takes (shouldn't be any, but it's possible). pub generics: Generics, } -impl Parse for AutoserdeFn { +impl Parse for StateFn { fn parse(input: ParseStream) -> Result { let parsed: Item = input.parse()?; @@ -97,8 +82,20 @@ impl Parse for AutoserdeFn { } } -pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream { - let AutoserdeFn { +/// The different types of state functions. +pub enum StateFnType { + BuildState, + BuildPaths, + RequestState, + SetHeaders, + AmalgamateStates, + GlobalBuildState, + ShouldRevalidate, +} + +// We just use a single implementation function for ease, but there's a separate macro for each type of state function +pub fn state_fn_impl(input: StateFn, fn_type: StateFnType) -> TokenStream { + let StateFn { block, args, generics, @@ -108,9 +105,12 @@ pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream return_type, } = input; - if fn_type.build_state { - // This will always be asynchronous - quote! { + match fn_type { + StateFnType::BuildState => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis async fn #name(path: ::std::string::String, locale: ::std::string::String) -> ::perseus::RenderFnResultWithCause<::std::string::String> { // The user's function // We can assume the return type to be `RenderFnResultWithCause` @@ -125,10 +125,24 @@ pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream let build_state_with_str = build_state.map(|val| ::serde_json::to_string(&val).unwrap()); build_state_with_str } - } - } else if fn_type.request_state { - // This will always be asynchronous - quote! { + }, + // This one only exists to appease the server-side/client-side division + StateFnType::BuildPaths => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + // This normal version is identical to the user's (we know it won't have any arguments, and we know its return type) + // We use the user's return type to prevent unused imports warnings in their code + #[cfg(not(target_arch = "wasm32"))] + #vis async fn #name() -> #return_type { + #block + } + }, + StateFnType::RequestState => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis async fn #name(path: ::std::string::String, locale: ::std::string::String, req: ::perseus::Request) -> ::perseus::RenderFnResultWithCause<::std::string::String> { // The user's function // We can assume the return type to be `RenderFnResultWithCause` @@ -143,10 +157,13 @@ pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream let req_state_with_str = req_state.map(|val| ::serde_json::to_string(&val).unwrap()); req_state_with_str } - } - } else if fn_type.set_headers { - // This will always be synchronous - quote! { + }, + // Always synchronous + StateFnType::SetHeaders => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis fn #name(props: ::std::option::Option<::std::string::String>) -> ::perseus::http::header::HeaderMap { // The user's function // We can assume the return type to be `HeaderMap` @@ -158,10 +175,13 @@ pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream let props_de = props.map(|val| ::serde_json::from_str(&val).unwrap()); #name(props_de) } - } - } else if fn_type.amalgamate_states { - // This will always be synchronous - quote! { + }, + // Always synchronous + StateFnType::AmalgamateStates => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis fn #name(states: ::perseus::States) -> ::perseus::RenderFnResultWithCause<::std::option::Option<::std::string::String>> { // The user's function // We can assume the return type to be `RenderFnResultWithCause>` @@ -176,9 +196,12 @@ pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream let amalgamated_state_with_str = amalgamated_state.map(|val| val.map(|val| ::serde_json::to_string(&val).unwrap())); amalgamated_state_with_str } - } - } else if fn_type.global_build_state { - quote! { + }, + StateFnType::GlobalBuildState => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + #[cfg(not(target_arch = "wasm32"))] #vis async fn #name() -> ::perseus::RenderFnResult<::std::string::String> { // The user's function // We can assume the return type to be `RenderFnResultWithCause` @@ -193,10 +216,18 @@ pub fn autoserde_impl(input: AutoserdeFn, fn_type: AutoserdeArgs) -> TokenStream let build_state_with_str = build_state.map(|val| ::serde_json::to_string(&val).unwrap()); build_state_with_str } - } - } else { - quote! { - compile_error!("function type not supported, must be one of: `build_state`, `request_state`, `set_headers`, `amalgamate_states`, or `global_build_state`") - } + }, + // This one only exists to appease the server-side/client-side division + StateFnType::ShouldRevalidate => quote! { + // We create a normal version of the function and one to appease the handlers in Wasm (which expect functions that take no arguments, etc.) + #[cfg(target_arch = "wasm32")] + #vis fn #name() {} + // This normal version is identical to the user's (we know it won't have any arguments, and we know its return type) + // We use the user's return type to prevent unused imports warnings in their code + #[cfg(not(target_arch = "wasm32"))] + #vis async fn #name() -> #return_type { + #block + } + }, } } diff --git a/packages/perseus-macro/src/template.rs b/packages/perseus-macro/src/template.rs index bd38da3b02..1297c6a9f0 100644 --- a/packages/perseus-macro/src/template.rs +++ b/packages/perseus-macro/src/template.rs @@ -162,7 +162,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { #block } - #component_name(cx, ()) + #component_name(cx) } } } diff --git a/packages/perseus-macro/src/template_rx.rs b/packages/perseus-macro/src/template_rx.rs index 2963692406..2ee9537f68 100644 --- a/packages/perseus-macro/src/template_rx.rs +++ b/packages/perseus-macro/src/template_rx.rs @@ -125,15 +125,15 @@ fn make_mid(ty: &Type) -> Type { /// Gets the code fragment used to support live reloading and HSR. // This is also used by the normal `#[template(...)]` macro pub fn get_live_reload_frag() -> TokenStream { - #[cfg(all(feature = "hsr", debug_assertions))] + #[cfg(all(feature = "hsr", debug_assertions, target_arch = "wasm32"))] let hsr_frag = quote! { ::perseus::state::hsr_freeze(frozen_state).await; }; - #[cfg(not(all(feature = "hsr", debug_assertions)))] + #[cfg(not(all(feature = "hsr", debug_assertions, target_arch = "wasm32")))] #[allow(unused_variables)] let hsr_frag = quote!(); - #[cfg(all(feature = "live-reload", debug_assertions))] + #[cfg(all(feature = "live-reload", debug_assertions, target_arch = "wasm32"))] let live_reload_frag = quote! {{ use ::perseus::state::Freeze; let render_ctx = ::perseus::get_render_ctx!(cx); @@ -157,7 +157,7 @@ pub fn get_live_reload_frag() -> TokenStream { } }); }}; - #[cfg(not(all(feature = "live-reload", debug_assertions)))] + #[cfg(not(all(feature = "live-reload", debug_assertions, target_arch = "wasm32")))] let live_reload_frag = quote!(); live_reload_frag @@ -165,7 +165,7 @@ pub fn get_live_reload_frag() -> TokenStream { /// Gets the code fragment used to support HSR thawing. This MUST be prefixed by a `#[cfg(target_arch = "wasm32")]`. pub fn get_hsr_thaw_frag() -> TokenStream { - #[cfg(all(feature = "hsr", debug_assertions))] + #[cfg(all(feature = "hsr", debug_assertions, target_arch = "wasm32"))] let hsr_thaw_frag = quote! {{ let render_ctx = ::perseus::get_render_ctx!(cx); ::perseus::spawn_local_scoped(cx, async move { @@ -178,7 +178,7 @@ pub fn get_hsr_thaw_frag() -> TokenStream { }); }}; // If HSR is disabled, there'll still be a Wasm-gate, which means we have to give it something to gate (or it'll gate the code after it, which is very bad!) - #[cfg(not(all(feature = "hsr", debug_assertions)))] + #[cfg(not(all(feature = "hsr", debug_assertions, target_arch = "wasm32")))] let hsr_thaw_frag = quote!({}); hsr_thaw_frag @@ -261,7 +261,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { #block } - #component_name(cx, ()) + #component_name(cx) } }, // This template takes its own state and global state @@ -389,7 +389,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream { #block } - #component_name(cx, ()) + #component_name(cx) } } } else { diff --git a/packages/perseus-warp/Cargo.toml b/packages/perseus-warp/Cargo.toml index c93415843c..00447bd777 100644 --- a/packages/perseus-warp/Cargo.toml +++ b/packages/perseus-warp/Cargo.toml @@ -24,3 +24,7 @@ thiserror = "1" fmterr = "0.1" futures = "0.3" sycamore = { version = "=0.8.0-beta.6", features = ["ssr"] } + +[features] +# Enables the default server configuration, which provides a convenience function if you're not adding any extra routes +dflt-server = [ "perseus/builder" ] diff --git a/packages/perseus-warp/src/dflt_server.rs b/packages/perseus-warp/src/dflt_server.rs new file mode 100644 index 0000000000..33c0a483c3 --- /dev/null +++ b/packages/perseus-warp/src/dflt_server.rs @@ -0,0 +1,24 @@ +use crate::perseus_routes; +use futures::executor::block_on; +use perseus::{ + builder::{get_host_and_port, get_props, get_standalone_and_act}, + internal::i18n::TranslationsManager, + stores::MutableStore, + PerseusAppBase, SsrNode, +}; +use std::net::SocketAddr; + +/// Creates and starts the default Perseus server with Warp. This should be run in a `main` function annotated with `#[tokio::main]` (which requires the `macros` and +/// `rt-multi-thread` features on the `tokio` dependency). +pub async fn dflt_server( + app: impl Fn() -> PerseusAppBase + 'static + Send + Sync + Clone, +) { + get_standalone_and_act(); + let props = get_props(app()); + let (host, port) = get_host_and_port(); + let addr: SocketAddr = format!("{}:{}", host, port) + .parse() + .expect("Invalid address provided to bind to."); + let routes = block_on(perseus_routes(props)); + warp::serve(routes).run(addr).await; +} diff --git a/packages/perseus-warp/src/lib.rs b/packages/perseus-warp/src/lib.rs index b2bdd1f506..c7e2828d77 100644 --- a/packages/perseus-warp/src/lib.rs +++ b/packages/perseus-warp/src/lib.rs @@ -9,6 +9,8 @@ documentation, and this should mostly be used as a secondary reference source. Y #![deny(missing_docs)] mod conv_req; +#[cfg(feature = "dflt-server")] +mod dflt_server; mod initial_load; mod page_data; mod perseus_routes; @@ -16,4 +18,6 @@ mod static_content; mod translations; pub use crate::perseus_routes::perseus_routes; +#[cfg(feature = "dflt-server")] +pub use dflt_server::dflt_server; pub use perseus::internal::serve::ServerOptions; diff --git a/packages/perseus/Cargo.toml b/packages/perseus/Cargo.toml index 2f0218cf5a..f3eceda308 100644 --- a/packages/perseus/Cargo.toml +++ b/packages/perseus/Cargo.toml @@ -17,40 +17,49 @@ categories = ["wasm", "web-programming", "development-tools", "asynchronous", "g sycamore = { version = "=0.8.0-beta.6", features = [ "ssr" ] } sycamore-router = "=0.8.0-beta.6" sycamore-futures = "=0.8.0-beta.6" -perseus-macro = { path = "../perseus-macro", version = "0.4.0-beta.1" } -# TODO review feature flags here -web-sys = { version = "0.3", features = [ "Headers", "Navigator", "NodeList", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window" ] } -wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4" +perseus-macro = { path = "../perseus-macro", version = "0.4.0-beta.1", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "1" -fmterr = "0.1" -futures = "0.3" -urlencoding = "2.1" -chrono = "0.4" -http = "0.2" async-trait = "0.1" +futures = "0.3" +fmterr = "0.1" fluent-bundle = { version = "0.15", optional = true } unic-langid = { version = "0.9", optional = true } intl-memoizer = { version = "0.5", optional = true } -tokio = { version = "1", features = [ "fs", "io-util" ] } -rexie = { version = "0.2", optional = true } -js-sys = { version = "0.3", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] regex = "1" +tokio = { version = "1", features = [ "fs", "io-util" ] } +fs_extra = { version = "1", optional = true } +http = "0.2" +urlencoding = "2.1" +chrono = "0.4" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +rexie = { version = "0.2", optional = true } +js-sys = { version = "0.3", optional = true } +console_error_panic_hook = { version = "0.1.6", optional = true } +# TODO review feature flags here +web-sys = { version = "0.3", features = [ "Headers", "Navigator", "NodeList", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window" ] } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" [features] # Live reloading will only take effect in development, and won't impact production # BUG This adds 1.9kB to the production bundle (that's without size optimizations though) -default = [ "live-reload", "hsr" ] +default = [ "live-reload", "hsr", "builder", "client-helpers", "macros", "dflt-engine" ] translator-fluent = ["fluent-bundle", "unic-langid", "intl-memoizer"] +# This feature adds support for a number of macros that will make your life MUCH easier (read: use this unless you have very specific needs or are completely insane) +macros = [ "perseus-macro" ] # This feature makes tinker-only plugins be registered (this flag is enabled internally in the engine) tinker-plugins = [] -# This feature enables server-side-only features, which should be used on both the server and in the builder -# This prevents leakage of server-side code -server-side = [] +# This feature enables code required only in the builder systems on the server-side. +builder = [ "fs_extra" ] +# This feature enable support for functions that make using the default engine configuration much easier. +dflt-engine = [ "builder" ] +# This features enables client-side helpers designed to be run in the browser. +client-helpers = [ "console_error_panic_hook" ] # This feature changes a few defaults so that Perseus works seemlessly when deployed with the `.perseus/` structure (this is activated automatically by `perseus deploy`, and should not be invoked manually!) standalone = [] # This feature enables Sycamore hydration by default (Sycamore hydration feature is always activated though) diff --git a/packages/perseus/src/client.rs b/packages/perseus/src/client.rs new file mode 100644 index 0000000000..e56284d9db --- /dev/null +++ b/packages/perseus/src/client.rs @@ -0,0 +1,59 @@ +use crate::{ + checkpoint, + internal::router::{perseus_router, PerseusRouterProps}, + plugins::PluginAction, + shell::get_render_cfg, + templates::TemplateNodeType, +}; +use wasm_bindgen::JsValue; + +use crate::{i18n::TranslationsManager, stores::MutableStore, PerseusAppBase}; + +/// The entrypoint into the app itself. This will be compiled to Wasm and actually executed, rendering the rest of the app. +/// Runs the app in the browser on the client-side. This is designed to be executed in a function annotated with `#[wasm_bindgen]`. +/// +/// This is entirely engine-agnostic, using only the properties from the given `PerseusApp`. +/// +/// For consistency with `run_dflt_engine`, this takes a function that returns the `PerseusApp`. +pub fn run_client( + app: impl Fn() -> PerseusAppBase, +) -> Result<(), JsValue> { + let app = app(); + let plugins = app.get_plugins(); + + checkpoint("begin"); + // Panics should always go to the console + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + + plugins + .functional_actions + .client_actions + .start + .run((), plugins.get_plugin_data()); + checkpoint("initial_plugins_complete"); + + // Get the root we'll be injecting the router into + let root = web_sys::window() + .unwrap() + .document() + .unwrap() + .query_selector(&format!("#{}", app.get_root())) + .unwrap() + .unwrap(); + + // Set up the properties we'll pass to the router + let router_props = PerseusRouterProps { + locales: app.get_locales(), + error_pages: app.get_error_pages(), + templates: app.get_templates_map(), + render_cfg: get_render_cfg().expect("render configuration invalid or not injected"), + }; + + // This top-level context is what we use for everything, allowing page state to be registered and stored for the lifetime of the app + sycamore::render_to(move |cx| perseus_router(cx, router_props), &root); + + Ok(()) +} + +/// A convenience type wrapper for the type returned by nearly all client-side entrypoints. +pub type ClientReturn = Result<(), JsValue>; diff --git a/examples/core/basic/.perseus/builder/src/bin/build.rs b/packages/perseus/src/engine/build.rs similarity index 64% rename from examples/core/basic/.perseus/builder/src/bin/build.rs rename to packages/perseus/src/engine/build.rs index feb781c063..22d7c4717c 100644 --- a/examples/core/basic/.perseus/builder/src/bin/build.rs +++ b/packages/perseus/src/engine/build.rs @@ -1,20 +1,18 @@ -use fmterr::fmt_err; -use perseus::{ - internal::build::{build_app, BuildProps}, - PluginAction, SsrNode, +use crate::build::{build_app, BuildProps}; +use crate::{ + errors::{EngineError, ServerError}, + i18n::TranslationsManager, + stores::MutableStore, + PerseusAppBase, PluginAction, SsrNode, }; -use perseus_engine as app; +use std::rc::Rc; -#[tokio::main] -async fn main() { - let exit_code = real_main().await; - std::process::exit(exit_code) -} - -async fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - std::env::set_current_dir("../").unwrap(); - let app = app::main::(); +/// Builds the app, calling all necessary plugin opportunities. This works solely with the properties provided in the given `PerseusApp`, so this is entirely engine-agnostic. +/// +/// Note that this expects to be run in the root of the project. +pub async fn build( + app: PerseusAppBase, +) -> Result<(), Rc> { let plugins = app.get_plugins(); plugins @@ -31,14 +29,13 @@ async fn real_main() -> i32 { let global_state = match gsc.get_build_state().await { Ok(global_state) => global_state, Err(err) => { - let err_msg = fmt_err(&err); + let err: Rc = Rc::new(ServerError::GlobalStateError(err).into()); plugins .functional_actions .build_actions .after_failed_global_state_creation - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } }; @@ -60,21 +57,21 @@ async fn real_main() -> i32 { }) .await; if let Err(err) = res { - let err_msg = fmt_err(&err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .build_actions .after_failed_build - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - 1 + .run(err.clone(), plugins.get_plugin_data()); + + Err(err) } else { plugins .functional_actions .build_actions .after_successful_build .run((), plugins.get_plugin_data()); - println!("Static generation successfully completed!"); - 0 + + Ok(()) } } diff --git a/packages/perseus/src/engine/dflt_engine.rs b/packages/perseus/src/engine/dflt_engine.rs new file mode 100644 index 0000000000..f38cd0632a --- /dev/null +++ b/packages/perseus/src/engine/dflt_engine.rs @@ -0,0 +1,104 @@ +// This file contains functions exclusive to the default engine systems + +use super::EngineOperation; +use crate::{i18n::TranslationsManager, stores::MutableStore, PerseusAppBase, SsrNode}; +use fmterr::fmt_err; +use futures::Future; +use std::env; + +/// A wrapper around `run_dflt_engine` for apps that only use exporting, and so don't need to bring in a server integration. This is designed to avoid extra +/// dependencies. If `perseus serve` is called on an app using this, it will `panic!` after building everything. +pub async fn run_dflt_engine_export_only(op: EngineOperation, app: A) -> i32 +where + M: MutableStore, + T: TranslationsManager, + A: Fn() -> PerseusAppBase + 'static + Send + Sync + Clone, +{ + let serve_fn = |_app: A| async { + panic!("`run_dflt_engine_export_only` cannot run a server; you should use `run_dflt_engine` instead and import a server integration (e.g. `perseus-warp`)") + }; + run_dflt_engine(op, app, serve_fn).await +} + +/// A convenience function that automatically runs the necessary engine operation based on the given directive. This provides almost no options for customization, and is +/// usually elided by a macro. More advanced use-cases should bypass this and call the functions this calls manually, with their own configurations. +/// +/// The third argument to this is a function to produce a server. In simple cases, this will be the `dflt_server` export from your server integration of choice (which is +/// assumed to use a Tokio 1.x runtime). This function must be infallible (any errors should be panics, as they *will* be treated as unrecoverable). +/// +/// If the action is to export a single error page, the HTTP status code of the error page to export and the output will be read as the first and second arguments +/// to the binary invocation. If this is not the desired behavior, you should handle the `EngineOperation::ExportErrorPage` case manually. +/// +/// This returns an exit code, which should be returned from the process. Any handled errors will be printed to the console. +pub async fn run_dflt_engine( + op: EngineOperation, + app: A, + serve_fn: impl Fn(A) -> F, +) -> i32 +where + M: MutableStore, + T: TranslationsManager, + F: Future, + A: Fn() -> PerseusAppBase + 'static + Send + Sync + Clone, +{ + match op { + EngineOperation::Build => match super::engine_build(app()).await { + Ok(_) => 0, + Err(err) => { + eprintln!("{}", fmt_err(&*err)); + 1 + } + }, + EngineOperation::Export => match super::engine_export(app()).await { + Ok(_) => 0, + Err(err) => { + eprintln!("{}", fmt_err(&*err)); + 1 + } + }, + EngineOperation::ExportErrorPage => { + // Get the HTTP status code to build from the arguments to this executable + // We print errors directly here because we can, and because this behavior is unique to the default engine + let args = env::args().collect::>(); + let code = match args.get(1) { + Some(arg) => { + match arg.parse::() { + Ok(err_code) => err_code, + Err(_) => { + eprintln!("HTTP status code for error page exporting must be a valid integer."); + return 1; + } + } + } + None => { + eprintln!("Error page exporting requires an HTTP status code for which to export the error page."); + return 1; + } + }; + // Get the output to write to from the second argument + let output = match args.get(2) { + Some(output) => output, + None => { + eprintln!("Error page exporting requires an output location."); + return 1; + } + }; + match super::engine_export_error_page(app(), code, output).await { + Ok(_) => 0, + Err(err) => { + eprintln!("{}", fmt_err(&*err)); + 1 + } + } + } + EngineOperation::Serve => { + serve_fn(app).await; + 0 + } + EngineOperation::Tinker => { + // This is infallible (though plugins could panic) + super::engine_tinker(app()); + 0 + } + } +} diff --git a/examples/core/basic/.perseus/builder/src/bin/export.rs b/packages/perseus/src/engine/export.rs similarity index 51% rename from examples/core/basic/.perseus/builder/src/bin/export.rs rename to packages/perseus/src/engine/export.rs index 8020744f74..133169d6fb 100644 --- a/examples/core/basic/.perseus/builder/src/bin/export.rs +++ b/packages/perseus/src/engine/export.rs @@ -1,53 +1,47 @@ -use fmterr::fmt_err; +use crate::build::{build_app, BuildProps}; +use crate::errors::ServerError; +use crate::export::{export_app, ExportProps}; +use crate::{internal::get_path_prefix_server, PerseusApp, PluginAction, Plugins, SsrNode}; use fs_extra::dir::{copy as copy_dir, CopyOptions}; -use perseus::{ - internal::{ - build::{build_app, BuildProps}, - export::{export_app, ExportProps}, - get_path_prefix_server, - }, - PerseusApp, PluginAction, SsrNode, -}; -use perseus_engine as app; +use std::collections::HashMap; use std::fs; use std::path::PathBuf; +use std::rc::Rc; -#[tokio::main] -async fn main() { - let exit_code = real_main().await; - std::process::exit(exit_code) -} - -async fn real_main() -> i32 { - // We want to be working in the root of `.perseus/` - std::env::set_current_dir("../").unwrap(); - let app = app::main::(); +use crate::errors::*; +use crate::{i18n::TranslationsManager, stores::MutableStore, PerseusAppBase}; +/// Exports the app to static files, given a `PerseusApp`. This is engine-agnostic, using the `exported` subfolder in the immutable store as a destination directory. By default +/// this will end up at `dist/exported/` (customizable through `PerseusApp`). +/// +/// Note that this expects to be run in the root of the project. +pub async fn export( + app: PerseusAppBase, +) -> Result<(), Rc> { let plugins = app.get_plugins(); + let static_aliases = app.get_static_aliases(); + // This won't have any trailing slashes (they're stripped by the immutable store initializer) + let dest = format!("{}/exported", app.get_immutable_store().get_path()); + let static_dir = app.get_static_dir(); - // Building and exporting must be sequential, but that can be done in parallel with static directory/alias copying - let exit_code = build_and_export().await; - if exit_code != 0 { - return exit_code; - } + build_and_export(app).await?; // After that's done, we can do two copy operations in parallel at least - let exit_code_1 = tokio::task::spawn_blocking(copy_static_dir); - let exit_code_2 = tokio::task::spawn_blocking(copy_static_aliases); - // These errors come from any panics in the threads, which should be propagated up to a panic in the main thread in this case - exit_code_1.await.unwrap(); - exit_code_2.await.unwrap(); + copy_static_aliases(&plugins, &static_aliases, &dest)?; + copy_static_dir(&plugins, &static_dir, &dest)?; plugins .functional_actions .export_actions .after_successful_export .run((), plugins.get_plugin_data()); - println!("Static exporting successfully completed!"); - 0 + + Ok(()) } -async fn build_and_export() -> i32 { - let app = app::main::(); +/// Performs the building and exporting processes using the given app. This is fully engine-agnostic, using only the data provided in the given `PerseusApp`. +async fn build_and_export( + app: PerseusAppBase, +) -> Result<(), Rc> { let plugins = app.get_plugins(); plugins @@ -65,14 +59,13 @@ async fn build_and_export() -> i32 { let global_state = match gsc.get_build_state().await { Ok(global_state) => global_state, Err(err) => { - let err_msg = fmt_err(&err); + let err: Rc = Rc::new(ServerError::GlobalStateError(err).into()); plugins .functional_actions .export_actions .after_failed_global_state_creation - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } }; let templates_map = app.get_templates_map(); @@ -94,14 +87,13 @@ async fn build_and_export() -> i32 { }) .await; if let Err(err) = build_res { - let err_msg = fmt_err(&err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_build - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } plugins .functional_actions @@ -124,84 +116,100 @@ async fn build_and_export() -> i32 { }) .await; if let Err(err) = export_res { - let err_msg = fmt_err(&err); + let err: Rc = Rc::new(err.into()); plugins .functional_actions .export_actions .after_failed_export - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } - 0 + Ok(()) } -fn copy_static_dir() -> i32 { - let app = app::main::(); - let plugins = app.get_plugins(); +/// Copies the static aliases into a distribution directory at `dest` (no trailing `/`). This should be the root of the destination directory for the exported files. +/// Because this provides a customizable destination, it is fully engine-agnostic. +/// +/// The error type here is a tuple of the location the asset was copied from, the location it was copied to, and the error in that process (which could be from `io` or +/// `fs_extra`). +fn copy_static_aliases( + plugins: &Plugins, + static_aliases: &HashMap, + dest: &str, +) -> Result<(), Rc> { // Loop through any static aliases and copy them in too // Unlike with the server, these could override pages! // We'll copy from the alias to the path (it could be a directory or a file) // Remember: `alias` has a leading `/`! - for (alias, path) in app.get_static_aliases() { + for (alias, path) in static_aliases { let from = PathBuf::from(path); - let to = format!("dist/exported{}", alias); + let to = format!("{}{}", dest, alias); if from.is_dir() { if let Err(err) = copy_dir(&from, &to, &CopyOptions::new()) { - let err_msg = format!( - "couldn't copy static alias directory from '{}' to '{}': '{}'", - from.to_str().map(|s| s.to_string()).unwrap(), + let err = EngineError::CopyStaticAliasDirErr { + source: err, to, - fmt_err(&err) - ); + from: path.to_string(), + }; + let err = Rc::new(err); plugins .functional_actions .export_actions .after_failed_static_alias_dir_copy - .run(err.to_string(), plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } } else if let Err(err) = fs::copy(&from, &to) { - let err_msg = format!( - "couldn't copy static alias file from '{}' to '{}': '{}'", - from.to_str().map(|s| s.to_string()).unwrap(), + let err = EngineError::CopyStaticAliasFileError { + source: err, to, - fmt_err(&err) - ); + from: path.to_string(), + }; + let err = Rc::new(err); plugins .functional_actions .export_actions .after_failed_static_alias_file_copy - .run(err, plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } } - 0 + Ok(()) } -fn copy_static_aliases() -> i32 { - let app = app::main::(); - let plugins = app.get_plugins(); +/// Copies the directory containing static data to be put in `/.perseus/static/` (URL). This takes in both the location of the static directory and the destination +/// directory for exported files. +fn copy_static_dir( + plugins: &Plugins, + static_dir_raw: &str, + dest: &str, +) -> Result<(), Rc> { // Copy the `static` directory into the export package if it exists // If the user wants extra, they can use static aliases, plugins are unnecessary here - let static_dir = PathBuf::from("../static"); + let static_dir = PathBuf::from(static_dir_raw); if static_dir.exists() { - if let Err(err) = copy_dir(&static_dir, "dist/exported/.perseus/", &CopyOptions::new()) { - let err_msg = format!("couldn't copy static directory: '{}'", fmt_err(&err)); + if let Err(err) = copy_dir( + &static_dir, + format!("{}/.perseus/", dest), + &CopyOptions::new(), + ) { + let err = EngineError::CopyStaticDirError { + source: err, + path: static_dir_raw.to_string(), + dest: dest.to_string(), + }; + let err = Rc::new(err); plugins .functional_actions .export_actions .after_failed_static_copy - .run(err.to_string(), plugins.get_plugin_data()); - eprintln!("{}", err_msg); - return 1; + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); } } - 0 + Ok(()) } diff --git a/packages/perseus/src/engine/export_error_page.rs b/packages/perseus/src/engine/export_error_page.rs new file mode 100644 index 0000000000..a561ef9c6a --- /dev/null +++ b/packages/perseus/src/engine/export_error_page.rs @@ -0,0 +1,64 @@ +use crate::{ + errors::EngineError, i18n::TranslationsManager, internal::serve::build_error_page, + stores::MutableStore, PerseusApp, PerseusAppBase, PluginAction, SsrNode, +}; +use std::{fs, rc::Rc}; + +/// Exports a single error page for the given HTTP status code to the given output location. If the status code doesn't exist or isn't handled, then the fallback page will be +/// exported. +/// +/// This expects to run in the root of the project. +/// +/// This can only return IO errors from failures to write to the given output location. (Wrapped in an `Rc` so they can be sent to plugins as well.) +pub async fn export_error_page( + app: PerseusAppBase, + code: u16, + output: &str, +) -> Result<(), Rc> { + let plugins = app.get_plugins(); + + let error_pages = app.get_error_pages(); + // Prepare the HTML shell + let index_view_str = app.get_index_view_str(); + let root_id = app.get_root(); + let immutable_store = app.get_immutable_store(); + // We assume the app has already been built before running this (so the render config must be available) + // It doesn't matter if the type parameters here are wrong, this function doesn't use them + let html_shell = + PerseusApp::get_html_shell(index_view_str, &root_id, &immutable_store, &plugins).await; + + plugins + .functional_actions + .export_error_page_actions + .before_export_error_page + .run((code, output.to_string()), plugins.get_plugin_data()); + + // Build that error page as the server does + let err_page_str = build_error_page("", code, "", None, &error_pages, &html_shell); + + // Write that to the given output location + match fs::write(&output, err_page_str) { + Ok(_) => (), + Err(err) => { + let err = EngineError::WriteErrorPageError { + source: err, + dest: output.to_string(), + }; + let err = Rc::new(err); + plugins + .functional_actions + .export_error_page_actions + .after_failed_write + .run(err.clone(), plugins.get_plugin_data()); + return Err(err); + } + }; + + plugins + .functional_actions + .export_error_page_actions + .after_successful_export_error_page + .run((), plugins.get_plugin_data()); + + Ok(()) +} diff --git a/packages/perseus/src/engine/get_op.rs b/packages/perseus/src/engine/get_op.rs new file mode 100644 index 0000000000..57c4dbe618 --- /dev/null +++ b/packages/perseus/src/engine/get_op.rs @@ -0,0 +1,50 @@ +use std::env; + +/// Determines the engine operation to be performed by examining environment variables (set automatically by the CLI as appropriate). +pub fn get_op() -> Option { + let var = match env::var("PERSEUS_ENGINE_OPERATION").ok() { + Some(var) => var, + None => { + return { + // The only typical use of a release-built binary is as a server, in which case we shouldn't need to specify this environment variable + // So, in production, we take the server as the default + // If a user wants a builder though, they can just set the environment variable + // TODO Document this! + if cfg!(debug_assertions) { + None + } else { + Some(EngineOperation::Serve) + } + }; + } + }; + + match var.as_str() { + "serve" => Some(EngineOperation::Serve), + "build" => Some(EngineOperation::Build), + "export" => Some(EngineOperation::Export), + "export_error_page" => Some(EngineOperation::ExportErrorPage), + "tinker" => Some(EngineOperation::Tinker), + _ => { + if cfg!(debug_assertions) { + None + } else { + Some(EngineOperation::Serve) + } + } + } +} + +/// A representation of the server-side engine operations that can be performed. +pub enum EngineOperation { + /// Run the server for the app. This assumes the app has already been built. + Serve, + /// Build the app. This process involves statically generating HTML and the like to be sent to the client. + Build, + /// Export the app by building it and also creating a file layout suitable for static file serving. + Export, + /// Export a single error page to a single file. + ExportErrorPage, + /// Run the tinker plugin actions. + Tinker, +} diff --git a/packages/perseus/src/engine/mod.rs b/packages/perseus/src/engine/mod.rs new file mode 100644 index 0000000000..4afb9b2b33 --- /dev/null +++ b/packages/perseus/src/engine/mod.rs @@ -0,0 +1,19 @@ +mod build; +mod export; +mod export_error_page; +mod tinker; +pub use build::build as engine_build; +pub use export::export as engine_export; +pub use export_error_page::export_error_page as engine_export_error_page; +pub use tinker::tinker as engine_tinker; + +#[cfg(feature = "dflt-engine")] +mod dflt_engine; +#[cfg(feature = "dflt-engine")] +pub use dflt_engine::{run_dflt_engine, run_dflt_engine_export_only}; + +mod get_op; +pub use get_op::{get_op, EngineOperation}; + +mod serve; +pub use serve::{get_host_and_port, get_props, get_standalone_and_act}; diff --git a/packages/perseus/src/engine/serve.rs b/packages/perseus/src/engine/serve.rs new file mode 100644 index 0000000000..f34ec7f8c3 --- /dev/null +++ b/packages/perseus/src/engine/serve.rs @@ -0,0 +1,103 @@ +use crate::i18n::TranslationsManager; +use crate::plugins::PluginAction; +use crate::server::{ServerOptions, ServerProps}; +use crate::stores::MutableStore; +use crate::PerseusAppBase; +use futures::executor::block_on; +use std::env; +use std::fs; +use sycamore::web::SsrNode; + +// TODO Can we unify the two modes of server execution now? +// This server executable can be run in two modes: +// dev: at the root of the project, works with that file structure +// prod: as a standalone executable with a `dist/` directory as a sibling (also present with the dev file structure) + +// Note: the default servers for integrations are now stored in the crates of those integrations + +/// Determines whether or not we're operating in standalone mode, and acts accordingly. This MUST be executed in the parent thread, as it switches the current directory. +pub fn get_standalone_and_act() -> bool { + // So we don't have to define a different `FsConfigManager` just for the server, we shift the execution context to the same level as everything else + // The server has to be a separate crate because otherwise the dependencies don't work with Wasm bundling + // If we're not running as a standalone binary, assume we're running in dev mode at the root of the user's project + if cfg!(feature = "standalone") { + // If we are running as a standalone binary, we have no idea where we're being executed from (#63), so we should set the working directory to be the same as the binary location + let binary_loc = env::current_exe().unwrap(); + let binary_dir = binary_loc.parent().unwrap(); // It's a file, there's going to be a parent if we're working on anything close to sanity + env::set_current_dir(binary_dir).unwrap(); + true + } else { + false + } +} + +/// Gets the host and port to serve on based on environment variables, which are universally used for configuration regardless of engine. +pub fn get_host_and_port() -> (String, u16) { + // We have to use two sets of environment variables until v0.4.0 + let host = env::var("PERSEUS_HOST"); + let port = env::var("PERSEUS_PORT"); + + let host = host.unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = port + .unwrap_or_else(|_| "8080".to_string()) + .parse::() + .expect("Port must be a number."); + + (host, port) +} + +/// Gets the properties to pass to the server, invoking plugin opportunities as necessary. This is entirely engine-agnostic. +pub fn get_props( + app: PerseusAppBase, +) -> ServerProps { + let plugins = app.get_plugins(); + + plugins + .functional_actions + .server_actions + .before_serve + .run((), plugins.get_plugin_data()); + + let static_dir_path = app.get_static_dir(); + + let app_root = app.get_root(); + let immutable_store = app.get_immutable_store(); + let index_view_str = app.get_index_view_str(); + // By the time this binary is being run, the app has already been built be the CLI (hopefully!), so we can depend on access to the render config + let index_view = block_on(PerseusAppBase::::get_html_shell( + index_view_str, + &app_root, + &immutable_store, + &plugins, + )); + + let opts = ServerOptions { + // We don't support setting some attributes from `wasm-pack` through plugins/`PerseusApp` because that would require CLI changes as well (a job for an alternative engine) + html_shell: index_view, + js_bundle: "dist/pkg/perseus_engine.js".to_string(), + // Our crate has the same name, so this will be predictable + wasm_bundle: "dist/pkg/perseus_engine_bg.wasm".to_string(), + // This probably won't exist, but on the off chance that the user needs to support older browsers, we'll provide it anyway + wasm_js_bundle: "dist/pkg/perseus_engine_bg.wasm.js".to_string(), + templates_map: app.get_atomic_templates_map(), + locales: app.get_locales(), + root_id: app_root, + snippets: "dist/pkg/snippets".to_string(), + error_pages: app.get_error_pages(), + // This will be available directly at `/.perseus/static` + static_dir: if fs::metadata(&static_dir_path).is_ok() { + Some(static_dir_path) + } else { + None + }, + static_aliases: app.get_static_aliases(), + }; + + ServerProps { + opts, + immutable_store, + mutable_store: app.get_mutable_store(), + global_state_creator: app.get_global_state_creator(), + translations_manager: block_on(app.get_translations_manager()), + } +} diff --git a/packages/perseus/src/engine/tinker.rs b/packages/perseus/src/engine/tinker.rs new file mode 100644 index 0000000000..fac2464963 --- /dev/null +++ b/packages/perseus/src/engine/tinker.rs @@ -0,0 +1,16 @@ +use crate::{i18n::TranslationsManager, stores::MutableStore}; +use crate::{plugins::PluginAction, PerseusAppBase, SsrNode}; + +/// Runs tinker plugin actions. +/// +/// Note that this expects to be run in the root of the project. +pub fn tinker(app: PerseusAppBase) { + let plugins = app.get_plugins(); + // Run all the tinker actions + // Note: this is deliberately synchronous, tinker actions that need a multithreaded async runtime should probably + // be making their own engines! + plugins + .functional_actions + .tinker + .run((), plugins.get_plugin_data()); +} diff --git a/packages/perseus/src/error_pages.rs b/packages/perseus/src/error_pages.rs index f7d71a2023..92d844a48e 100644 --- a/packages/perseus/src/error_pages.rs +++ b/packages/perseus/src/error_pages.rs @@ -1,11 +1,16 @@ use crate::translator::Translator; -use crate::{DomNode, Html, HydrateNode, SsrNode}; +use crate::Html; +#[cfg(not(target_arch = "wasm32"))] +use crate::SsrNode; +#[cfg(target_arch = "wasm32")] +use crate::{DomNode, HydrateNode}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::rc::Rc; use sycamore::prelude::Scope; use sycamore::view; use sycamore::view::View; +#[cfg(target_arch = "wasm32")] use web_sys::Element; /// The callback to a template the user must provide for error pages. This is passed the status code, the error message, the URL of the @@ -77,6 +82,7 @@ impl ErrorPages { // template_fn(cx, url.to_string(), status, err.to_string(), translator) // } } +#[cfg(target_arch = "wasm32")] impl ErrorPages { /// Renders the appropriate error page to the given DOM container. pub fn render_page( @@ -96,6 +102,7 @@ impl ErrorPages { ); } } +#[cfg(target_arch = "wasm32")] impl ErrorPages { /// Hydrates the appropriate error page to the given DOM container. This is used for when an error page is rendered by the server /// and then needs interactivity. @@ -107,6 +114,25 @@ impl ErrorPages { err: &str, translator: Option>, container: &Element, + ) { + let template_fn = self.get_template_fn(status); + let hydrate_view = template_fn(cx, url.to_string(), status, err.to_string(), translator); + // TODO Now convert that `HydrateNode` to a `DomNode` + let dom_view = hydrate_view; + // Render that to the given container + sycamore::hydrate_to(|_| dom_view, container); + } + /// Renders the appropriate error page to the given DOM container. This is implemented on `HydrateNode` to avoid having to have two `Html` type parameters everywhere + /// (one for templates and one for error pages). + // TODO Convert from a `HydrateNode` to a `DomNode` + pub fn render_page( + &self, + cx: Scope, + url: &str, + status: u16, + err: &str, + translator: Option>, + container: &Element, ) { let template_fn = self.get_template_fn(status); // Render that to the given container @@ -116,6 +142,7 @@ impl ErrorPages { ); } } +#[cfg(not(target_arch = "wasm32"))] impl ErrorPages { /// Renders the error page to a string. This should then be hydrated on the client-side. No reactive scope is provided to this function, it uses an internal one. pub fn render_to_string( diff --git a/packages/perseus/src/errors.rs b/packages/perseus/src/errors.rs index 8c0988d0aa..0ee42e226a 100644 --- a/packages/perseus/src/errors.rs +++ b/packages/perseus/src/errors.rs @@ -1,5 +1,6 @@ #![allow(missing_docs)] +#[cfg(not(target_arch = "wasm32"))] use crate::i18n::TranslationsManagerError; use thiserror::Error; @@ -8,8 +9,48 @@ use thiserror::Error; pub enum Error { #[error(transparent)] ClientError(#[from] ClientError), + #[cfg(not(target_arch = "wasm32"))] #[error(transparent)] ServerError(#[from] ServerError), + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] + #[error(transparent)] + EngineError(#[from] EngineError), +} + +/// Errors that can occur in the server-side engine system (responsible for building the app). +#[cfg(all(feature = "builder", not(target_arch = "wasm32")))] +#[derive(Error, Debug)] +pub enum EngineError { + // Many of the build/export processes return these more generic errors + #[error(transparent)] + ServerError(#[from] ServerError), + #[error("couldn't copy static directory at '{path}' to '{dest}'")] + CopyStaticDirError { + #[source] + source: fs_extra::error::Error, + path: String, + dest: String, + }, + #[error("couldn't copy static alias file from '{from}' to '{to}'")] + CopyStaticAliasFileError { + #[source] + source: std::io::Error, + from: String, + to: String, + }, + #[error("couldn't copy static alias directory from '{from}' to '{to}'")] + CopyStaticAliasDirErr { + #[source] + source: fs_extra::error::Error, + from: String, + to: String, + }, + #[error("couldn't write the generated error page to '{dest}'")] + WriteErrorPageError { + #[source] + source: std::io::Error, + dest: String, + }, } /// Errors that can occur in the browser. @@ -36,6 +77,7 @@ pub enum ClientError { } /// Errors that can occur in the build process or while the server is running. +#[cfg(not(target_arch = "wasm32"))] #[derive(Error, Debug)] pub enum ServerError { #[error("render function '{fn_name}' in template '{template_name}' failed (cause: {cause:?})")] @@ -62,6 +104,7 @@ pub enum ServerError { ServeError(#[from] ServeError), } /// Converts a server error into an HTTP status code. +#[cfg(not(target_arch = "wasm32"))] pub fn err_to_status_code(err: &ServerError) -> u16 { match err { ServerError::ServeError(ServeError::PageNotFound { .. }) => 404, @@ -86,6 +129,7 @@ pub enum GlobalStateError { } /// Errors that can occur while reading from or writing to a mutable or immutable store. +// We do need this on the client to complete some things #[derive(Error, Debug)] pub enum StoreError { #[error("asset '{name}' not found in store")] @@ -125,6 +169,7 @@ pub enum FetchError { } /// Errors that can occur while building an app. +#[cfg(not(target_arch = "wasm32"))] #[derive(Error, Debug)] pub enum BuildError { #[error("template '{template_name}' is missing feature '{feature_name}' (required due to its properties)")] @@ -150,6 +195,7 @@ pub enum BuildError { } /// Errors that can occur while exporting an app to static files. +#[cfg(not(target_arch = "wasm32"))] #[derive(Error, Debug)] pub enum ExportError { #[error("template '{template_name}' can't be exported because it depends on strategies that can't be run at build-time (only build state and build paths can be use din exportable templates)")] @@ -165,6 +211,7 @@ pub enum ServeError { PageNotFound { path: String }, #[error("both build and request states were defined for a template when only one or fewer were expected (should it be able to amalgamate states?)")] BothStatesDefined, + #[cfg(not(target_arch = "wasm32"))] #[error("couldn't parse revalidation datetime (try cleaning all assets)")] BadRevalidate { #[from] diff --git a/packages/perseus/src/export.rs b/packages/perseus/src/export.rs index a1bedaeeaa..665771a292 100644 --- a/packages/perseus/src/export.rs +++ b/packages/perseus/src/export.rs @@ -1,9 +1,9 @@ use crate::errors::*; use crate::i18n::{Locales, TranslationsManager}; -use crate::server::{get_render_cfg, HtmlShell, PageData}; +use crate::server::{get_render_cfg, HtmlShell}; use crate::stores::ImmutableStore; use crate::template::TemplateMap; -use crate::SsrNode; +use crate::{PageData, SsrNode}; use futures::future::{try_join, try_join_all}; /// Gets the static page data. diff --git a/packages/perseus/src/i18n/mod.rs b/packages/perseus/src/i18n/mod.rs index 435a2eb6cc..74288a29cf 100644 --- a/packages/perseus/src/i18n/mod.rs +++ b/packages/perseus/src/i18n/mod.rs @@ -1,9 +1,13 @@ +#[cfg(target_arch = "wasm32")] mod client_translations_manager; +#[cfg(target_arch = "wasm32")] mod locale_detector; mod locales; mod translations_manager; +#[cfg(target_arch = "wasm32")] pub use client_translations_manager::ClientTranslationsManager; +#[cfg(target_arch = "wasm32")] pub use locale_detector::detect_locale; pub use locales::Locales; pub use translations_manager::{ diff --git a/packages/perseus/src/i18n/translations_manager.rs b/packages/perseus/src/i18n/translations_manager.rs index dabab9c14f..ef52fdeede 100644 --- a/packages/perseus/src/i18n/translations_manager.rs +++ b/packages/perseus/src/i18n/translations_manager.rs @@ -25,9 +25,13 @@ pub enum TranslationsManagerError { } use crate::translator::Translator; +#[cfg(not(target_arch = "wasm32"))] use futures::future::join_all; +#[cfg(not(target_arch = "wasm32"))] use std::collections::HashMap; +#[cfg(not(target_arch = "wasm32"))] use tokio::fs::File; +#[cfg(not(target_arch = "wasm32"))] use tokio::io::AsyncReadExt; /// A trait for systems that manage where to put translations. At simplest, we'll just write them to static files, but they might also @@ -53,6 +57,7 @@ pub trait TranslationsManager: std::fmt::Debug + Clone + Send + Sync { } /// A utility function for allowing parallel futures execution. This returns a tuple of the locale and the translations as a JSON string. +#[cfg(not(target_arch = "wasm32"))] async fn get_translations_str_and_cache( locale: String, manager: &FsTranslationsManager, @@ -77,17 +82,23 @@ async fn get_translations_str_and_cache( /// As this is used as the default translations manager by most apps, this also supports not using i18n at all. #[derive(Clone, Debug)] pub struct FsTranslationsManager { + #[cfg(not(target_arch = "wasm32"))] root_path: String, /// A map of locales to cached translations. This decreases the number of file reads significantly for the locales specified. This /// does NOT cache dynamically, and will only cache the requested locales. Translators can be created when necessary from these. + #[cfg(not(target_arch = "wasm32"))] cached_translations: HashMap, /// The locales being cached for easier access. + #[cfg(not(target_arch = "wasm32"))] cached_locales: Vec, /// The file extension expected (e.g. JSON, FTL, etc). This allows for greater flexibility of translation engines (future). + #[cfg(not(target_arch = "wasm32"))] file_ext: String, /// This will be `true` is this translations manager is being used for an app that's not using i18n. + #[cfg(not(target_arch = "wasm32"))] is_dummy: bool, } +#[cfg(not(target_arch = "wasm32"))] impl FsTranslationsManager { /// Creates a new filesystem translations manager. You should provide a path like `/translations` here. You should also provide /// the locales you want to cache, which will have their translations stored in memory. Any supported locales not specified here @@ -116,8 +127,10 @@ impl FsTranslationsManager { manager } } +// `FsTranslationsManager` needs to exist in the browser, but it shouldn't do anything #[async_trait::async_trait] impl TranslationsManager for FsTranslationsManager { + #[cfg(not(target_arch = "wasm32"))] fn new_dummy() -> Self { Self { root_path: String::new(), @@ -127,6 +140,7 @@ impl TranslationsManager for FsTranslationsManager { is_dummy: true, } } + #[cfg(not(target_arch = "wasm32"))] async fn get_translations_str_for_locale( &self, locale: String, @@ -172,6 +186,7 @@ impl TranslationsManager for FsTranslationsManager { } } } + #[cfg(not(target_arch = "wasm32"))] async fn get_translator_for_locale( &self, locale: String, @@ -204,4 +219,22 @@ impl TranslationsManager for FsTranslationsManager { Ok(translator) } + #[cfg(target_arch = "wasm32")] + fn new_dummy() -> Self { + Self {} + } + #[cfg(target_arch = "wasm32")] + async fn get_translations_str_for_locale( + &self, + _locale: String, + ) -> Result { + Ok(String::new()) + } + #[cfg(target_arch = "wasm32")] + async fn get_translator_for_locale( + &self, + _locale: String, + ) -> Result { + Ok(crate::internal::i18n::Translator::new(String::new(), String::new()).unwrap()) + } } diff --git a/packages/perseus/src/init.rs b/packages/perseus/src/init.rs index 61874f1472..1ae84db0a4 100644 --- a/packages/perseus/src/init.rs +++ b/packages/perseus/src/init.rs @@ -1,16 +1,20 @@ use crate::plugins::PluginAction; -#[cfg(feature = "server-side")] +#[cfg(not(target_arch = "wasm32"))] use crate::server::{get_render_cfg, HtmlShell}; -#[cfg(feature = "server-side")] +use crate::stores::ImmutableStore; +#[cfg(not(target_arch = "wasm32"))] use crate::utils::get_path_prefix_server; use crate::{ - i18n::{FsTranslationsManager, Locales, TranslationsManager}, + i18n::{Locales, TranslationsManager}, state::GlobalStateCreator, - stores::{FsMutableStore, ImmutableStore, MutableStore}, + stores::MutableStore, templates::TemplateMap, ErrorPages, Html, Plugins, SsrNode, Template, }; use futures::Future; +#[cfg(target_arch = "wasm32")] +use std::marker::PhantomData; +#[cfg(not(target_arch = "wasm32"))] use std::pin::Pin; use std::{collections::HashMap, rc::Rc}; use sycamore::prelude::Scope; @@ -48,10 +52,12 @@ impl std::fmt::Debug for ErrorPagesGetter { /// The different types of translations managers that can be stored. This allows us to store dummy translations managers directly, without holding futures. If this stores a full /// translations manager though, it will store it as a `Future`, which is later evaluated. +#[cfg(not(target_arch = "wasm32"))] enum Tm { Dummy(T), Full(Pin>>), } +#[cfg(not(target_arch = "wasm32"))] impl std::fmt::Debug for Tm { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Tm").finish() @@ -91,23 +97,35 @@ pub struct PerseusAppBase { /// The app's error pages. error_pages: ErrorPagesGetter, /// The global state creator for the app. + #[cfg(not(target_arch = "wasm32"))] global_state_creator: GlobalStateCreator, /// The internationalization information for the app. locales: Locales, /// The static aliases the app serves. + #[cfg(not(target_arch = "wasm32"))] static_aliases: HashMap, /// The plugins the app uses. plugins: Rc>, /// The app's immutable store. + #[cfg(not(target_arch = "wasm32"))] immutable_store: ImmutableStore, /// The HTML template that'll be used to render the app into. This must be static, but can be generated or sourced in any way. Note that this MUST /// contain a `
` with the `id` set to whatever the value of `self.root` is. index_view: String, /// The app's mutable store. + #[cfg(not(target_arch = "wasm32"))] mutable_store: M, /// The app's translations manager, expressed as a function yielding a `Future`. This is only ever needed on the server-side, and can't be set up properly on the client-side because /// we can't use futures in the app initialization in Wasm. + #[cfg(not(target_arch = "wasm32"))] translations_manager: Tm, + /// The location of the directory to use for static assets that will placed under the URL `/.perseus/static/`. By default, this is the `static/` directory at the root + /// of your project. Note that the directory set here will only be used if it exists. + #[cfg(not(target_arch = "wasm32"))] + static_dir: String, + // We need this on the client-side to account for the unused type parameters + #[cfg(target_arch = "wasm32")] + _marker: PhantomData<(M, T)>, } // The usual implementation in which the default mutable store is used @@ -120,38 +138,57 @@ impl PerseusAppBase { /// /// This is asynchronous because it creates a translations manager in the background. // It makes no sense to implement `Default` on this, so we silence Clippy deliberately + #[cfg(not(target_arch = "wasm32"))] #[allow(clippy::new_without_default)] pub fn new() -> Self { Self::new_with_mutable_store(FsMutableStore::new("./dist/mutable".to_string())) } + /// Creates a new instance of a Perseus app using the default filesystem-based mutable store. For most apps, this will be sufficient. Note that this initializes the translations manager + /// as a dummy, and adds no templates or error pages. + /// + /// In development, you can get away with defining no error pages, but production apps (e.g. those created with `perseus deploy`) MUST set their own custom error pages. + /// + /// This is asynchronous because it creates a translations manager in the background. + // It makes no sense to implement `Default` on this, so we silence Clippy deliberately + #[cfg(target_arch = "wasm32")] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self::new_wasm() + } } // If one's using the default translations manager, caching should be handled automatically for them impl PerseusAppBase { /// The same as `.locales_and_translations_manager()`, but this accepts a literal `Locales` `struct`, which means this can be used when you're using `FsTranslationsManager` but when you don't /// know if your app is using i18n or not (almost always middleware). pub fn locales_lit_and_translations_manager(mut self, locales: Locales) -> Self { + #[cfg(not(target_arch = "wasm32"))] let using_i18n = locales.using_i18n; + self.locales = locales; - // If we're using i18n, do caching stuff - // If not, use a dummy translations manager - if using_i18n { - // By default, all translations are cached - let all_locales: Vec = self - .locales - .get_all() - .iter() - // We have a `&&String` at this point, hence the double clone - .cloned() - .cloned() - .collect(); - let tm_fut = FsTranslationsManager::new( - crate::internal::i18n::DFLT_TRANSLATIONS_DIR.to_string(), - all_locales, - crate::internal::i18n::TRANSLATOR_FILE_EXT.to_string(), - ); - self.translations_manager = Tm::Full(Box::pin(tm_fut)); - } else { - self.translations_manager = Tm::Dummy(FsTranslationsManager::new_dummy()); + // We only handle the translations manager on the server-side (it doesn't exist on the client-side) + #[cfg(not(target_arch = "wasm32"))] + { + // If we're using i18n, do caching stuff + // If not, use a dummy translations manager + if using_i18n { + // By default, all translations are cached + let all_locales: Vec = self + .locales + .get_all() + .iter() + // We have a `&&String` at this point, hence the double clone + .cloned() + .cloned() + .collect(); + let tm_fut = FsTranslationsManager::new( + crate::internal::i18n::DFLT_TRANSLATIONS_DIR.to_string(), + all_locales, + crate::internal::i18n::TRANSLATOR_FILE_EXT.to_string(), + ); + self.translations_manager = Tm::Full(Box::pin(tm_fut)); + } else { + self.translations_manager = Tm::Dummy(FsTranslationsManager::new_dummy()); + } } self @@ -175,6 +212,7 @@ impl PerseusAppBase { // The base implementation, generic over the mutable store and translations manager impl PerseusAppBase { /// Creates a new instance of a Perseus app, with the default options and a custom mutable store. + #[allow(unused_variables)] pub fn new_with_mutable_store(mutable_store: M) -> Self { Self { root: "root".to_string(), @@ -182,6 +220,7 @@ impl PerseusAppBase { template_getters: TemplateGetters(Vec::new()), // We do offer default error pages, but they'll panic if they're called for production building error_pages: ErrorPagesGetter(Box::new(ErrorPages::default)), + #[cfg(not(target_arch = "wasm32"))] global_state_creator: GlobalStateCreator::default(), // By default, we'll disable i18n (as much as I may want more websites to support more languages...) locales: Locales { @@ -190,15 +229,44 @@ impl PerseusAppBase { using_i18n: false, }, // By default, we won't serve any static content outside the `static/` directory + #[cfg(not(target_arch = "wasm32"))] static_aliases: HashMap::new(), // By default, we won't use any plugins plugins: Rc::new(Plugins::new()), - // This is relative to `.perseus/` + #[cfg(not(target_arch = "wasm32"))] immutable_store: ImmutableStore::new("./dist".to_string()), + #[cfg(not(target_arch = "wasm32"))] mutable_store, + #[cfg(not(target_arch = "wasm32"))] translations_manager: Tm::Dummy(T::new_dummy()), // Many users won't need anything fancy in the index view, so we provide a default index_view: DFLT_INDEX_VIEW.to_string(), + #[cfg(not(target_arch = "wasm32"))] + static_dir: "./static".to_string(), + #[cfg(target_arch = "wasm32")] + _marker: PhantomData, + } + } + /// Internal function for Wasm initialization. This should never be called by the user! + #[cfg(target_arch = "wasm32")] + fn new_wasm() -> Self { + Self { + root: "root".to_string(), + // We do initialize with no templates, because an app without templates is in theory possible (and it's more convenient to call `.template()` for each one) + template_getters: TemplateGetters(Vec::new()), + // We do offer default error pages, but they'll panic if they're called for production building + error_pages: ErrorPagesGetter(Box::new(ErrorPages::default)), + // By default, we'll disable i18n (as much as I may want more websites to support more languages...) + locales: Locales { + default: "xx-XX".to_string(), + other: Vec::new(), + using_i18n: false, + }, + // By default, we won't use any plugins + plugins: Rc::new(Plugins::new()), + // Many users won't need anything fancy in the index view, so we provide a default + index_view: DFLT_INDEX_VIEW.to_string(), + _marker: PhantomData, } } @@ -208,6 +276,16 @@ impl PerseusAppBase { self.root = val.to_string(); self } + /// Sets the location of the directory storing static assets to be hosted under the URL `/.perseus/static/`. + #[allow(unused_variables)] + #[allow(unused_mut)] + pub fn static_dir(mut self, val: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + { + self.static_dir = val.to_string(); + } + self + } /// Sets all the app's templates. This takes a vector of boxed functions that return templates. pub fn templates(mut self, val: Vec Template>>) -> Self { self.template_getters.0 = val; @@ -224,8 +302,13 @@ impl PerseusAppBase { self } /// Sets the app's global state creator. + #[allow(unused_variables)] + #[allow(unused_mut)] pub fn global_state_creator(mut self, val: GlobalStateCreator) -> Self { - self.global_state_creator = val; + #[cfg(not(target_arch = "wasm32"))] + { + self.global_state_creator = val; + } self } /// Sets the locales information for the app. The first argument is the default locale (used as a fallback for users with no locale preferences set in their browsers), and @@ -256,8 +339,13 @@ impl PerseusAppBase { /// When your code is run on the server, the `Future` will be `.await`ed on, but on Wasm, it will be discarded and ignored, since the translations manager isn't needed in Wasm. /// /// This is generally intended for use with custom translations manager or specific use-cases with the default (mostly to do with custom caching behavior). + #[allow(unused_variables)] + #[allow(unused_mut)] pub fn translations_manager(mut self, val: impl Future + 'static) -> Self { - self.translations_manager = Tm::Full(Box::pin(val)); + #[cfg(not(target_arch = "wasm32"))] + { + self.translations_manager = Tm::Full(Box::pin(val)); + } self } /// Explicitly disables internationalization. You shouldn't ever need to call this, as it's the default, but you may want to if you're writing middleware that doesn't support i18n. @@ -268,16 +356,27 @@ impl PerseusAppBase { using_i18n: false, }; // All translations manager must implement this function, which is designed for this exact purpose - self.translations_manager = Tm::Dummy(T::new_dummy()); + #[cfg(not(target_arch = "wasm32"))] + { + self.translations_manager = Tm::Dummy(T::new_dummy()); + } self } /// Sets all the app's static aliases. This takes a map of URLs (e.g. `/file`) to resource paths, relative to the project directory (e.g. `style.css`). + #[allow(unused_variables)] + #[allow(unused_mut)] pub fn static_aliases(mut self, val: HashMap) -> Self { - self.static_aliases = val; + #[cfg(not(target_arch = "wasm32"))] + { + self.static_aliases = val; + } self } /// Adds a single static alias (convenience function). This takes a URL path (e.g. `/file`) followed by a path to a resource (which must be within the project directory, e.g. `style.css`). + #[allow(unused_variables)] + #[allow(unused_mut)] pub fn static_alias(mut self, url: &str, resource: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] // We don't elaborate the alias to an actual filesystem path until the getter self.static_aliases .insert(url.to_string(), resource.to_string()); @@ -290,13 +389,23 @@ impl PerseusAppBase { } /// Sets the mutable store for the app to use, which you would change for some production server environments if you wanted to store build artifacts that can change at runtime in a /// place other than on the filesystem (created for serverless functions specifically). + #[allow(unused_variables)] + #[allow(unused_mut)] pub fn mutable_store(mut self, val: M) -> Self { - self.mutable_store = val; + #[cfg(not(target_arch = "wasm32"))] + { + self.mutable_store = val; + } self } /// Sets the immutable store for the app to use. You should almost never need to change this unless you're not working with the CLI. + #[allow(unused_variables)] + #[allow(unused_mut)] pub fn immutable_store(mut self, val: ImmutableStore) -> Self { - self.immutable_store = val; + #[cfg(not(target_arch = "wasm32"))] + { + self.immutable_store = val; + } self } /// Sets the index view as a string. This should be used if you're using an `index.html` file or the like. @@ -334,18 +443,24 @@ impl PerseusAppBase { .run((), self.plugins.get_plugin_data()) .unwrap_or_else(|| self.root.to_string()) } + /// Gets the directory containing static assets to be hosted under the URL `/.perseus/static/`. + // TODO Plugin action for this? + #[cfg(not(target_arch = "wasm32"))] + pub fn get_static_dir(&self) -> String { + self.static_dir.to_string() + } /// Gets the index view as a string, without generating an HTML shell (pass this into `::get_html_shell()` to do that). /// /// Note that this automatically adds `` to the start of the HTMl shell produced, which can only be overriden with a control plugin (though you should really never do this /// in Perseus, which targets HTML on the web). - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub fn get_index_view_str(&self) -> String { // We have to add an HTML document type declaration, otherwise the browser could think it's literally anything! (This shouldn't be a problem, but it could be in 100 years...) format!("\n{}", self.index_view) } /// Gets an HTML shell from an index view string. This is broken out so that it can be executed after the app has been built (which requries getting the translations manager, consuming /// `self`). As inconvenient as this is, it's necessitated, otherwise exporting would try to access the built app before it had actually been built. - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub async fn get_html_shell( index_view_str: String, root: &str, @@ -463,7 +578,7 @@ impl PerseusAppBase { map } /// Gets the templates in an `Arc`-based `HashMap` for concurrent access. This should only be relevant on the server-side. - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub fn get_atomic_templates_map(&self) -> crate::templates::ArcTemplateMap { let mut map = HashMap::new(); @@ -507,6 +622,7 @@ impl PerseusAppBase { error_pages } /// Gets the global state creator. This can't be directly modified by plugins because of reactive type complexities. + #[cfg(not(target_arch = "wasm32"))] pub fn get_global_state_creator(&self) -> GlobalStateCreator { self.global_state_creator.clone() } @@ -523,7 +639,7 @@ impl PerseusAppBase { /// Gets the server-side translations manager. Like the mutable store, this can't be modified by plugins due to trait complexities. /// /// This involves evaluating the future stored for the translations manager, and so this consumes `self`. - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub async fn get_translations_manager(self) -> T { match self.translations_manager { Tm::Dummy(tm) => tm, @@ -531,7 +647,7 @@ impl PerseusAppBase { } } /// Gets the immutable store. - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub fn get_immutable_store(&self) -> ImmutableStore { let immutable_store = self.immutable_store.clone(); self.plugins @@ -542,7 +658,7 @@ impl PerseusAppBase { .unwrap_or(immutable_store) } /// Gets the mutable store. This can't be modified by plugins due to trait complexities, so plugins should instead expose a function that the user can use to manually set it. - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub fn get_mutable_store(&self) -> M { self.mutable_store.clone() } @@ -552,7 +668,7 @@ impl PerseusAppBase { } /// Gets the static aliases. This will check all provided resource paths to ensure they don't reference files outside the project directory, due to potential security risks in production /// (we don't want to accidentally serve an arbitrary in a production environment where a path may point to somewhere evil, like an alias to `/etc/passwd`). - #[cfg(feature = "server-side")] + #[cfg(not(target_arch = "wasm32"))] pub fn get_static_aliases(&self) -> HashMap { let mut static_aliases = self.static_aliases.clone(); // This will return a map of plugin name to another map of static aliases that that plugin produced @@ -585,22 +701,8 @@ impl PerseusAppBase { } else if path.starts_with("../") { // Anything outside this directory is a security risk as well panic!("it's a security risk to include paths outside the current directory in `static_aliases` ('{}')", path); - } else if path.starts_with("./") { - // `./` -> `../` (moving to execution from `.perseus/`) - // But if we're operating standalone, it stays the same - if cfg!(feature = "standalone") { - path.to_string() - } else { - format!(".{}", path) - } } else { - // Anything else gets a `../` prepended - // But if we're operating standalone, it stays the same - if cfg!(feature = "standalone") { - path.to_string() - } else { - format!("../{}", path) - } + path.to_string() }; scoped_static_aliases.insert(url, new_path); @@ -620,6 +722,9 @@ pub fn PerseusRoot(cx: Scope) -> View { } } +use crate::i18n::FsTranslationsManager; +use crate::stores::FsMutableStore; + /// An alias for the usual kind of Perseus app, which uses the filesystem-based mutable store and translations manager. pub type PerseusApp = PerseusAppBase; /// An alias for a Perseus app that uses a custom mutable store type. diff --git a/packages/perseus/src/lib.rs b/packages/perseus/src/lib.rs index f1196fb037..9d19a76494 100644 --- a/packages/perseus/src/lib.rs +++ b/packages/perseus/src/lib.rs @@ -34,14 +34,23 @@ pub mod state; /// Utilities for working with immutable and mutable stores. You can learn more about these in the book. pub mod stores; +#[cfg(not(target_arch = "wasm32"))] mod build; +#[cfg(all(feature = "client-helpers", target_arch = "wasm32"))] +mod client; +#[cfg(all(feature = "builder", not(target_arch = "wasm32")))] +mod engine; mod error_pages; +#[cfg(not(target_arch = "wasm32"))] mod export; mod i18n; mod init; mod macros; +mod page_data; mod router; +#[cfg(not(target_arch = "wasm32"))] mod server; +#[cfg(target_arch = "wasm32")] mod shell; mod template; mod translator; @@ -49,12 +58,22 @@ mod utils; // The rest of this file is devoted to module structuring // Re-exports +#[cfg(not(target_arch = "wasm32"))] pub use http; +#[cfg(not(target_arch = "wasm32"))] pub use http::Request as HttpRequest; pub use sycamore_futures::spawn_local_scoped; /// All HTTP requests use empty bodies for simplicity of passing them around. They'll never need payloads (value in path requested). +#[cfg(not(target_arch = "wasm32"))] pub type Request = HttpRequest<()>; -pub use perseus_macro::{autoserde, head, main, make_rx, template, template_rx, test}; +#[cfg(all(feature = "client-helpers", target_arch = "wasm32"))] +pub use client::{run_client, ClientReturn}; +#[cfg(feature = "macros")] +pub use perseus_macro::{ + amalgamate_states, browser, browser_main, build_paths, build_state, engine, engine_main, + global_build_state, head, main, main_export, make_rx, request_state, set_headers, + should_revalidate, template, template_rx, test, +}; pub use sycamore::prelude::{DomNode, Html, HydrateNode, SsrNode}; pub use sycamore_router::{navigate, navigate_replace, Route}; // TODO Should we be exporting `Route` anymore? @@ -63,9 +82,14 @@ pub use sycamore_router::{navigate, navigate_replace, Route}; // TODO Should we // Items that should be available at the root (this should be nearly everything used in a typical Perseus app) pub use crate::error_pages::ErrorPages; pub use crate::errors::{ErrorCause, GenericErrorWithCause}; +pub use crate::page_data::PageData; pub use crate::plugins::{Plugin, PluginAction, Plugins}; +#[cfg(target_arch = "wasm32")] pub use crate::shell::checkpoint; -pub use crate::template::{HeadFn, RenderFnResult, RenderFnResultWithCause, States, Template}; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::template::{HeadFn, States}; +pub use crate::template::{RenderFnResult, RenderFnResultWithCause, Template}; +#[cfg(not(target_arch = "wasm32"))] pub use crate::utils::{cache_fallible_res, cache_res}; // Everything in the `init.rs` file should be available at the top-level for convenience pub use crate::init::*; @@ -75,6 +99,11 @@ pub mod templates { pub use crate::router::{RouterLoadState, RouterState}; pub use crate::template::*; } +/// Utilities for building an app. +#[cfg(all(feature = "builder", not(target_arch = "wasm32")))] +pub mod builder { + pub use crate::engine::*; +} /// A series of exports that should be unnecessary for nearly all uses of Perseus. These are used principally in developing alternative /// engines. pub mod internal { @@ -87,6 +116,7 @@ pub mod internal { } /// Internal utilities for working with the serving process. These will be useful for building integrations for hosting Perseus /// on different platforms. + #[cfg(not(target_arch = "wasm32"))] pub mod serve { pub use crate::server::*; } @@ -99,19 +129,26 @@ pub mod internal { pub use crate::error_pages::*; } /// Internal utilities for working with the app shell. + #[cfg(target_arch = "wasm32")] pub mod shell { pub use crate::shell::*; } - /// Internal utilities for building. + /// Internal utilities for building apps at a very low level. + #[cfg(not(target_arch = "wasm32"))] pub mod build { pub use crate::build::*; } - /// Internal utilities for exporting. + /// Internal utilities for exporting apps at a very low level. + #[cfg(not(target_arch = "wasm32"))] pub mod export { pub use crate::export::*; } - pub use crate::utils::{get_path_prefix_client, get_path_prefix_server}; + #[cfg(target_arch = "wasm32")] + pub use crate::utils::get_path_prefix_client; + #[cfg(not(target_arch = "wasm32"))] + pub use crate::utils::get_path_prefix_server; /// Internal utilities for logging. These are just re-exports so that users don't have to have `web_sys` and `wasm_bindgen` to use `web_log!`. + #[cfg(target_arch = "wasm32")] pub mod log { pub use wasm_bindgen::JsValue; pub use web_sys::console::log_1 as log_js_value; diff --git a/packages/perseus/src/macros.rs b/packages/perseus/src/macros.rs index 7fc5ad4c76..52459f6d2c 100644 --- a/packages/perseus/src/macros.rs +++ b/packages/perseus/src/macros.rs @@ -14,14 +14,9 @@ macro_rules! add_translations_manager { }; } -#[cfg(feature = "standalone")] -#[doc(hidden)] -/// The default translations directory when we're running as a standalone binary. -pub static DFLT_TRANSLATIONS_DIR: &str = "./translations"; -#[cfg(not(feature = "standalone"))] #[doc(hidden)] /// The default translations directory when we're running with the `.perseus/` support structure. -pub static DFLT_TRANSLATIONS_DIR: &str = "../translations"; +pub static DFLT_TRANSLATIONS_DIR: &str = "./translations"; /// Defines the components to create an entrypoint for the app. The actual entrypoint is created in the `.perseus/` crate (where we can /// get all the dependencies without driving the user's `Cargo.toml` nuts). This also defines the template map. This is intended to make diff --git a/packages/perseus/src/server/page_data.rs b/packages/perseus/src/page_data.rs similarity index 100% rename from packages/perseus/src/server/page_data.rs rename to packages/perseus/src/page_data.rs diff --git a/packages/perseus/src/plugins/functional.rs b/packages/perseus/src/plugins/functional.rs index ef811b8f0e..4768ce008e 100644 --- a/packages/perseus/src/plugins/functional.rs +++ b/packages/perseus/src/plugins/functional.rs @@ -1,7 +1,11 @@ +#[cfg(not(target_arch = "wasm32"))] +use crate::errors::EngineError; use crate::plugins::*; use crate::Html; use std::any::Any; use std::collections::HashMap; +#[cfg(not(target_arch = "wasm32"))] +use std::rc::Rc; /// An action which can be taken by many plugins. When run, a functional action will return a map of plugin names to their return types. pub struct FunctionalPluginAction { @@ -69,10 +73,13 @@ pub struct FunctionalPluginActions { /// Actions pertaining to the modification of settings created with `PerseusApp`. pub settings_actions: FunctionalPluginSettingsActions, /// Actions pertaining to the build process. + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] pub build_actions: FunctionalPluginBuildActions, /// Actions pertaining to the export process. + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] pub export_actions: FunctionalPluginExportActions, /// Actions pertaining to the process of exporting an error page. + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] pub export_error_page_actions: FunctionalPluginExportErrorPageActions, /// Actions pertaining to the server. pub server_actions: FunctionalPluginServerActions, @@ -84,8 +91,11 @@ impl Default for FunctionalPluginActions { Self { tinker: FunctionalPluginAction::default(), settings_actions: FunctionalPluginSettingsActions::::default(), + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] build_actions: FunctionalPluginBuildActions::default(), + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] export_actions: FunctionalPluginExportActions::default(), + #[cfg(all(feature = "builder", not(target_arch = "wasm32")))] export_error_page_actions: FunctionalPluginExportErrorPageActions::default(), server_actions: FunctionalPluginServerActions::default(), client_actions: FunctionalPluginClientActions::default(), @@ -143,6 +153,7 @@ pub struct FunctionalPluginHtmlShellActions { /// Functional actions that pertain to the build process. Note that these actions are not available for the build /// stage of the export process, and those should be registered separately. +#[cfg(all(feature = "builder", not(target_arch = "wasm32")))] #[derive(Default, Debug)] pub struct FunctionalPluginBuildActions { /// Runs before the build process. @@ -150,12 +161,12 @@ pub struct FunctionalPluginBuildActions { /// Runs after the build process if it completes successfully. pub after_successful_build: FunctionalPluginAction<(), ()>, /// Runs after the build process if it fails. - pub after_failed_build: FunctionalPluginAction, + pub after_failed_build: FunctionalPluginAction, ()>, /// Runs after the build process if it failed to generate global state. - pub after_failed_global_state_creation: - FunctionalPluginAction, + pub after_failed_global_state_creation: FunctionalPluginAction, ()>, } /// Functional actions that pertain to the export process. +#[cfg(all(feature = "builder", not(target_arch = "wasm32")))] #[derive(Default, Debug)] pub struct FunctionalPluginExportActions { /// Runs before the export process. @@ -163,24 +174,23 @@ pub struct FunctionalPluginExportActions { /// Runs after the build stage in the export process if it completes successfully. pub after_successful_build: FunctionalPluginAction<(), ()>, /// Runs after the build stage in the export process if it fails. - pub after_failed_build: FunctionalPluginAction, + pub after_failed_build: FunctionalPluginAction, ()>, /// Runs after the export process if it fails. - pub after_failed_export: FunctionalPluginAction, - /// Runs if copying the static directory failed. The error type here is likely from a third-party library, so it's - /// provided as a string (otherwise we'd be hauling in an extra library for one type). - pub after_failed_static_copy: FunctionalPluginAction, - /// Runs if copying a static alias that was a directory failed. The error type here is likely from a third-party library, so it's - /// provided as a string (otherwise we'd be hauling in an extra library for one type). - pub after_failed_static_alias_dir_copy: FunctionalPluginAction, - /// Runs if copying a static alias that was a file failed. - pub after_failed_static_alias_file_copy: FunctionalPluginAction, + pub after_failed_export: FunctionalPluginAction, ()>, + /// Runs if copying the static directory failed. + pub after_failed_static_copy: FunctionalPluginAction, ()>, + /// Runs if copying a static alias that was a directory failed. The argument to this is a tuple of the from and to locations of the copy, along with the error. + pub after_failed_static_alias_dir_copy: FunctionalPluginAction, ()>, + /// Runs if copying a static alias that was a file failed. The argument to this is a tuple of the from and to locations of the copy, along with the error. + pub after_failed_static_alias_file_copy: FunctionalPluginAction, ()>, /// Runs after the export process if it completes successfully. pub after_successful_export: FunctionalPluginAction<(), ()>, - /// Runs after the export process if it failed to generate global state. - pub after_failed_global_state_creation: - FunctionalPluginAction, + /// Runs after the export process if it failed to generate global state. Note that the error here will always be a `GlobalStateError`, but it must be processed as a + /// `ServerError` due to ownership constraints. + pub after_failed_global_state_creation: FunctionalPluginAction, ()>, } /// Functional actions that pertain to the process of exporting an error page. +#[cfg(all(feature = "builder", not(target_arch = "wasm32")))] #[derive(Default, Debug)] pub struct FunctionalPluginExportErrorPageActions { /// Runs before the process of exporting an error page, providing the HTTP status code to be exported and the output filename (relative to the root of the project, not to `.perseus/`). @@ -188,7 +198,7 @@ pub struct FunctionalPluginExportErrorPageActions { /// Runs after a error page was exported successfully. pub after_successful_export_error_page: FunctionalPluginAction<(), ()>, /// Runs if writing to the output file failed. Error and filename are given. - pub after_failed_write: FunctionalPluginAction<(std::io::Error, String), ()>, + pub after_failed_write: FunctionalPluginAction, ()>, } /// Functional actions that pertain to the server. #[derive(Default, Debug)] diff --git a/packages/perseus/src/router/app_route.rs b/packages/perseus/src/router/app_route.rs index 30002610f5..4182ac1b9b 100644 --- a/packages/perseus/src/router/app_route.rs +++ b/packages/perseus/src/router/app_route.rs @@ -1,39 +1,77 @@ -use crate::Html; +use super::{match_route, RouteVerdict}; +use crate::{i18n::Locales, templates::TemplateMap, Html}; +use std::collections::HashMap; use sycamore_router::Route; -use super::RouteVerdict; +// /// Creates an app-specific routing `struct`. Sycamore expects an `enum` to do this, so we create a `struct` that behaves similarly. If +// /// we don't do this, we can't get the information necessary for routing into the `enum` at all (context and global variables don't suit +// /// this particular case). +// #[macro_export] +// macro_rules! create_app_route { +// { +// name => $name:ident, +// render_cfg => $render_cfg:expr, +// templates => $templates:expr, +// locales => $locales:expr +// } => { +// /// The route type for the app, with all routing logic inbuilt through the generation macro. +// #[derive(::std::clone::Clone)] +// struct $name($crate::internal::router::RouteVerdict); +// impl $crate::internal::router::PerseusRoute for $name { +// fn get_verdict(&self) -> &$crate::internal::router::RouteVerdict { +// &self.0 +// } +// } +// impl ::sycamore_router::Route for $name { +// fn match_route(path: &[&str]) -> Self { +// let verdict = $crate::internal::router::match_route(path, $render_cfg, $templates, $locales); +// Self(verdict) +// } +// } +// }; +// } -/// Creates an app-specific routing `struct`. Sycamore expects an `enum` to do this, so we create a `struct` that behaves similarly. If -/// we don't do this, we can't get the information necessary for routing into the `enum` at all (context and global variables don't suit -/// this particular case). -#[macro_export] -macro_rules! create_app_route { - { - name => $name:ident, - render_cfg => $render_cfg:expr, - templates => $templates:expr, - locales => $locales:expr - } => { - /// The route type for the app, with all routing logic inbuilt through the generation macro. - #[derive(::std::clone::Clone)] - struct $name($crate::internal::router::RouteVerdict); - impl $crate::internal::router::PerseusRoute for $name { - fn get_verdict(&self) -> &$crate::internal::router::RouteVerdict { - &self.0 - } - } - impl ::sycamore_router::Route for $name { - fn match_route(path: &[&str]) -> Self { - let verdict = $crate::internal::router::match_route(path, $render_cfg, $templates, $locales); - Self(verdict) - } +/// The Perseus route system, which implements Sycamore `Route`, but adds additional data for Perseus' processing system. +pub struct PerseusRoute { + /// The current route verdict. The initialization value of this is completely irrelevant (it will be overriden immediately by the internal routing logic). + pub verdict: RouteVerdict, + /// The app's render configuration. + pub render_cfg: HashMap, + /// The templates the app is using. + pub templates: TemplateMap, + /// The app's i18n configuration. + pub locales: Locales, +} +// Sycamore would only use this if we were processing dynamic routes, which we're not +// In other words, it's fine that these values would break everything if they were used, they're just compiler appeasement +impl Default for PerseusRoute { + fn default() -> Self { + Self { + verdict: RouteVerdict::NotFound, + render_cfg: HashMap::default(), + templates: TemplateMap::default(), + locales: Locales { + default: String::default(), + other: Vec::default(), + using_i18n: bool::default(), + }, } - }; + } } - -/// A trait for the routes in Perseus apps. This should be used almost exclusively internally, and you should never need to touch -/// it unless you're building a custom engine. -pub trait PerseusRoute: Route + Clone { - /// Gets the route verdict for the current route. - fn get_verdict(&self) -> &RouteVerdict; +impl PerseusRoute { + /// Gets the current route verdict. + pub fn get_verdict(&self) -> &RouteVerdict { + &self.verdict + } +} +impl Route for PerseusRoute { + fn match_route(&self, path: &[&str]) -> Self { + let verdict = match_route(path, &self.render_cfg, &self.templates, &self.locales); + Self { + verdict, + render_cfg: self.render_cfg.clone(), + templates: self.templates.clone(), + locales: self.locales.clone(), + } + } } diff --git a/packages/perseus/src/router/mod.rs b/packages/perseus/src/router/mod.rs index ba5d63f907..7c39ebfb1c 100644 --- a/packages/perseus/src/router/mod.rs +++ b/packages/perseus/src/router/mod.rs @@ -1,6 +1,7 @@ mod app_route; // This just exposes a macro mod match_route; mod route_verdict; +#[cfg(target_arch = "wasm32")] mod router_component; mod router_state; @@ -9,5 +10,6 @@ pub use match_route::{ get_template_for_path, get_template_for_path_atomic, match_route, match_route_atomic, }; pub use route_verdict::{RouteInfo, RouteInfoAtomic, RouteVerdict, RouteVerdictAtomic}; +#[cfg(target_arch = "wasm32")] pub use router_component::*; // TODO pub use router_state::{RouterLoadState, RouterState}; diff --git a/packages/perseus/src/router/router_component.rs b/packages/perseus/src/router/router_component.rs index f4c74cf19c..a0b5cd18dd 100644 --- a/packages/perseus/src/router/router_component.rs +++ b/packages/perseus/src/router/router_component.rs @@ -7,16 +7,17 @@ use crate::{ router::{PerseusRoute, RouteInfo, RouteVerdict}, shell::{app_shell, get_initial_state, InitialState, ShellProps}, }, - templates::{RenderCtx, RouterLoadState, RouterState, TemplateNodeType}, + templates::{RenderCtx, RouterLoadState, RouterState, TemplateMap, TemplateNodeType}, DomNode, ErrorPages, Html, }; use std::cell::RefCell; +use std::collections::HashMap; use std::rc::Rc; use sycamore::{ prelude::{component, create_effect, create_signal, view, NodeRef, ReadSignal, Scope, View}, Prop, }; -use sycamore_router::{HistoryIntegration, Router}; +use sycamore_router::{HistoryIntegration, RouterBase}; use web_sys::Element; // We don't want to bring in a styling library, so we do this the old-fashioned way! @@ -43,7 +44,7 @@ struct OnRouteChangeProps<'a, G: Html> { container_rx: NodeRef, router_state: RouterState, translations_manager: Rc>, - error_pages: Rc>, + error_pages: Rc>, initial_container: Option, } @@ -130,9 +131,13 @@ fn on_route_change( #[derive(Debug, Prop)] pub struct PerseusRouterProps { /// The error pages the app is using. - pub error_pages: ErrorPages, + pub error_pages: ErrorPages, /// The locales settings the app is using. pub locales: Locales, + /// The templates the app is using. + pub templates: TemplateMap, + /// The render configuration of the app (which lays out routing information, among other things). + pub render_cfg: HashMap, } /// The Perseus router. This is used internally in the Perseus engine, and you shouldn't need to access this directly unless @@ -141,13 +146,23 @@ pub struct PerseusRouterProps { /// Note: this deliberately has a snake case name, and should be called directly with `cx` as the first argument, allowing the `AppRoute` generic /// creates with `create_app_root!` to be provided easily. That given `cx` property will be used for all context registration in the app. #[component] -pub fn perseus_router + 'static>( +pub fn perseus_router( cx: Scope, PerseusRouterProps { error_pages, locales, + templates, + render_cfg, }: PerseusRouterProps, ) -> View { + // Create a `Route` to pass through Sycamore with the information we need + let route = PerseusRoute { + verdict: RouteVerdict::NotFound, + templates, + render_cfg, + locales: locales.clone(), + }; + // Get the root that the server will have injected initial load content into // This will be moved into a reactive `
` by the app shell // This is an `Option` until we know we aren't doing locale detection (in which case it wouldn't exist) @@ -263,9 +278,11 @@ pub fn perseus_router + 'stati crate::state::connect_to_reload_server(live_reload_indicator.clone()); view! { cx, - Router { + // This is a lower-level version of `Router` that lets us provide a `Route` with the data we want + RouterBase { integration: HistoryIntegration::new(), - view: move |cx, route: &ReadSignal| { + route, + view: move |cx, route: &ReadSignal>| { // Sycamore's reactivity is broken by a future, so we need to explicitly add the route to the reactive dependencies here // We do need the future though (otherwise `container_rx` doesn't link to anything until it's too late) create_effect(cx, move || { diff --git a/packages/perseus/src/server/html_shell.rs b/packages/perseus/src/server/html_shell.rs index 0524fb608d..9c39a273cb 100644 --- a/packages/perseus/src/server/html_shell.rs +++ b/packages/perseus/src/server/html_shell.rs @@ -1,5 +1,5 @@ use crate::error_pages::ErrorPageData; -use crate::server::PageData; +use crate::PageData; use std::collections::HashMap; use std::{env, fmt}; @@ -68,7 +68,7 @@ impl HtmlShell { #[cfg(not(feature = "wasm2js"))] let load_wasm_bundle = format!( r#" - import init, {{ run }} from "{path_prefix}/.perseus/bundle.js"; + import init, {{ main as run }} from "{path_prefix}/.perseus/bundle.js"; async function main() {{ await init("{path_prefix}/.perseus/bundle.wasm"); run(); @@ -80,7 +80,7 @@ impl HtmlShell { #[cfg(feature = "wasm2js")] let load_wasm_bundle = format!( r#" - import init, {{ run }} from "{path_prefix}/.perseus/bundle.js"; + import init, {{ main as run }} from "{path_prefix}/.perseus/bundle.js"; async function main() {{ await init("{path_prefix}/.perseus/bundle.wasm.js"); run(); diff --git a/packages/perseus/src/server/mod.rs b/packages/perseus/src/server/mod.rs index 4112501899..3746ac2849 100644 --- a/packages/perseus/src/server/mod.rs +++ b/packages/perseus/src/server/mod.rs @@ -5,14 +5,12 @@ mod build_error_page; mod get_render_cfg; mod html_shell; mod options; -mod page_data; mod render; pub use build_error_page::build_error_page; pub use get_render_cfg::get_render_cfg; pub use html_shell::HtmlShell; pub use options::{ServerOptions, ServerProps}; -pub use page_data::PageData; pub use render::{get_page, get_page_for_template, GetPageProps}; /// Removes empty elements from a path, which is important due to double slashes. This returns a vector of the path's components; diff --git a/packages/perseus/src/server/render.rs b/packages/perseus/src/server/render.rs index 05a62aaa04..aebae700bf 100644 --- a/packages/perseus/src/server/render.rs +++ b/packages/perseus/src/server/render.rs @@ -1,10 +1,10 @@ -use super::PageData; use crate::errors::*; use crate::i18n::TranslationsManager; use crate::stores::{ImmutableStore, MutableStore}; use crate::template::{PageProps, States, Template, TemplateMap}; use crate::translator::Translator; use crate::utils::decode_time_str; +use crate::PageData; use crate::Request; use crate::SsrNode; use chrono::{DateTime, Utc}; diff --git a/packages/perseus/src/shell.rs b/packages/perseus/src/shell.rs index 43fab27663..67c232c1d8 100644 --- a/packages/perseus/src/shell.rs +++ b/packages/perseus/src/shell.rs @@ -2,10 +2,10 @@ use crate::error_pages::ErrorPageData; use crate::errors::*; use crate::i18n::ClientTranslationsManager; use crate::router::{RouteVerdict, RouterLoadState, RouterState}; -use crate::server::PageData; use crate::template::{PageProps, Template, TemplateNodeType}; use crate::utils::get_path_prefix_client; use crate::ErrorPages; +use crate::PageData; use fmterr::fmt_err; use std::cell::RefCell; use std::collections::HashMap; @@ -246,7 +246,7 @@ pub struct ShellProps<'a> { /// A *client-side* translations manager to use (this manages caching translations). pub translations_manager: Rc>, /// The error pages, for use if something fails. - pub error_pages: Rc>, + pub error_pages: Rc>, /// The container responsible for the initial render from the server (non-interactive, this may need to be wiped). pub initial_container: Element, /// The container for reactive content. diff --git a/packages/perseus/src/state/global_state.rs b/packages/perseus/src/state/global_state.rs index 3b809b55e2..6c1ec9cb5f 100644 --- a/packages/perseus/src/state/global_state.rs +++ b/packages/perseus/src/state/global_state.rs @@ -19,16 +19,12 @@ pub type GlobalStateCreatorFn = Rc; #[derive(Default, Clone)] pub struct GlobalStateCreator { /// The function that creates state at build-time. This is roughly equivalent to the *build state* strategy for templates. + #[cfg(not(target_arch = "wasm32"))] build: Option, } impl std::fmt::Debug for GlobalStateCreator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("GlobalStateCreator") - .field( - "build", - &self.build.as_ref().map(|_| "GlobalStateCreatorFn"), - ) - .finish() + f.debug_struct("GlobalStateCreator").finish() } } impl GlobalStateCreator { @@ -37,19 +33,22 @@ impl GlobalStateCreator { Self::default() } /// Adds a function to generate global state at build-time. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn build_state_fn( mut self, val: impl GlobalStateCreatorFnType + Send + Sync + 'static, ) -> Self { - #[cfg(feature = "server-side")] - { - self.build = Some(Rc::new(val)); - } + self.build = Some(Rc::new(val)); self } + /// Adds a function to generate global state at build-time. + #[cfg(target_arch = "wasm32")] + pub fn build_state_fn(self, _val: impl Fn() + 'static) -> Self { + self + } + /// Gets the global state at build-time. If no function was registered to this, we'll return `None`. + #[cfg(not(target_arch = "wasm32"))] pub async fn get_build_state(&self) -> Result, GlobalStateError> { if let Some(get_server_state) = &self.build { let res = get_server_state.call().await; diff --git a/packages/perseus/src/state/mod.rs b/packages/perseus/src/state/mod.rs index 4d95118cbe..64b0b8b895 100644 --- a/packages/perseus/src/state/mod.rs +++ b/packages/perseus/src/state/mod.rs @@ -8,20 +8,20 @@ pub use global_state::{GlobalState, GlobalStateCreator}; pub use page_state_store::PageStateStore; pub use rx_state::{AnyFreeze, Freeze, MakeRx, MakeUnrx}; -#[cfg(feature = "idb-freezing")] +#[cfg(all(feature = "idb-freezing", target_arch = "wasm32"))] mod freeze_idb; -#[cfg(feature = "idb-freezing")] +#[cfg(all(feature = "idb-freezing", target_arch = "wasm32"))] pub use freeze_idb::*; // TODO Be specific here // We'll allow live reloading (of which HSR is a subset) if it's feature-enabled and we're in development mode -#[cfg(all(feature = "live-reload", debug_assertions))] +#[cfg(all(feature = "live-reload", debug_assertions, target_arch = "wasm32"))] mod live_reload; -#[cfg(all(feature = "live-reload", debug_assertions))] +#[cfg(all(feature = "live-reload", debug_assertions, target_arch = "wasm32"))] pub(crate) use live_reload::connect_to_reload_server; -#[cfg(all(feature = "live-reload", debug_assertions))] +#[cfg(all(feature = "live-reload", debug_assertions, target_arch = "wasm32"))] pub use live_reload::force_reload; -#[cfg(all(feature = "hsr", debug_assertions))] +#[cfg(all(feature = "hsr", debug_assertions, target_arch = "wasm32"))] mod hsr; -#[cfg(all(feature = "hsr", debug_assertions))] +#[cfg(all(feature = "hsr", debug_assertions, target_arch = "wasm32"))] pub use hsr::*; // TODO diff --git a/packages/perseus/src/stores/immutable.rs b/packages/perseus/src/stores/immutable.rs index 700ab87730..30424a6f05 100644 --- a/packages/perseus/src/stores/immutable.rs +++ b/packages/perseus/src/stores/immutable.rs @@ -1,4 +1,6 @@ +#[cfg(not(target_arch = "wasm32"))] use crate::errors::*; +#[cfg(not(target_arch = "wasm32"))] use tokio::{ fs::{create_dir_all, File}, io::{AsyncReadExt, AsyncWriteExt}, @@ -11,14 +13,28 @@ use tokio::{ /// Note: the `.write()` methods on this implementation will create any missing parent directories automatically. #[derive(Clone, Debug)] pub struct ImmutableStore { + #[cfg(not(target_arch = "wasm32"))] root_path: String, } impl ImmutableStore { - /// Creates a new immutable store. You should provide a path like `dist/` here. + /// Creates a new immutable store. You should provide a path like `dist` here. Note that any trailing slashes will be automatically stripped. + #[cfg(not(target_arch = "wasm32"))] pub fn new(root_path: String) -> Self { + let root_path = root_path + .strip_prefix('/') + .unwrap_or(&root_path) + .to_string(); Self { root_path } } + /// Gets the filesystem path used for this immutable store. + /// + /// This is designed to be used in particular by the engine to work out where to put static assets and the like when exporting. + #[cfg(not(target_arch = "wasm32"))] + pub fn get_path(&self) -> &str { + &self.root_path + } /// Reads the given asset from the filesystem asynchronously. + #[cfg(not(target_arch = "wasm32"))] pub async fn read(&self, name: &str) -> Result { let asset_path = format!("{}/{}", self.root_path, name); let mut file = File::open(&asset_path) @@ -51,6 +67,7 @@ impl ImmutableStore { } /// Writes the given asset to the filesystem asynchronously. This must only be used at build-time, and must not be changed /// afterward. + #[cfg(not(target_arch = "wasm32"))] pub async fn write(&self, name: &str, content: &str) -> Result<(), StoreError> { let asset_path = format!("{}/{}", self.root_path, name); let mut dir_tree: Vec<&str> = asset_path.split('/').collect(); diff --git a/packages/perseus/src/stores/mutable.rs b/packages/perseus/src/stores/mutable.rs index cfa3fb4c43..562a610f9f 100644 --- a/packages/perseus/src/stores/mutable.rs +++ b/packages/perseus/src/stores/mutable.rs @@ -1,4 +1,5 @@ use crate::errors::*; +#[cfg(not(target_arch = "wasm32"))] use tokio::{ fs::{create_dir_all, File}, io::{AsyncReadExt, AsyncWriteExt}, @@ -21,17 +22,21 @@ pub trait MutableStore: std::fmt::Debug + Clone + Send + Sync { /// Note: the `.write()` methods on this implementation will create any missing parent directories automatically. #[derive(Clone, Debug)] pub struct FsMutableStore { + #[cfg(not(target_arch = "wasm32"))] root_path: String, } +#[cfg(not(target_arch = "wasm32"))] impl FsMutableStore { /// Creates a new filesystem configuration manager. You should provide a path like `dist/mutable` here. Make sure that this is /// not the same path as the immutable store, as this will cause potentially problematic overlap between the two systems. + #[cfg(not(target_arch = "wasm32"))] pub fn new(root_path: String) -> Self { Self { root_path } } } #[async_trait::async_trait] impl MutableStore for FsMutableStore { + #[cfg(not(target_arch = "wasm32"))] async fn read(&self, name: &str) -> Result { let asset_path = format!("{}/{}", self.root_path, name); let mut file = File::open(&asset_path) @@ -63,6 +68,7 @@ impl MutableStore for FsMutableStore { } } // This creates a directory structure as necessary + #[cfg(not(target_arch = "wasm32"))] async fn write(&self, name: &str, content: &str) -> Result<(), StoreError> { let asset_path = format!("{}/{}", self.root_path, name); let mut dir_tree: Vec<&str> = asset_path.split('/').collect(); @@ -98,4 +104,12 @@ impl MutableStore for FsMutableStore { Ok(()) } + #[cfg(target_arch = "wasm32")] + async fn read(&self, _name: &str) -> Result { + Ok(String::new()) + } + #[cfg(target_arch = "wasm32")] + async fn write(&self, _name: &str, _content: &str) -> Result<(), StoreError> { + Ok(()) + } } diff --git a/packages/perseus/src/template/core.rs b/packages/perseus/src/template/core.rs index 41be27d721..5bd464f4cd 100644 --- a/packages/perseus/src/template/core.rs +++ b/packages/perseus/src/template/core.rs @@ -1,17 +1,28 @@ // This file contains logic to define how templates are rendered -use super::{default_headers, PageProps, RenderCtx, States}; +#[cfg(not(target_arch = "wasm32"))] +use super::default_headers; +use super::PageProps; +#[cfg(not(target_arch = "wasm32"))] +use super::{RenderCtx, States}; use crate::errors::*; +#[cfg(not(target_arch = "wasm32"))] use crate::make_async_trait; use crate::translator::Translator; use crate::utils::provide_context_signal_replace; +#[cfg(not(target_arch = "wasm32"))] use crate::utils::AsyncFnReturn; use crate::Html; +#[cfg(not(target_arch = "wasm32"))] use crate::Request; +#[cfg(not(target_arch = "wasm32"))] use crate::SsrNode; +#[cfg(not(target_arch = "wasm32"))] use futures::Future; +#[cfg(not(target_arch = "wasm32"))] use http::header::HeaderMap; use sycamore::prelude::{Scope, View}; +#[cfg(not(target_arch = "wasm32"))] use sycamore::utils::hydrate::with_no_hydration_context; /// A generic error type that can be adapted for any errors the user may want to return from a render function. `.into()` can be used @@ -27,14 +38,17 @@ pub type RenderFnResult = std::result::Result = std::result::Result; // A series of asynchronous closure traits that prevent the user from having to pin their functions +#[cfg(not(target_arch = "wasm32"))] make_async_trait!(GetBuildPathsFnType, RenderFnResult>); // The build state strategy needs an error cause if it's invoked from incremental +#[cfg(not(target_arch = "wasm32"))] make_async_trait!( GetBuildStateFnType, RenderFnResultWithCause, path: String, locale: String ); +#[cfg(not(target_arch = "wasm32"))] make_async_trait!( GetRequestStateFnType, RenderFnResultWithCause, @@ -42,6 +56,7 @@ make_async_trait!( locale: String, req: Request ); +#[cfg(not(target_arch = "wasm32"))] make_async_trait!(ShouldRevalidateFnType, RenderFnResultWithCause); // A series of closure types that should not be typed out more than once @@ -52,18 +67,25 @@ pub type TemplateFn = Box View + Send + Sync>; /// A type alias for the function that modifies the document head. This is just a template function that will always be server-side /// rendered in function (it may be rendered on the client, but it will always be used to create an HTML string, rather than a reactive /// template). +#[cfg(not(target_arch = "wasm32"))] pub type HeadFn = TemplateFn; +#[cfg(not(target_arch = "wasm32"))] /// The type of functions that modify HTTP response headers. pub type SetHeadersFn = Box) -> HeaderMap + Send + Sync>; /// The type of functions that get build paths. +#[cfg(not(target_arch = "wasm32"))] pub type GetBuildPathsFn = Box; /// The type of functions that get build state. +#[cfg(not(target_arch = "wasm32"))] pub type GetBuildStateFn = Box; /// The type of functions that get request state. +#[cfg(not(target_arch = "wasm32"))] pub type GetRequestStateFn = Box; /// The type of functions that check if a template sghould revalidate. +#[cfg(not(target_arch = "wasm32"))] pub type ShouldRevalidateFn = Box; /// The type of functions that amalgamate build and request states. +#[cfg(not(target_arch = "wasm32"))] pub type AmalgamateStatesFn = Box RenderFnResultWithCause> + Send + Sync>; @@ -82,37 +104,46 @@ pub struct Template { /// A function that will be used to populate the document's `` with metadata such as the title. This will be passed state in /// the same way as `template`, but will always be rendered to a string, whcih will then be interpolated directly into the ``, /// so reactivity here will not work! + #[cfg(not(target_arch = "wasm32"))] head: TemplateFn, /// A function to be run when the server returns an HTTP response. This should return headers for said response, given the template's /// state. The most common use-case of this is to add cache control that respects revalidation. This will only be run on successful /// responses, and does have the power to override existing headers. By default, this will create sensible cache control headers. + #[cfg(not(target_arch = "wasm32"))] set_headers: SetHeadersFn, /// A function that gets the paths to render for at built-time. This is equivalent to `get_static_paths` in NextJS. If /// `incremental_generation` is `true`, more paths can be rendered at request time on top of these. + #[cfg(not(target_arch = "wasm32"))] get_build_paths: Option, /// Defines whether or not any new paths that match this template will be prerendered and cached in production. This allows you to /// have potentially billions of templates and retain a super-fast build process. The first user will have an ever-so-slightly slower /// experience, and everyone else gets the beneftis afterwards. This requires `get_build_paths`. Note that the template root will NOT /// be rendered on demand, and must be explicitly defined if it's wanted. It can uuse a different template. + #[cfg(not(target_arch = "wasm32"))] incremental_generation: bool, /// A function that gets the initial state to use to prerender the template at build time. This will be passed the path of the template, and /// will be run for any sub-paths. This is equivalent to `get_static_props` in NextJS. + #[cfg(not(target_arch = "wasm32"))] get_build_state: Option, /// A function that will run on every request to generate a state for that request. This allows server-side-rendering. This is equivalent /// to `get_server_side_props` in NextJS. This can be used with `get_build_state`, though custom amalgamation logic must be provided. + #[cfg(not(target_arch = "wasm32"))] get_request_state: Option, /// A function to be run on every request to check if a template prerendered at build-time should be prerendered again. This is equivalent /// to revalidation after a time in NextJS, with the improvement of custom logic. If used with `revalidate_after`, this function will /// only be run after that time period. This function will not be parsed anything specific to the request that invoked it. + #[cfg(not(target_arch = "wasm32"))] should_revalidate: Option, /// A length of time after which to prerender the template again. This is equivalent to revalidating in NextJS. This should specify a /// string interval to revalidate after. That will be converted into a datetime to wait for, which will be updated after every revalidation. /// Note that, if this is used with incremental generation, the counter will only start after the first render (meaning if you expect /// a weekly re-rendering cycle for all pages, they'd likely all be out of sync, you'd need to manually implement that with /// `should_revalidate`). + #[cfg(not(target_arch = "wasm32"))] revalidate_after: Option, /// Custom logic to amalgamate potentially different states generated at build and request time. This is only necessary if your template /// uses both `build_state` and `request_state`. If not specified and both are generated, request state will be prioritized. + #[cfg(not(target_arch = "wasm32"))] amalgamate_states: Option, } impl std::fmt::Debug for Template { @@ -122,34 +153,7 @@ impl std::fmt::Debug for Template { .field("template", &"TemplateFn") .field("head", &"HeadFn") .field("set_headers", &"SetHeadersFn") - .field( - "get_build_paths", - &self.get_build_paths.as_ref().map(|_| "GetBuildPathsFn"), - ) - .field( - "get_build_state", - &self.get_build_state.as_ref().map(|_| "GetBuildStateFn"), - ) - .field( - "get_request_state", - &self.get_request_state.as_ref().map(|_| "GetRequestStateFn"), - ) - .field( - "should_revalidate", - &self - .should_revalidate - .as_ref() - .map(|_| "ShouldRevalidateFn"), - ) - .field("revalidate_after", &self.revalidate_after) - .field( - "amalgamate_states", - &self - .amalgamate_states - .as_ref() - .map(|_| "AmalgamateStatesFn"), - ) - .field("incremental_generation", &self.incremental_generation) + // TODO Server-specific stuff .finish() } } @@ -160,21 +164,31 @@ impl Template { path: path.to_string(), template: Box::new(|cx, _| sycamore::view! { cx, }), // Unlike `template`, this may not be set at all (especially in very simple apps) + #[cfg(not(target_arch = "wasm32"))] head: Box::new(|cx, _| sycamore::view! { cx, }), // We create sensible header defaults here + #[cfg(not(target_arch = "wasm32"))] set_headers: Box::new(|_| default_headers()), + #[cfg(not(target_arch = "wasm32"))] get_build_paths: None, + #[cfg(not(target_arch = "wasm32"))] incremental_generation: false, + #[cfg(not(target_arch = "wasm32"))] get_build_state: None, + #[cfg(not(target_arch = "wasm32"))] get_request_state: None, + #[cfg(not(target_arch = "wasm32"))] should_revalidate: None, + #[cfg(not(target_arch = "wasm32"))] revalidate_after: None, + #[cfg(not(target_arch = "wasm32"))] amalgamate_states: None, } } // Render executors /// Executes the user-given function that renders the template on the client-side ONLY. This takes in an extsing global state. + #[cfg(target_arch = "wasm32")] #[allow(clippy::too_many_arguments)] pub fn render_for_template_client<'a>( &self, @@ -189,6 +203,7 @@ impl Template { (self.template)(cx, props) } /// Executes the user-given function that renders the template on the server-side ONLY. This automatically initializes an isolated global state. + #[cfg(not(target_arch = "wasm32"))] pub fn render_for_template_server<'a>( &self, props: PageProps, @@ -205,6 +220,7 @@ impl Template { } /// Executes the user-given function that renders the document ``, returning a string to be interpolated manually. /// Reactivity in this function will not take effect due to this string rendering. Note that this function will provide a translator context. + #[cfg(not(target_arch = "wasm32"))] pub fn render_head_str(&self, props: PageProps, translator: &Translator) -> String { sycamore::render_to_string(|cx| { // The context we have here has no context elements set on it, so we set all the defaults (job of the router component on the client-side) @@ -217,6 +233,7 @@ impl Template { }) } /// Gets the list of templates that should be prerendered for at build-time. + #[cfg(not(target_arch = "wasm32"))] pub async fn get_build_paths(&self) -> Result, ServerError> { if let Some(get_build_paths) = &self.get_build_paths { let res = get_build_paths.call().await; @@ -240,6 +257,7 @@ impl Template { /// Gets the initial state for a template. This needs to be passed the full path of the template, which may be one of those generated by /// `.get_build_paths()`. This also needs the locale being rendered to so that more compelx applications like custom documentation /// systems can be enabled. + #[cfg(not(target_arch = "wasm32"))] pub async fn get_build_state( &self, path: String, @@ -267,6 +285,7 @@ impl Template { /// Gets the request-time state for a template. This is equivalent to SSR, and will not be performed at build-time. Unlike /// `.get_build_paths()` though, this will be passed information about the request that triggered the render. Errors here can be caused /// by either the server or the client, so the user must specify an [`ErrorCause`]. This is also passed the locale being rendered to. + #[cfg(not(target_arch = "wasm32"))] pub async fn get_request_state( &self, path: String, @@ -294,6 +313,7 @@ impl Template { } /// Amalagmates given request and build states. Errors here can be caused by either the server or the client, so the user must specify /// an [`ErrorCause`]. + #[cfg(not(target_arch = "wasm32"))] pub fn amalgamate_states(&self, states: States) -> Result, ServerError> { if let Some(amalgamate_states) = &self.amalgamate_states { let res = amalgamate_states(states); @@ -317,6 +337,7 @@ impl Template { /// Checks, by the user's custom logic, if this template should revalidate. This function isn't presently parsed anything, but has /// network access etc., and can really do whatever it likes. Errors here can be caused by either the server or the client, so the /// user must specify an [`ErrorCause`]. + #[cfg(not(target_arch = "wasm32"))] pub async fn should_revalidate(&self) -> Result { if let Some(should_revalidate) = &self.should_revalidate { let res = should_revalidate.call().await; @@ -339,6 +360,7 @@ impl Template { } /// Gets the template's headers for the given state. These will be inserted into any successful HTTP responses for this template, /// and they have the power to override. + #[cfg(not(target_arch = "wasm32"))] pub fn get_headers(&self, state: Option) -> HeaderMap { (self.set_headers)(state) } @@ -350,45 +372,55 @@ impl Template { self.path.clone() } /// Gets the interval after which the template will next revalidate. + #[cfg(not(target_arch = "wasm32"))] pub fn get_revalidate_interval(&self) -> Option { self.revalidate_after.clone() } // Render characteristic checkers /// Checks if this template can revalidate existing prerendered templates. + #[cfg(not(target_arch = "wasm32"))] pub fn revalidates(&self) -> bool { self.should_revalidate.is_some() || self.revalidate_after.is_some() } /// Checks if this template can revalidate existing prerendered templates after a given time. + #[cfg(not(target_arch = "wasm32"))] pub fn revalidates_with_time(&self) -> bool { self.revalidate_after.is_some() } /// Checks if this template can revalidate existing prerendered templates based on some given logic. + #[cfg(not(target_arch = "wasm32"))] pub fn revalidates_with_logic(&self) -> bool { self.should_revalidate.is_some() } /// Checks if this template can render more templates beyond those paths it explicitly defines. + #[cfg(not(target_arch = "wasm32"))] pub fn uses_incremental(&self) -> bool { self.incremental_generation } /// Checks if this template is a template to generate paths beneath it. + #[cfg(not(target_arch = "wasm32"))] pub fn uses_build_paths(&self) -> bool { self.get_build_paths.is_some() } /// Checks if this template needs to do anything on requests for it. + #[cfg(not(target_arch = "wasm32"))] pub fn uses_request_state(&self) -> bool { self.get_request_state.is_some() } /// Checks if this template needs to do anything at build time. + #[cfg(not(target_arch = "wasm32"))] pub fn uses_build_state(&self) -> bool { self.get_build_state.is_some() } /// Checks if this template has custom logic to amalgamate build and reqquest states if both are generated. + #[cfg(not(target_arch = "wasm32"))] pub fn can_amalgamate_states(&self) -> bool { self.amalgamate_states.is_some() } /// Checks if this template defines no rendering logic whatsoever. Such templates will be rendered using SSG. Basic templates can /// still modify headers. + #[cfg(not(target_arch = "wasm32"))] pub fn is_basic(&self) -> bool { !self.uses_build_paths() && !self.uses_build_state() @@ -398,7 +430,9 @@ impl Template { } // Builder setters - // These will only be enabled under the `server-side` feature to prevent server-side code leaking into the Wasm binary (only the template setter is needed) + // The server-only ones have a different version for Wasm that takes in an empty function (this means we don't have to bring in function types, and therefore we + // can avoid bringing in the whole `http` module --- a very significant saving!) + // The macros handle the creation of empty functions to make user's lives easier /// Sets the template rendering function to use. pub fn template( mut self, @@ -407,117 +441,136 @@ impl Template { self.template = Box::new(val); self } + /// Sets the document head rendering function to use. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn head( mut self, val: impl Fn(Scope, PageProps) -> View + Send + Sync + 'static, ) -> Template { // Headers are always prerendered on the server-side - #[cfg(feature = "server-side")] - { - self.head = Box::new(val); - } + self.head = Box::new(val); + self + } + /// Sets the document head rendering function to use. + #[cfg(target_arch = "wasm32")] + pub fn head(self, _val: impl Fn() + 'static) -> Template { self } + /// Sets the function to set headers. This will override Perseus' inbuilt header defaults. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn set_headers_fn( mut self, val: impl Fn(Option) -> HeaderMap + Send + Sync + 'static, ) -> Template { - #[cfg(feature = "server-side")] - { - self.set_headers = Box::new(val); - } + self.set_headers = Box::new(val); + self + } + /// Sets the function to set headers. This will override Perseus' inbuilt header defaults. + #[cfg(target_arch = "wasm32")] + pub fn set_headers_fn(self, _val: impl Fn() + 'static) -> Template { self } + /// Enables the *build paths* strategy with the given function. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn build_paths_fn( mut self, val: impl GetBuildPathsFnType + Send + Sync + 'static, ) -> Template { - #[cfg(feature = "server-side")] - { - self.get_build_paths = Some(Box::new(val)); - } + self.get_build_paths = Some(Box::new(val)); self } + /// Enables the *build paths* strategy with the given function. + #[cfg(target_arch = "wasm32")] + pub fn build_paths_fn(self, _val: impl Fn() + 'static) -> Template { + self + } + /// Enables the *incremental generation* strategy. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn incremental_generation(mut self) -> Template { - #[cfg(feature = "server-side")] - { - self.incremental_generation = true; - } + self.incremental_generation = true; self } + /// Enables the *incremental generation* strategy. + #[cfg(target_arch = "wasm32")] + pub fn incremental_generation(self) -> Template { + self + } + /// Enables the *build state* strategy with the given function. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn build_state_fn( mut self, val: impl GetBuildStateFnType + Send + Sync + 'static, ) -> Template { - #[cfg(feature = "server-side")] - { - self.get_build_state = Some(Box::new(val)); - } + self.get_build_state = Some(Box::new(val)); + self + } + /// Enables the *build state* strategy with the given function. + #[cfg(target_arch = "wasm32")] + pub fn build_state_fn(self, _val: impl Fn() + 'static) -> Template { self } + /// Enables the *request state* strategy with the given function. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn request_state_fn( mut self, val: impl GetRequestStateFnType + Send + Sync + 'static, ) -> Template { - #[cfg(feature = "server-side")] - { - self.get_request_state = Some(Box::new(val)); - } + self.get_request_state = Some(Box::new(val)); + self + } + /// Enables the *request state* strategy with the given function. + #[cfg(target_arch = "wasm32")] + pub fn request_state_fn(self, _val: impl Fn() + 'static) -> Template { self } + /// Enables the *revalidation* strategy (logic variant) with the given function. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn should_revalidate_fn( mut self, val: impl ShouldRevalidateFnType + Send + Sync + 'static, ) -> Template { - #[cfg(feature = "server-side")] - { - self.should_revalidate = Some(Box::new(val)); - } + self.should_revalidate = Some(Box::new(val)); + self + } + /// Enables the *revalidation* strategy (logic variant) with the given function. + #[cfg(target_arch = "wasm32")] + pub fn should_revalidate_fn(self, _val: impl Fn() + 'static) -> Template { self } + /// Enables the *revalidation* strategy (time variant). This takes a time string of a form like `1w` for one week. More details are available /// [in the book](https://arctic-hen7.github.io/perseus/strategies/revalidation.html#time-syntax). - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn revalidate_after(mut self, val: String) -> Template { - #[cfg(feature = "server-side")] - { - self.revalidate_after = Some(val); - } + self.revalidate_after = Some(val); + self + } + /// Enables the *revalidation* strategy (time variant). This takes a time string of a form like `1w` for one week. More details are available + /// [in the book](https://arctic-hen7.github.io/perseus/strategies/revalidation.html#time-syntax). + #[cfg(target_arch = "wasm32")] + pub fn revalidate_after(self, _val: String) -> Template { self } + /// Enables state amalgamation with the given function. - #[allow(unused_mut)] - #[allow(unused_variables)] + #[cfg(not(target_arch = "wasm32"))] pub fn amalgamate_states_fn( mut self, val: impl Fn(States) -> RenderFnResultWithCause> + Send + Sync + 'static, ) -> Template { - #[cfg(feature = "server-side")] - { - self.amalgamate_states = Some(Box::new(val)); - } + self.amalgamate_states = Some(Box::new(val)); + self + } + /// Enables state amalgamation with the given function. + #[cfg(target_arch = "wasm32")] + pub fn amalgamate_states_fn(self, _val: impl Fn() + 'static) -> Template { self } } diff --git a/packages/perseus/src/template/mod.rs b/packages/perseus/src/template/mod.rs index 1886d134f2..d87479be82 100644 --- a/packages/perseus/src/template/mod.rs +++ b/packages/perseus/src/template/mod.rs @@ -1,13 +1,17 @@ mod core; // So called because this contains what is essentially the core exposed logic of Perseus +#[cfg(not(target_arch = "wasm32"))] mod default_headers; mod page_props; mod render_ctx; +#[cfg(not(target_arch = "wasm32"))] mod states; mod templates_map; pub use self::core::*; // There are a lot of render function traits in here, there's no point in spelling them all out +#[cfg(not(target_arch = "wasm32"))] pub use default_headers::default_headers; pub use page_props::PageProps; pub use render_ctx::RenderCtx; +#[cfg(not(target_arch = "wasm32"))] pub use states::States; pub use templates_map::{ArcTemplateMap, TemplateMap}; diff --git a/packages/perseus/src/template/render_ctx.rs b/packages/perseus/src/template/render_ctx.rs index d4fd3d78ed..dadbff26af 100644 --- a/packages/perseus/src/template/render_ctx.rs +++ b/packages/perseus/src/template/render_ctx.rs @@ -1,6 +1,5 @@ use crate::errors::*; use crate::router::{RouterLoadState, RouterState}; -#[cfg(all(feature = "live-reload", debug_assertions))] use crate::state::{ AnyFreeze, Freeze, FrozenApp, GlobalState, MakeRx, MakeUnrx, PageStateStore, ThawPrefs, }; diff --git a/packages/perseus/src/utils/mod.rs b/packages/perseus/src/utils/mod.rs index 1430d7b008..08f9dba318 100644 --- a/packages/perseus/src/utils/mod.rs +++ b/packages/perseus/src/utils/mod.rs @@ -1,13 +1,17 @@ mod async_fn_trait; +#[cfg(not(target_arch = "wasm32"))] mod cache_res; mod context; +#[cfg(not(target_arch = "wasm32"))] mod decode_time_str; mod log; mod path_prefix; mod test; pub use async_fn_trait::AsyncFnReturn; +#[cfg(not(target_arch = "wasm32"))] pub use cache_res::{cache_fallible_res, cache_res}; pub(crate) use context::provide_context_signal_replace; +#[cfg(not(target_arch = "wasm32"))] pub use decode_time_str::decode_time_str; -pub use path_prefix::{get_path_prefix_client, get_path_prefix_server}; +pub use path_prefix::*; diff --git a/packages/perseus/src/utils/path_prefix.rs b/packages/perseus/src/utils/path_prefix.rs index 03c84a1482..9d0318daa1 100644 --- a/packages/perseus/src/utils/path_prefix.rs +++ b/packages/perseus/src/utils/path_prefix.rs @@ -1,12 +1,11 @@ -use std::env; -use wasm_bindgen::JsCast; -use web_sys::{HtmlBaseElement, Url}; - /// Gets the path prefix to apply on the server. This uses the `PERSEUS_BASE_PATH` environment variable, which avoids hardcoding /// something as changeable as this into the final binary. Hence however, that variable must be the same as what's set in `` (done /// automatically). /// Trailing forward slashes will be trimmed automatically. +#[cfg(not(target_arch = "wasm32"))] pub fn get_path_prefix_server() -> String { + use std::env; + let base_path = env::var("PERSEUS_BASE_PATH").unwrap_or_else(|_| "".to_string()); base_path .strip_suffix('/') @@ -16,7 +15,11 @@ pub fn get_path_prefix_server() -> String { /// Gets the path prefix to apply in the browser. This uses the HTML `` element, which would be required anyway to make Sycamore's /// router co-operate with a relative path hosting. +#[cfg(target_arch = "wasm32")] pub fn get_path_prefix_client() -> String { + use wasm_bindgen::JsCast; + use web_sys::{HtmlBaseElement, Url}; + let base_path = match web_sys::window() .unwrap() .document() diff --git a/scripts/test.rs b/scripts/test.rs index 959a432941..37bba720dc 100644 --- a/scripts/test.rs +++ b/scripts/test.rs @@ -30,14 +30,14 @@ fn real_main() -> i32 { let shell_param = "-command"; let exec_name = { - // This is intended to be run from the root of the project + // This is intended to be run from the root of the project (which this script always will be because of Bonnie's requirements) let output = Command::new(shell_exec) .args([shell_param, &format!( - "bonnie dev example {category} {example} test --no-run --integration {integration}", + "bonnie dev example {category} {example} test --no-run", category=&category, example=&example, - integration=&integration )]) + .env("EXAMPLE_INTEGRATION", &integration) .output() .expect("couldn't build tests (command execution failed)"); let exit_code = match output.status.code() { @@ -57,11 +57,13 @@ fn real_main() -> i32 { }; // Run the server from that executable in the background - // This has to be run from the correct execution context (inside the `.perseus/server/` directory for teh target example) + // This has to be run from the correct execution context (inside the root of the target example) let mut server = Command::new(exec_name) - .current_dir(&format!("examples/{}/{}/.perseus/server", &category, &example)) + .current_dir(&format!("examples/{}/{}", &category, &example)) // Tell the server we're in testing mode .env("PERSEUS_TESTING", "true") + // We're in dev mode, so we have to tell the binary what to do + .env("PERSEUS_ENGINE_OPERATION", "serve") .spawn() .expect("couldn't start test server (command execution failed)"); @@ -69,8 +71,8 @@ fn real_main() -> i32 { let exit_code = { let output = Command::new(shell_exec) .current_dir(&format!("examples/{}/{}", &category, &example)) - .args([shell_param, "cargo test -- --test-threads 1"]) // TODO Confirm that this syntax works on Windows + .args([shell_param, &format!("cargo test --features 'perseus-integration/{}' -- --test-threads 1", &integration)]) .envs(if is_headless { vec![("PERSEUS_RUN_WASM_TESTS", "true"), ("PERSEUS_RUN_WASM_TESTS_HEADLESS", "true")] } else {