From 2fb434bb52eb422bde8db5ffa649d4ad94a8432e Mon Sep 17 00:00:00 2001 From: Alex Daigle Date: Wed, 26 Apr 2023 23:44:54 -0300 Subject: [PATCH] docs: fixed several typos (#272) * fix * recommendation --- docs/0.4.x/en-US/first-app/defining.md | 4 ++-- docs/0.4.x/en-US/first-app/dev-cycle.md | 2 +- docs/0.4.x/en-US/first-app/generating-pages.md | 2 +- docs/0.4.x/en-US/first-app/installation.md | 2 +- docs/0.4.x/en-US/fundamentals/plugins.md | 2 +- docs/0.4.x/en-US/fundamentals/preloading.md | 2 +- docs/0.4.x/en-US/what-is-perseus.md | 8 ++++---- docs/next/en-US/first-app/defining.md | 6 +++--- docs/next/en-US/first-app/dev-cycle.md | 2 +- docs/next/en-US/first-app/generating-pages.md | 2 +- docs/next/en-US/first-app/installation.md | 2 +- docs/next/en-US/fundamentals/plugins.md | 2 +- docs/next/en-US/fundamentals/preloading.md | 2 +- docs/next/en-US/what-is-perseus.md | 8 ++++---- packages/perseus/src/state/state_store.rs | 2 +- 15 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/0.4.x/en-US/first-app/defining.md b/docs/0.4.x/en-US/first-app/defining.md index c608188d8c..d762e3f041 100644 --- a/docs/0.4.x/en-US/first-app/defining.md +++ b/docs/0.4.x/en-US/first-app/defining.md @@ -1,6 +1,6 @@ # Defining a Perseus App -Once you've got all your dependencies installed, it's time to create the entrypoint to your Perseus app. In most Rust programs. you'll have a `main.rs` file that contains some `fn main() { .. }` that executes yuor code, and Perseus is no exception. However, remember that Perseus has two parts: the engine-side and the client-side, so you actually need *two* `main()` functions, one for each. Now, don't put anything in `src/main.rs` just yet, because, as we'll see later, there's actually a much more convenient way of handling all this. +Once you've got all your dependencies installed, it's time to create the entrypoint to your Perseus app. In most Rust programs. you'll have a `main.rs` file that contains some `fn main() { .. }` that executes your code, and Perseus is no exception. However, remember that Perseus has two parts: the engine-side and the client-side, so you actually need *two* `main()` functions, one for each. Now, don't put anything in `src/main.rs` just yet, because, as we'll see later, there's actually a much more convenient way of handling all this. Remember, you can tell Rust to only compile some code on the engine-side by putting `#[cfg(engine)]` over it, and you can use `#[cfg(client)]` to do the same for the browser. So, our code in `main.rs` should logically look something like this: @@ -18,7 +18,7 @@ fn main() { Now, this actually isn't too far off, except that running WebAssembly is a little different than you might think. Currently, there isn't really a good concept of a 'binary' Wasm program, you'll always be coding a library that some JavaScript imports and runs. In the case of Perseus apps, we use a `main.rs` file because it makes more logical sense, since Perseus handles all that nasty JS stuff behind the scenes. From your point of view, you're just writing a normal binary. However, there is something special that the client-side function has to do: it has to return a `Result<(), JsValue>`, where `JsValue` is a special type that represents *stuff* in JS-land. You can use Perseus' [`ClientReturn`](=type.ClientReturn@perseus) type alias for this, but note that Perseus actually *can't* return an error from its invocation: all errors are gracefully handled, even panics (although they will eventually propagate up as an unhandled exception in the calling JS, which is why any panics in Perseus will appear as two messages in your browser console rather than one). -Further, Perseus makes the engine and client code pretty convenient with two features (which are enabled by default): `dflt-engine`, and `client-helpers`. The first of these gives us the [`run_dftl_engine()`](=engine/fn.run_dflt_engine@perseus) fucntion, which takes an [`EngineOperation`](=engine/enum.EngineOperation@perseus) derived from the [`get_op()`](=engine/fn.get_op@perseus) function (which just parses environment variables passed through by the CLI), a function that returns a [`PerseusApp`](=prelude/struct.PerseusAppBase) (whcih we'll get to), and some function to run your server. +Further, Perseus makes the engine and client code pretty convenient with two features (which are enabled by default): `dflt-engine`, and `client-helpers`. The first of these gives us the [`run_dftl_engine()`](=engine/fn.run_dflt_engine@perseus) function, which takes an [`EngineOperation`](=engine/enum.EngineOperation@perseus) derived from the [`get_op()`](=engine/fn.get_op@perseus) function (which just parses environment variables passed through by the CLI), a function that returns a [`PerseusApp`](=prelude/struct.PerseusAppBase) (which we'll get to), and some function to run your server. As for the client-side, Perseus provides `run_client()`, which just takes a function that returns a `PerseusApp`. diff --git a/docs/0.4.x/en-US/first-app/dev-cycle.md b/docs/0.4.x/en-US/first-app/dev-cycle.md index 591ec4a18d..c801a16014 100644 --- a/docs/0.4.x/en-US/first-app/dev-cycle.md +++ b/docs/0.4.x/en-US/first-app/dev-cycle.md @@ -2,7 +2,7 @@ When you're developing a Perseus app, you'll generally have two "modes": coding, and fine-tuning. In the *coding* stage, you're building features of your app, which will typically involve quite a lot of working on business logic, etc. If you're familiar with Rust programming, this is the stage when you'd be using `cargo check` instead of `cargo run`. Conveniently, Perseus provides `perseus check -w` for this, which will not only `cargo check` your app's engine-side, but also the browser-side, because each one is built for a different target. This command is *much* faster than `perseus serve`, because it just checks your code, rather than actually compiling it. If you want to test your build logic as well, you can run `perseus check -gw`, which will also test this (but that will take a bit longer). -When you're using an IDE, like VS Code, you'll usually want proper syntax highlighting, and you may find that Perseus causea few problems. This is because Perseus distinguishes between the engine and the browser by using a custom feature, so you'll need to create a `.cargo/config.toml` file in the root of your project with the following contents: +When you're using an IDE, like VS Code, you'll usually want proper syntax highlighting, and you may find that Perseus can cause a few problems. This is because Perseus distinguishes between the engine and the browser by using a custom feature, so you'll need to create a `.cargo/config.toml` file in the root of your project with the following contents: ```toml [build] diff --git a/docs/0.4.x/en-US/first-app/generating-pages.md b/docs/0.4.x/en-US/first-app/generating-pages.md index b7cd73cb73..5418e58d4a 100644 --- a/docs/0.4.x/en-US/first-app/generating-pages.md +++ b/docs/0.4.x/en-US/first-app/generating-pages.md @@ -52,7 +52,7 @@ Those [`#[engine_only_fn]`](=prelude/attr.engine_only_fn@perseus) macros are ver You might be wondering about error handling on the engine-side: surely, if you're connecting to a database, you would need to return errors sometimes? What if the server building your app loses its internet connection? Well, you actually can return errors. In fact, try changing the return types of both `head` and `get_build_state` to return `Result`, where `T` was what they returned before. If you then wrap what they're returning in `Ok(..)`, there will be no errors. Perseus is designed to accept either fallible or infallible functions, and the error type can be whatever you like, as long as it implements `std::error::Error`. For `get_build_state` though, it's actually a tiny bit more complicated than this, as you'll need to wrap your error type in something called [`BlamedError`](prelude/struct.BlamedError@perseus), which you can learn more about in [the section on build-time state generation](:state/build). -And finally, we come to that famous `get_template` function, which we call from `PerseusApp` to get this whole template. This is responsible for producing a [`Template`](prelude/struct.Template@perseus) that strings everything together. This too takes a `G: Html` bound, and the `Template::build("index")` call is setting up a new template whose pages will all fal under `/index`, but `index` is a special name, and it resolves to an empty string. In other words, you're creating the template for the root of your site. Then we declare our build state function, out view function, and our head function. Since we're not actually using our state in the head, we could have used `.head()` instead of `.head_with_state()`, but we showed the state for demonstration purposes. Finally, we call `.build()` to create the full `Template`, which we return. +And finally, we come to that famous `get_template` function, which we call from `PerseusApp` to get this whole template. This is responsible for producing a [`Template`](prelude/struct.Template@perseus) that strings everything together. This too takes a `G: Html` bound, and the `Template::build("index")` call is setting up a new template whose pages will all fall under `/index`, but `index` is a special name, and it resolves to an empty string. In other words, you're creating the template for the root of your site. Then we declare our build state function, out view function, and our head function. Since we're not actually using our state in the head, we could have used `.head()` instead of `.head_with_state()`, but we showed the state for demonstration purposes. Finally, we call `.build()` to create the full `Template`, which we return. This is called the *functional definition* pattern in Perseus: you define your `Template`s inside functions (usually called `get_template()`), which you then call in `PerseusApp`. diff --git a/docs/0.4.x/en-US/first-app/installation.md b/docs/0.4.x/en-US/first-app/installation.md index 80a1a378d2..8e7375d28a 100644 --- a/docs/0.4.x/en-US/first-app/installation.md +++ b/docs/0.4.x/en-US/first-app/installation.md @@ -32,7 +32,7 @@ Next, put the following in your app's `Cargo.toml`: {{#include ../../../examples/core/basic/Cargo.toml.example}} ``` -The main things to pay attention to here are the dependencies, which are laid out differently from most Rust apps. Perseus is built in two parts: the *engine-side*, which is responsible for prerendering your pages, serving content, exporting your app, etc.; and the *client-side*, which runs inside a user's browser to make Perseus interactive, handling routing, interactivity, etc. The engine-side of your app will build to whatever target you compile it for, like `x86_64-unknown-linux-gnu`, which you would have on an OS like Ubuntu. This means Rust will translate your code into machine code that computers with that kind of processor and OS can understand (if you were running on an M1 Mac, the target would be quite different). The browser has its own sepaarate target, which ensures that you don't have to compile your code for every possible device that a user might view it on --- the browser takes care of all that, and runs Wasm, which is its own special language that Rust can translate itself into. +The main things to pay attention to here are the dependencies, which are laid out differently from most Rust apps. Perseus is built in two parts: the *engine-side*, which is responsible for prerendering your pages, serving content, exporting your app, etc.; and the *client-side*, which runs inside a user's browser to make Perseus interactive, handling routing, interactivity, etc. The engine-side of your app will build to whatever target you compile it for, like `x86_64-unknown-linux-gnu`, which you would have on an OS like Ubuntu. This means Rust will translate your code into machine code that computers with that kind of processor and OS can understand (if you were running on an M1 Mac, the target would be quite different). The browser has its own separate target, which ensures that you don't have to compile your code for every possible device that a user might view it on --- the browser takes care of all that, and runs Wasm, which is its own special language that Rust can translate itself into. That all means that there are some features that don't belong in the browser (like building your app), and others that don't belong in the engine (like managing routing), so Perseus *target-gates* these, using Rust's `#[cfg(..)]` macro to make sure that certain things are only compiled at the right time. This reduces compilation times, and also slims down the bundles for both the engine and the browser (because they contain no unnecessary code). Sometimes, you'll want to do this in your own code as well, like if you have some function that should only run on the browser-side. Remember how we set up that `rustflags` key in `.cargo/config.toml`? Well, that's so you can use it just like this! If you want code to only be compiled for the browser, you put `#[cfg(client)]` on top of it, and you can use `#[cfg(engine)]` to do the same for the engine. You'll usually see this in Rust code, but your `Cargo.toml` can use it too for declaring dependencies that will only be used on one particular target. Here, we're making sure to bring in `perseus` everywhere, but `perseus-warp` (our server integration) should only be used on the engine-side. When you bring in a new dependency, think about whether it has to be available on the browser-side, because it often doesn't. For example, you could bring in the `regex` crate to automatically highlight any technical terms in a documentation site, but you can actually do that solely on the engine-side if you handle all that in the state generation process (which we'll get to). This avoids bringing the `regex` crate into the browser, which keeps your `.wasm` bundle nice and slim. A smaller Wasm bundle means it can be transferred over the network more quickly, which means faster page loads. diff --git a/docs/0.4.x/en-US/fundamentals/plugins.md b/docs/0.4.x/en-US/fundamentals/plugins.md index f323697c2e..3083c057b1 100644 --- a/docs/0.4.x/en-US/fundamentals/plugins.md +++ b/docs/0.4.x/en-US/fundamentals/plugins.md @@ -4,7 +4,7 @@ One of the most powerful features of Perseus is its extensibility, which comes i However, 99.99% of the time, you won't need to do any of this, because your needs will be met far more effectively by either a [custom server](:fundamentals/serving-exporting), or a plugin. -Plugins in Perseus are library crates, usually published on crates.io, that can be used as dependencies in your app that have access to various *plugin opportunities*, whcih are basically points in your app where Perseus allows third-party code to do certain things. +Plugins in Perseus are library crates, usually published on crates.io, that can be used as dependencies in your app that have access to various *plugin opportunities*, which are basically points in your app where Perseus allows third-party code to do certain things. Plugins fall into two types: *functional* and *control*. Functional plugins are very simple: they're given some data at a certain time, they do some stuff, and then they return some data. For a single plugin opportunity, there can be as many functional plugins registered as you like. For example, a plugin can define extra static aliases, and, of course, the results of many plugins doing this can all be collated together. Control plugins work differently: for a single control plugin opportunity, only one plugins can act, for example redefining the index view (since you can't necessarily combine two completely different index views from two completely different plugins). diff --git a/docs/0.4.x/en-US/fundamentals/preloading.md b/docs/0.4.x/en-US/fundamentals/preloading.md index 083fe38d38..6847334cec 100644 --- a/docs/0.4.x/en-US/fundamentals/preloading.md +++ b/docs/0.4.x/en-US/fundamentals/preloading.md @@ -2,7 +2,7 @@ One superpower of Perseus is its caching system, which takes any pages the user has already been to, figures out the minimium amount of information necessary to restore them without any network requests, and stores that, ensuring that pressing the back button leads to an instant response. Sometimes, however, you want this to work in the other direction too: if you are fairly confident of which page a user will go to next, you can *preload* it to make sure they get the content immediately. -Now, usually you would do preloading through the browser, whcih will fetch resources intelligently to minimize load times, but, again, Perseus knows better than the browser in a lot of cases. To render a new page, all it needs is the page's state and its document metadata, which actually come from a special internal link (behind `/.perseus/page`). Preloading this through the browser is finicky, and it doesn't allow Perseus to do some pre-parsing to keep things speedy, so Perseus provides its own imperative preloading interface. +Now, usually you would do preloading through the browser, which will fetch resources intelligently to minimize load times, but, again, Perseus knows better than the browser in a lot of cases. To render a new page, all it needs is the page's state and its document metadata, which actually come from a special internal link (behind `/.perseus/page`). Preloading this through the browser is finicky, and it doesn't allow Perseus to do some pre-parsing to keep things speedy, so Perseus provides its own imperative preloading interface. There are two ways of using this interface: there's the easy way, and the fine-grained way. The easy way is to use the `.preload()` method on the `Reactor`, which spawns a future for you and panics on errors that you caused (like a misspelled route), while silently failing on errors from the server. Alternately, you could use the `.try_preload()` method, which lets you handle the errors, and forces you to manage the asynchronicity yourself. If you want more control over the error handling (which applies especially if you're preloading a route that you haven't hardcoded), then you should use this method instead. diff --git a/docs/0.4.x/en-US/what-is-perseus.md b/docs/0.4.x/en-US/what-is-perseus.md index ec6bb8583f..e4a0a3201d 100644 --- a/docs/0.4.x/en-US/what-is-perseus.md +++ b/docs/0.4.x/en-US/what-is-perseus.md @@ -8,15 +8,15 @@ We can obviously agree that Rust is much better than JavaScript: it's way faster Now, you might have come across other web development libraries and frameworks for Rust before, but there's a big difference between those two terms, so let's sort that out first. A *library* is a piece of code that you use to help you build your site. A *framework* is a mammoth of code that uses your code to build your site. Think of it like the difference between `futures::executor::block_on` and `#[tokio::main]`: one is being used by you to handle a bit of `async`, and the other is using your code to handle *all* the `async`. In the same way, a library is a great choice for when you want to build a small site, or when you want to replace just part of a site with Rust. For these kinds of things, we absolutely recommend [Sycamore](https://github.com/sycamore-rs/sycamore), on which Perseus is based. -However, sometimes you'll need to break out the big guns. Sometimes, you'll need to render content in advance so that your users see it straight away, rather than a blank page while your Wasm boots up. Sometimes, you'll want to have a*stateful* app. This doesn't just mean you've got buttons and forms, etc., but that you're building your app in a special kind of pattern, which Perseus is built around. Let's say you have a simple static blog: you might have a `/post` URL, under which all your posts can be found. Fundamentally, all these posts have the same structure, just with different titles, dates, tags, and contents, so you might choose to create some kind of *template* for them, and then maybe build a Markdown parser or the like to push all that into your app to create *pages*. Essentially, **template + state = page**. In Perseus, this is all handled for you, and you just create templates, like `/post`, along with ways to render their state. +However, sometimes you'll need to break out the big guns. Sometimes, you'll need to render content in advance so that your users see it straight away, rather than a blank page while your Wasm boots up. Sometimes, you'll want to have a *stateful* app. This doesn't just mean you've got buttons and forms, etc., but that you're building your app in a special kind of pattern, which Perseus is built around. Let's say you have a simple static blog: you might have a `/post` URL, under which all your posts can be found. Fundamentally, all these posts have the same structure, just with different titles, dates, tags, and contents, so you might choose to create some kind of *template* for them, and then maybe build a Markdown parser or the like to push all that into your app to create *pages*. Essentially, **template + state = page**. In Perseus, this is all handled for you, and you just create templates, like `/post`, along with ways to render their state. -For example, for a blog, you might create a new post template with `Template::build("post")`, and then create a function that takes in some state and plugs it into a Sycamore `view! { .. }` to render some content. You might take in a `struct` containining contents, titles, tags, etc. If you then specify a function that can list the pages that this template should create (e.g. by getting all the Markdown files in a certain directory), and then another one that takes each path and generates state for it, Perseus will string it all together and give a lightning-fast app. +For example, for a blog, you might create a new post template with `Template::build("post")`, and then create a function that takes in some state and plugs it into a Sycamore `view! { .. }` to render some content. You might take in a `struct` containing contents, titles, tags, etc. If you then specify a function that can list the pages that this template should create (e.g. by getting all the Markdown files in a certain directory), and then another one that takes each path and generates state for it, Perseus will string it all together and give a lightning-fast app. Beyond this, Perseus has all sorts of extra features, like inbuilt error handling systems that allow you to gracefully display error messages if state generation fails, or if your app panics, or something else like that. All you do is match an `enum ClientError`, and Perseus shows your errors to the client. Beyond that, if you want to build an app in multiple languages, Perseus will let you do it straight away: just replace the text in your code with identifiers inside the `t!()` macro, and define a map of translation IDs to text for each language you want to support. Variable interpolation is supported out of the box, and you can unleash the full power of [Fluent](https://projectfluent.org) for handling pluralization rules, genders, etc. -Going even further, Perseus' state generation platform is built for even the most advanced use-cases: let's say you have not a blog, but an ecommerce site selling a thousand products. Well, a thousand would actually build very quickly, so perhaps a million. Still probably looking at less than a second, but we'll go with it. Maybe you don't want to build all that at build time. Simple! Just add `.incremental_generation()` to your template definition and then...you're done. If a user goes to a product page that doesn't exist yet, it will passed to your state generation functions, and, if it's a page that exists, those functions can produce the page, and Perseus will serve it. For any future users, that page will be cached and returned immediately. It's like building your whole app over time, on-demand. And, if you have an index of all your products, you could automatically *revalidate* that every, say, 24 hours, to make sure users have a fairly up to date listing. Or you could logic-based revalidation that checks each time whether or not there are actually any new products, before rebuilding. You could even combine the two: only check every few hours whether or not there are new products, and, if there are, rebuild that page. +Going even further, Perseus' state generation platform is built for even the most advanced use-cases: let's say you have not a blog, but an ecommerce site selling a thousand products. Well, a thousand would actually build very quickly, so perhaps a million. Still probably looking at less than a second, but we'll go with it. Maybe you don't want to build all that at build time. Simple! Just add `.incremental_generation()` to your template definition and then...you're done. If a user goes to a product page that doesn't exist yet, it will be passed to your state generation functions, and, if it's a page that exists, those functions can produce the page, and Perseus will serve it. For any future users, that page will be cached and returned immediately. It's like building your whole app over time, on-demand. And, if you have an index of all your products, you could automatically *revalidate* that every, say, 24 hours, to make sure users have a fairly up to date listing. Or you could logic-based revalidation that checks each time whether or not there are actually any new products, before rebuilding. You could even combine the two: only check every few hours whether or not there are new products, and, if there are, rebuild that page. -To be clear, and this is important if you aren't familiar with web development, Perseus is not a library, it's a framework. It's a giant engine into which you plug your code that will connect everything together and optimize it, producing a super-fast site that outperforms every JS framework under the sun. It might well seem like you don't need a lot of these features, and, if you don't, you can just run `perseus export` to get a series of static HTML files that you can serve to users however you like, with a simple Wasm bundle making sure whatever interactivity you have works as smoothly as possible (and it will still be unreasonably fast). If you're used to systems programming, the whole idea of a framework might seem a bit absurd, but it's very often required in web development, simply because the best experiences come from complex features, like rendering your site to HTML in advance, or caching transations, or delayable capsules that can be infinitely nested to create lazy-loaded pages, etc. Some of these are easy to implement, others are not. The point of Perseus is to handle this all for you so you can get on with what you want to write: your app. Even better, Perseus is built on [Sycamore](https://github.com/sycamore-rs/sycamore), which handles reactivity primitives, meaning there is a separation of concerns, unlike with other current Rust frameworks: one team is in charge of the reactivity, and another in charge of the framework, meaning more features are developed more quickly, and bugs are fixed more rapidly, while both systems remain fantastically maintainted. +To be clear, and this is important if you aren't familiar with web development, Perseus is not a library, it's a framework. It's a giant engine into which you plug your code that will connect everything together and optimize it, producing a super-fast site that outperforms every JS framework under the sun. It might well seem like you don't need a lot of these features, and, if you don't, you can just run `perseus export` to get a series of static HTML files that you can serve to users however you like, with a simple Wasm bundle making sure whatever interactivity you have works as smoothly as possible (and it will still be unreasonably fast). If you're used to systems programming, the whole idea of a framework might seem a bit absurd, but it's very often required in web development, simply because the best experiences come from complex features, like rendering your site to HTML in advance, or caching transactions, or delayable capsules that can be infinitely nested to create lazy-loaded pages, etc. Some of these are easy to implement, others are not. The point of Perseus is to handle this all for you so you can get on with what you want to write: your app. Even better, Perseus is built on [Sycamore](https://github.com/sycamore-rs/sycamore), which handles reactivity primitives, meaning there is a separation of concerns, unlike with other current Rust frameworks: one team is in charge of the reactivity, and another in charge of the framework, meaning more features are developed more quickly, and bugs are fixed more rapidly, while both systems remain fantastically . If Perseus doesn't sound like your cup of tea, there are several other Rust frameworks you might like to check out: [Sycamore](https://github.com/sycamore-rs/sycamore) is the library on which Perseus is based, if you want to keep the same sort of style; [Yew](https://yew.rs) is a very popular library; and [Seed](https://seed-rs.org) is another. There's also [Sauron](https://github.com/ivanceras/sauron), [MoonZoon](https://github.com/MoonZoon/MoonZoon), and [Leptos](https://github.com/leptos-rs/leptos), just to name a few. If you'd like to see some more in-depth comparisons between these projects, check out [the comparisons page](comparisons). diff --git a/docs/next/en-US/first-app/defining.md b/docs/next/en-US/first-app/defining.md index c608188d8c..df0fac3523 100644 --- a/docs/next/en-US/first-app/defining.md +++ b/docs/next/en-US/first-app/defining.md @@ -1,6 +1,6 @@ # Defining a Perseus App -Once you've got all your dependencies installed, it's time to create the entrypoint to your Perseus app. In most Rust programs. you'll have a `main.rs` file that contains some `fn main() { .. }` that executes yuor code, and Perseus is no exception. However, remember that Perseus has two parts: the engine-side and the client-side, so you actually need *two* `main()` functions, one for each. Now, don't put anything in `src/main.rs` just yet, because, as we'll see later, there's actually a much more convenient way of handling all this. +Once you've got all your dependencies installed, it's time to create the entrypoint to your Perseus app. In most Rust programs. you'll have a `main.rs` file that contains some `fn main() { .. }` that executes your code, and Perseus is no exception. However, remember that Perseus has two parts: the engine-side and the client-side, so you actually need *two* `main()` functions, one for each. Now, don't put anything in `src/main.rs` just yet, because, as we'll see later, there's actually a much more convenient way of handling all this. Remember, you can tell Rust to only compile some code on the engine-side by putting `#[cfg(engine)]` over it, and you can use `#[cfg(client)]` to do the same for the browser. So, our code in `main.rs` should logically look something like this: @@ -18,11 +18,11 @@ fn main() { Now, this actually isn't too far off, except that running WebAssembly is a little different than you might think. Currently, there isn't really a good concept of a 'binary' Wasm program, you'll always be coding a library that some JavaScript imports and runs. In the case of Perseus apps, we use a `main.rs` file because it makes more logical sense, since Perseus handles all that nasty JS stuff behind the scenes. From your point of view, you're just writing a normal binary. However, there is something special that the client-side function has to do: it has to return a `Result<(), JsValue>`, where `JsValue` is a special type that represents *stuff* in JS-land. You can use Perseus' [`ClientReturn`](=type.ClientReturn@perseus) type alias for this, but note that Perseus actually *can't* return an error from its invocation: all errors are gracefully handled, even panics (although they will eventually propagate up as an unhandled exception in the calling JS, which is why any panics in Perseus will appear as two messages in your browser console rather than one). -Further, Perseus makes the engine and client code pretty convenient with two features (which are enabled by default): `dflt-engine`, and `client-helpers`. The first of these gives us the [`run_dftl_engine()`](=engine/fn.run_dflt_engine@perseus) fucntion, which takes an [`EngineOperation`](=engine/enum.EngineOperation@perseus) derived from the [`get_op()`](=engine/fn.get_op@perseus) function (which just parses environment variables passed through by the CLI), a function that returns a [`PerseusApp`](=prelude/struct.PerseusAppBase) (whcih we'll get to), and some function to run your server. +Further, Perseus makes the engine and client code pretty convenient with two features (which are enabled by default): `dflt-engine`, and `client-helpers`. The first of these gives us the [`run_dftl_engine()`](=engine/fn.run_dflt_engine@perseus) function, which takes an [`EngineOperation`](=engine/enum.EngineOperation@perseus) derived from the [`get_op()`](=engine/fn.get_op@perseus) function (which just parses environment variables passed through by the CLI), a function that returns a [`PerseusApp`](=prelude/struct.PerseusAppBase) (which we'll get to), and some function to run your server. As for the client-side, Perseus provides `run_client()`, which just takes a function that returns a `PerseusApp`. -So what is this `PerseusApp`, you might ask? This `struct` forms the bridge between Perseus' internals, and your own code, because it's how you tell Perseus what your app looks like. In fact, because the vast majority of engine and client `main()` functions are so formulaic, Perseus provides a convenient macro, [`#[perseus::main(..)]`](=attr.main@perseus), which you can use to annotate a *single* `main()` function that returns a `PerseusApp`, and that macro will then do the rest automatically. Most of time, this is what you want, but you can always take a look at [the source code]() of that macro if you want to drill deeper into customizing your app (again, you will probably *never* need to do this, even if you're creating an insanely advanced app). +So what is this `PerseusApp`, you might ask? This `struct` forms the bridge between Perseus' internals, and your own code, because it's how you tell Perseus what your app looks like. In fact, because the vast majority of engine and client `main()` functions are so formulaic, Perseus provides a convenient macro, [`#[perseus::main(..)]`](=attr.main@perseus), which you can use to annotate a *single* `main()` function that returns a `PerseusApp`, and that macro will then do the rest automatically. Most of the time, this is what you want, but you can always take a look at [the source code]() of that macro if you want to drill deeper into customizing your app (again, you will probably *never* need to do this, even if you're creating an insanely advanced app). So, our actual `src/main.rs` file would look something like this (theory over, *now* we start coding): diff --git a/docs/next/en-US/first-app/dev-cycle.md b/docs/next/en-US/first-app/dev-cycle.md index 591ec4a18d..c801a16014 100644 --- a/docs/next/en-US/first-app/dev-cycle.md +++ b/docs/next/en-US/first-app/dev-cycle.md @@ -2,7 +2,7 @@ When you're developing a Perseus app, you'll generally have two "modes": coding, and fine-tuning. In the *coding* stage, you're building features of your app, which will typically involve quite a lot of working on business logic, etc. If you're familiar with Rust programming, this is the stage when you'd be using `cargo check` instead of `cargo run`. Conveniently, Perseus provides `perseus check -w` for this, which will not only `cargo check` your app's engine-side, but also the browser-side, because each one is built for a different target. This command is *much* faster than `perseus serve`, because it just checks your code, rather than actually compiling it. If you want to test your build logic as well, you can run `perseus check -gw`, which will also test this (but that will take a bit longer). -When you're using an IDE, like VS Code, you'll usually want proper syntax highlighting, and you may find that Perseus causea few problems. This is because Perseus distinguishes between the engine and the browser by using a custom feature, so you'll need to create a `.cargo/config.toml` file in the root of your project with the following contents: +When you're using an IDE, like VS Code, you'll usually want proper syntax highlighting, and you may find that Perseus can cause a few problems. This is because Perseus distinguishes between the engine and the browser by using a custom feature, so you'll need to create a `.cargo/config.toml` file in the root of your project with the following contents: ```toml [build] diff --git a/docs/next/en-US/first-app/generating-pages.md b/docs/next/en-US/first-app/generating-pages.md index b7cd73cb73..5418e58d4a 100644 --- a/docs/next/en-US/first-app/generating-pages.md +++ b/docs/next/en-US/first-app/generating-pages.md @@ -52,7 +52,7 @@ Those [`#[engine_only_fn]`](=prelude/attr.engine_only_fn@perseus) macros are ver You might be wondering about error handling on the engine-side: surely, if you're connecting to a database, you would need to return errors sometimes? What if the server building your app loses its internet connection? Well, you actually can return errors. In fact, try changing the return types of both `head` and `get_build_state` to return `Result`, where `T` was what they returned before. If you then wrap what they're returning in `Ok(..)`, there will be no errors. Perseus is designed to accept either fallible or infallible functions, and the error type can be whatever you like, as long as it implements `std::error::Error`. For `get_build_state` though, it's actually a tiny bit more complicated than this, as you'll need to wrap your error type in something called [`BlamedError`](prelude/struct.BlamedError@perseus), which you can learn more about in [the section on build-time state generation](:state/build). -And finally, we come to that famous `get_template` function, which we call from `PerseusApp` to get this whole template. This is responsible for producing a [`Template`](prelude/struct.Template@perseus) that strings everything together. This too takes a `G: Html` bound, and the `Template::build("index")` call is setting up a new template whose pages will all fal under `/index`, but `index` is a special name, and it resolves to an empty string. In other words, you're creating the template for the root of your site. Then we declare our build state function, out view function, and our head function. Since we're not actually using our state in the head, we could have used `.head()` instead of `.head_with_state()`, but we showed the state for demonstration purposes. Finally, we call `.build()` to create the full `Template`, which we return. +And finally, we come to that famous `get_template` function, which we call from `PerseusApp` to get this whole template. This is responsible for producing a [`Template`](prelude/struct.Template@perseus) that strings everything together. This too takes a `G: Html` bound, and the `Template::build("index")` call is setting up a new template whose pages will all fall under `/index`, but `index` is a special name, and it resolves to an empty string. In other words, you're creating the template for the root of your site. Then we declare our build state function, out view function, and our head function. Since we're not actually using our state in the head, we could have used `.head()` instead of `.head_with_state()`, but we showed the state for demonstration purposes. Finally, we call `.build()` to create the full `Template`, which we return. This is called the *functional definition* pattern in Perseus: you define your `Template`s inside functions (usually called `get_template()`), which you then call in `PerseusApp`. diff --git a/docs/next/en-US/first-app/installation.md b/docs/next/en-US/first-app/installation.md index 80a1a378d2..8e7375d28a 100644 --- a/docs/next/en-US/first-app/installation.md +++ b/docs/next/en-US/first-app/installation.md @@ -32,7 +32,7 @@ Next, put the following in your app's `Cargo.toml`: {{#include ../../../examples/core/basic/Cargo.toml.example}} ``` -The main things to pay attention to here are the dependencies, which are laid out differently from most Rust apps. Perseus is built in two parts: the *engine-side*, which is responsible for prerendering your pages, serving content, exporting your app, etc.; and the *client-side*, which runs inside a user's browser to make Perseus interactive, handling routing, interactivity, etc. The engine-side of your app will build to whatever target you compile it for, like `x86_64-unknown-linux-gnu`, which you would have on an OS like Ubuntu. This means Rust will translate your code into machine code that computers with that kind of processor and OS can understand (if you were running on an M1 Mac, the target would be quite different). The browser has its own sepaarate target, which ensures that you don't have to compile your code for every possible device that a user might view it on --- the browser takes care of all that, and runs Wasm, which is its own special language that Rust can translate itself into. +The main things to pay attention to here are the dependencies, which are laid out differently from most Rust apps. Perseus is built in two parts: the *engine-side*, which is responsible for prerendering your pages, serving content, exporting your app, etc.; and the *client-side*, which runs inside a user's browser to make Perseus interactive, handling routing, interactivity, etc. The engine-side of your app will build to whatever target you compile it for, like `x86_64-unknown-linux-gnu`, which you would have on an OS like Ubuntu. This means Rust will translate your code into machine code that computers with that kind of processor and OS can understand (if you were running on an M1 Mac, the target would be quite different). The browser has its own separate target, which ensures that you don't have to compile your code for every possible device that a user might view it on --- the browser takes care of all that, and runs Wasm, which is its own special language that Rust can translate itself into. That all means that there are some features that don't belong in the browser (like building your app), and others that don't belong in the engine (like managing routing), so Perseus *target-gates* these, using Rust's `#[cfg(..)]` macro to make sure that certain things are only compiled at the right time. This reduces compilation times, and also slims down the bundles for both the engine and the browser (because they contain no unnecessary code). Sometimes, you'll want to do this in your own code as well, like if you have some function that should only run on the browser-side. Remember how we set up that `rustflags` key in `.cargo/config.toml`? Well, that's so you can use it just like this! If you want code to only be compiled for the browser, you put `#[cfg(client)]` on top of it, and you can use `#[cfg(engine)]` to do the same for the engine. You'll usually see this in Rust code, but your `Cargo.toml` can use it too for declaring dependencies that will only be used on one particular target. Here, we're making sure to bring in `perseus` everywhere, but `perseus-warp` (our server integration) should only be used on the engine-side. When you bring in a new dependency, think about whether it has to be available on the browser-side, because it often doesn't. For example, you could bring in the `regex` crate to automatically highlight any technical terms in a documentation site, but you can actually do that solely on the engine-side if you handle all that in the state generation process (which we'll get to). This avoids bringing the `regex` crate into the browser, which keeps your `.wasm` bundle nice and slim. A smaller Wasm bundle means it can be transferred over the network more quickly, which means faster page loads. diff --git a/docs/next/en-US/fundamentals/plugins.md b/docs/next/en-US/fundamentals/plugins.md index f323697c2e..3083c057b1 100644 --- a/docs/next/en-US/fundamentals/plugins.md +++ b/docs/next/en-US/fundamentals/plugins.md @@ -4,7 +4,7 @@ One of the most powerful features of Perseus is its extensibility, which comes i However, 99.99% of the time, you won't need to do any of this, because your needs will be met far more effectively by either a [custom server](:fundamentals/serving-exporting), or a plugin. -Plugins in Perseus are library crates, usually published on crates.io, that can be used as dependencies in your app that have access to various *plugin opportunities*, whcih are basically points in your app where Perseus allows third-party code to do certain things. +Plugins in Perseus are library crates, usually published on crates.io, that can be used as dependencies in your app that have access to various *plugin opportunities*, which are basically points in your app where Perseus allows third-party code to do certain things. Plugins fall into two types: *functional* and *control*. Functional plugins are very simple: they're given some data at a certain time, they do some stuff, and then they return some data. For a single plugin opportunity, there can be as many functional plugins registered as you like. For example, a plugin can define extra static aliases, and, of course, the results of many plugins doing this can all be collated together. Control plugins work differently: for a single control plugin opportunity, only one plugins can act, for example redefining the index view (since you can't necessarily combine two completely different index views from two completely different plugins). diff --git a/docs/next/en-US/fundamentals/preloading.md b/docs/next/en-US/fundamentals/preloading.md index 083fe38d38..6847334cec 100644 --- a/docs/next/en-US/fundamentals/preloading.md +++ b/docs/next/en-US/fundamentals/preloading.md @@ -2,7 +2,7 @@ One superpower of Perseus is its caching system, which takes any pages the user has already been to, figures out the minimium amount of information necessary to restore them without any network requests, and stores that, ensuring that pressing the back button leads to an instant response. Sometimes, however, you want this to work in the other direction too: if you are fairly confident of which page a user will go to next, you can *preload* it to make sure they get the content immediately. -Now, usually you would do preloading through the browser, whcih will fetch resources intelligently to minimize load times, but, again, Perseus knows better than the browser in a lot of cases. To render a new page, all it needs is the page's state and its document metadata, which actually come from a special internal link (behind `/.perseus/page`). Preloading this through the browser is finicky, and it doesn't allow Perseus to do some pre-parsing to keep things speedy, so Perseus provides its own imperative preloading interface. +Now, usually you would do preloading through the browser, which will fetch resources intelligently to minimize load times, but, again, Perseus knows better than the browser in a lot of cases. To render a new page, all it needs is the page's state and its document metadata, which actually come from a special internal link (behind `/.perseus/page`). Preloading this through the browser is finicky, and it doesn't allow Perseus to do some pre-parsing to keep things speedy, so Perseus provides its own imperative preloading interface. There are two ways of using this interface: there's the easy way, and the fine-grained way. The easy way is to use the `.preload()` method on the `Reactor`, which spawns a future for you and panics on errors that you caused (like a misspelled route), while silently failing on errors from the server. Alternately, you could use the `.try_preload()` method, which lets you handle the errors, and forces you to manage the asynchronicity yourself. If you want more control over the error handling (which applies especially if you're preloading a route that you haven't hardcoded), then you should use this method instead. diff --git a/docs/next/en-US/what-is-perseus.md b/docs/next/en-US/what-is-perseus.md index ec6bb8583f..da78d0e477 100644 --- a/docs/next/en-US/what-is-perseus.md +++ b/docs/next/en-US/what-is-perseus.md @@ -8,15 +8,15 @@ We can obviously agree that Rust is much better than JavaScript: it's way faster Now, you might have come across other web development libraries and frameworks for Rust before, but there's a big difference between those two terms, so let's sort that out first. A *library* is a piece of code that you use to help you build your site. A *framework* is a mammoth of code that uses your code to build your site. Think of it like the difference between `futures::executor::block_on` and `#[tokio::main]`: one is being used by you to handle a bit of `async`, and the other is using your code to handle *all* the `async`. In the same way, a library is a great choice for when you want to build a small site, or when you want to replace just part of a site with Rust. For these kinds of things, we absolutely recommend [Sycamore](https://github.com/sycamore-rs/sycamore), on which Perseus is based. -However, sometimes you'll need to break out the big guns. Sometimes, you'll need to render content in advance so that your users see it straight away, rather than a blank page while your Wasm boots up. Sometimes, you'll want to have a*stateful* app. This doesn't just mean you've got buttons and forms, etc., but that you're building your app in a special kind of pattern, which Perseus is built around. Let's say you have a simple static blog: you might have a `/post` URL, under which all your posts can be found. Fundamentally, all these posts have the same structure, just with different titles, dates, tags, and contents, so you might choose to create some kind of *template* for them, and then maybe build a Markdown parser or the like to push all that into your app to create *pages*. Essentially, **template + state = page**. In Perseus, this is all handled for you, and you just create templates, like `/post`, along with ways to render their state. +However, sometimes you'll need to break out the big guns. Sometimes, you'll need to render content in advance so that your users see it straight away, rather than a blank page while your Wasm boots up. Sometimes, you'll want to have a *stateful* app. This doesn't just mean you've got buttons and forms, etc., but that you're building your app in a special kind of pattern, which Perseus is built around. Let's say you have a simple static blog: you might have a `/post` URL, under which all your posts can be found. Fundamentally, all these posts have the same structure, just with different titles, dates, tags, and contents, so you might choose to create some kind of *template* for them, and then maybe build a Markdown parser or the like to push all that into your app to create *pages*. Essentially, **template + state = page**. In Perseus, this is all handled for you, and you just create templates, like `/post`, along with ways to render their state. -For example, for a blog, you might create a new post template with `Template::build("post")`, and then create a function that takes in some state and plugs it into a Sycamore `view! { .. }` to render some content. You might take in a `struct` containining contents, titles, tags, etc. If you then specify a function that can list the pages that this template should create (e.g. by getting all the Markdown files in a certain directory), and then another one that takes each path and generates state for it, Perseus will string it all together and give a lightning-fast app. +For example, for a blog, you might create a new post template with `Template::build("post")`, and then create a function that takes in some state and plugs it into a Sycamore `view! { .. }` to render some content. You might take in a `struct` containing contents, titles, tags, etc. If you then specify a function that can list the pages that this template should create (e.g. by getting all the Markdown files in a certain directory), and then another one that takes each path and generates state for it, Perseus will string it all together and give a lightning-fast app. Beyond this, Perseus has all sorts of extra features, like inbuilt error handling systems that allow you to gracefully display error messages if state generation fails, or if your app panics, or something else like that. All you do is match an `enum ClientError`, and Perseus shows your errors to the client. Beyond that, if you want to build an app in multiple languages, Perseus will let you do it straight away: just replace the text in your code with identifiers inside the `t!()` macro, and define a map of translation IDs to text for each language you want to support. Variable interpolation is supported out of the box, and you can unleash the full power of [Fluent](https://projectfluent.org) for handling pluralization rules, genders, etc. -Going even further, Perseus' state generation platform is built for even the most advanced use-cases: let's say you have not a blog, but an ecommerce site selling a thousand products. Well, a thousand would actually build very quickly, so perhaps a million. Still probably looking at less than a second, but we'll go with it. Maybe you don't want to build all that at build time. Simple! Just add `.incremental_generation()` to your template definition and then...you're done. If a user goes to a product page that doesn't exist yet, it will passed to your state generation functions, and, if it's a page that exists, those functions can produce the page, and Perseus will serve it. For any future users, that page will be cached and returned immediately. It's like building your whole app over time, on-demand. And, if you have an index of all your products, you could automatically *revalidate* that every, say, 24 hours, to make sure users have a fairly up to date listing. Or you could logic-based revalidation that checks each time whether or not there are actually any new products, before rebuilding. You could even combine the two: only check every few hours whether or not there are new products, and, if there are, rebuild that page. +Going even further, Perseus' state generation platform is built for even the most advanced use-cases: let's say you have not a blog, but an ecommerce site selling a thousand products. Well, a thousand would actually build very quickly, so perhaps a million. Still probably looking at less than a second, but we'll go with it. Maybe you don't want to build all that at build time. Simple! Just add `.incremental_generation()` to your template definition and then...you're done. If a user goes to a product page that doesn't exist yet, it will be passed to your state generation functions, and, if it's a page that exists, those functions can produce the page, and Perseus will serve it. For any future users, that page will be cached and returned immediately. It's like building your whole app over time, on-demand. And, if you have an index of all your products, you could automatically *revalidate* that every, say, 24 hours, to make sure users have a fairly up to date listing. Or you could logic-based revalidation that checks each time whether or not there are actually any new products, before rebuilding. You could even combine the two: only check every few hours whether or not there are new products, and, if there are, rebuild that page. -To be clear, and this is important if you aren't familiar with web development, Perseus is not a library, it's a framework. It's a giant engine into which you plug your code that will connect everything together and optimize it, producing a super-fast site that outperforms every JS framework under the sun. It might well seem like you don't need a lot of these features, and, if you don't, you can just run `perseus export` to get a series of static HTML files that you can serve to users however you like, with a simple Wasm bundle making sure whatever interactivity you have works as smoothly as possible (and it will still be unreasonably fast). If you're used to systems programming, the whole idea of a framework might seem a bit absurd, but it's very often required in web development, simply because the best experiences come from complex features, like rendering your site to HTML in advance, or caching transations, or delayable capsules that can be infinitely nested to create lazy-loaded pages, etc. Some of these are easy to implement, others are not. The point of Perseus is to handle this all for you so you can get on with what you want to write: your app. Even better, Perseus is built on [Sycamore](https://github.com/sycamore-rs/sycamore), which handles reactivity primitives, meaning there is a separation of concerns, unlike with other current Rust frameworks: one team is in charge of the reactivity, and another in charge of the framework, meaning more features are developed more quickly, and bugs are fixed more rapidly, while both systems remain fantastically maintainted. +To be clear, and this is important if you aren't familiar with web development, Perseus is not a library, it's a framework. It's a giant engine into which you plug your code that will connect everything together and optimize it, producing a super-fast site that outperforms every JS framework under the sun. It might well seem like you don't need a lot of these features, and, if you don't, you can just run `perseus export` to get a series of static HTML files that you can serve to users however you like, with a simple Wasm bundle making sure whatever interactivity you have works as smoothly as possible (and it will still be unreasonably fast). If you're used to systems programming, the whole idea of a framework might seem a bit absurd, but it's very often required in web development, simply because the best experiences come from complex features, like rendering your site to HTML in advance, or caching transactions, or delayable capsules that can be infinitely nested to create lazy-loaded pages, etc. Some of these are easy to implement, others are not. The point of Perseus is to handle this all for you so you can get on with what you want to write: your app. Even better, Perseus is built on [Sycamore](https://github.com/sycamore-rs/sycamore), which handles reactivity primitives, meaning there is a separation of concerns, unlike with other current Rust frameworks: one team is in charge of the reactivity, and another in charge of the framework, meaning more features are developed more quickly, and bugs are fixed more rapidly, while both systems remain fantastically maintained. If Perseus doesn't sound like your cup of tea, there are several other Rust frameworks you might like to check out: [Sycamore](https://github.com/sycamore-rs/sycamore) is the library on which Perseus is based, if you want to keep the same sort of style; [Yew](https://yew.rs) is a very popular library; and [Seed](https://seed-rs.org) is another. There's also [Sauron](https://github.com/ivanceras/sauron), [MoonZoon](https://github.com/MoonZoon/MoonZoon), and [Leptos](https://github.com/leptos-rs/leptos), just to name a few. If you'd like to see some more in-depth comparisons between these projects, check out [the comparisons page](comparisons). diff --git a/packages/perseus/src/state/state_store.rs b/packages/perseus/src/state/state_store.rs index 8334efae15..a995c8538f 100644 --- a/packages/perseus/src/state/state_store.rs +++ b/packages/perseus/src/state/state_store.rs @@ -577,7 +577,7 @@ impl PssEntry { /// set it to never having had state (and there will be nothing in the /// frozen state for it), which is fine; but, if they *removed* state /// from an entity that previously had it, this will return an error to the - /// HSR thaw attempt (whcih will try to add the old state back). In that + /// HSR thaw attempt (which will try to add the old state back). In that /// case, the error should be discarded by the caller, who should accept /// the changed data model. pub fn set_state(&mut self, state: Box) -> Result<(), ClientError> {