Skip to content

Commit

Permalink
feat: ✨ add plugins system (#62)
Browse files Browse the repository at this point in the history
* feat(plugins): ✨ added fundamentals of plugins system

This also adds plugin examples to the `basic` example, which will be removed before merging.

* feat(plugins): ✨ added nearly all functional actions and improved plugins framework

* feat(plugins): ✨ added control plugin opportunities

* fix(plugins): 🔥 removed plugin actions for setting html shell and static dir paths

They were hell for the deployment process, and didn't add any value.

* chore: ♻️ cleaned up old todos and renamed a new checkpoint

* docs(plugins): 📝 added introductory plugin docs

* feat(plugins): ✨ added settings actions for plugins

We can't modify `MutableStore` or `TranslationsManager` yet though.

BREAKING CHANGE: `build_app`/`export_app`now take a `&TemplateMap` (`get_templates_vec` abolished)

* feat(plugins): ✨ added `tinker` action and command

* feat(examples): ✨ added `plugins` example and removed plugins code from other examples

This includes tests.

* fix(plugins): 🐛 fixed plugin data system

Note that `PluginData` is now replaced by `Any`.

* docs(book): ✏️ fixed missing link to lighthouse in book intro

* refactor(plugins): ♻️ removed plugin type system

Any plugin can now take functional or control actions. Docs still need updating.

* refactor(plugins): 🔥 removed old `get_immutable_store` actions

These are replaced by the `set_immutable_store` settings action

* fix(exporting): 🐛 fixed engine crate name change bug in exporting

* docs(book): 📝 added docs for plugins
  • Loading branch information
arctic-hen7 authored Oct 16, 2021
1 parent 31f6ed1 commit ca0aaa2
Show file tree
Hide file tree
Showing 54 changed files with 1,253 additions and 135 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"testing",
"templates",
"exporting",
"website"
"website",
"plugins"
]
}
16 changes: 8 additions & 8 deletions docs/0.1.x/en-US/arch.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Architecture

Perseus is a complex system, and this page will aim to explain the basics in a beginner-friendly way. If you've already used similar frameworks from the JS world like NextJS, then some of this may be familiar to you. If you're having trouble following along, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) and ask us to clarify some sections, because this page in particular should be accessible to everyone. If you'd like more specific help, [ask on Gitter](TODO)!
Perseus is a complex system, and this page will aim to explain the basics in a beginner-friendly way. If you've already used similar frameworks from the JS world like NextJS, then some of this may be familiar to you. If you're having trouble following along, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose) and ask us to clarify some sections, because this page in particular should be accessible to everyone. If you'd like more specific help, [ask on Gitter]()!

## Templates and Pages

The core of Perseus is the idea of templates. When you create a Perseus app, what you're basically doing is telling Perseus how to compile your code into a series of *pages*. **Each page has a unique URL on your final website.** If you have a blog, and every post is stored as something like `post/title`, then each post would be a unique page.
The core of Perseus is the idea of templates. When you create a Perseus app, what you're basically doing is telling Perseus how to compile your code into a series of _pages_. **Each page has a unique URL on your final website.** If you have a blog, and every post is stored as something like `post/title`, then each post would be a unique page.

But this doesn't mean you have to write the code for every page individually! Perseus does this for you, and only asks you to write *templates*. A template can generate one page or many, and a great example of one would be a `post` template. Each template has a *root path*, which is essentially like the space on your website that that template controls. For example, a post template might control `/post`, meaning it can render pages at `/post`, `/post/test`, `/post/longer/path`, etc. In theory, a template could render pages outside its domain, but this would be a bad idea for structure, and makes your code difficult to understand.
But this doesn't mean you have to write the code for every page individually! Perseus does this for you, and only asks you to write _templates_. A template can generate one page or many, and a great example of one would be a `post` template. Each template has a _root path_, which is essentially like the space on your website that that template controls. For example, a post template might control `/post`, meaning it can render pages at `/post`, `/post/test`, `/post/longer/path`, etc. In theory, a template could render pages outside its domain, but this would be a bad idea for structure, and makes your code difficult to understand.

### State

What differentiates pages from templates is *state*, which tells a page how to fill out its template to give unique content. For example, our post template would probably have a `content` field in its state, and its pages would use that to render their unique content!
What differentiates pages from templates is _state_, which tells a page how to fill out its template to give unique content. For example, our post template would probably have a `content` field in its state, and its pages would use that to render their unique content!

In terms of writing code, a page's state is just a `struct` that can be serialized and deserialized with [Serde](https://serde.rs).

## Rendering Strategies

Each template has a rendering strategy, which it uses to create its pages. There are a number of rendering strategies in Perseus, each of which is documented in detail in its own section. What's important to understand for now is that there are two main ways a template can render pages, at *build time*, or at *request time*. If a template renders at build time, it generates the code for your pages when you build your app, which means you end up serving static pages. This is *really fast*. However, sometimes you need information specific to each request to render a page (e.g. an authentication token), and you can't render at build. Instead, you'd render at request time, which gives you access to information about the HTTP request a user sent for your page.
Each template has a rendering strategy, which it uses to create its pages. There are a number of rendering strategies in Perseus, each of which is documented in detail in its own section. What's important to understand for now is that there are two main ways a template can render pages, at _build time_, or at _request time_. If a template renders at build time, it generates the code for your pages when you build your app, which means you end up serving static pages. This is _really fast_. However, sometimes you need information specific to each request to render a page (e.g. an authentication token), and you can't render at build. Instead, you'd render at request time, which gives you access to information about the HTTP request a user sent for your page.

Here's a list of Perseus' currently supported rendering strategies. These can all be combined, but some combinations make more sense than others.

Expand All @@ -28,12 +28,12 @@ Here's a list of Perseus' currently supported rendering strategies. These can al
| Revalidation | Rebuilds pages conditionally | Hybrid |
| Incremental generation | Builds pages on-demand | Hybrid |

There are two *hybrid* strategies listed above. They're a little more complicated, and out of the scope of this page, but they operate at both build *and* request-time, allowing you to reap the benefits of both worlds!
There are two _hybrid_ strategies listed above. They're a little more complicated, and out of the scope of this page, but they operate at both build _and_ request-time, allowing you to reap the benefits of both worlds!

## Routing

*This section describes how Perseus works under the hood. Skip it if you want.*
_This section describes how Perseus works under the hood. Skip it if you want._

Perseus doesn't just host your pages at their URLs though. In fact, Perseus has a generic handler for *any URL*, which returns what we call the *app shell*. That's a concept from the single-page app (e.g. ReactJS), where your app always has a constant shell around it, and each page is loaded into that shell, making page transitions more seamless. Perseus adopts this as well, but with the added benefits of super-fast static rendering strategies and a more lightweight shell.
Perseus doesn't just host your pages at their URLs though. In fact, Perseus has a generic handler for _any URL_, which returns what we call the _app shell_. That's a concept from the single-page app (e.g. ReactJS), where your app always has a constant shell around it, and each page is loaded into that shell, making page transitions more seamless. Perseus adopts this as well, but with the added benefits of super-fast static rendering strategies and a more lightweight shell.

The shell includes a router (courtesy of [Sycamore](https://github.com/sycamore-rs/sycamore)), which determines what page the user wants, and then sends a request to a special endpoint behind `/.perseus`. That then renders the page and returns some static HTML and the page's state.
7 changes: 7 additions & 0 deletions docs/next/en-US/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
- [Communicating with a Server](/docs/server-communication)
- [Stores](/docs/stores)
- [Static Exporting](/docs/exporting)
- [Plugins](/docs/plugins/intro)
- [Functional Actions](/docs/plugins/functional)
- [Control Actions](/docs/plugins/control)
- [Using Plugins](/docs/plugins/using)
- [The `tinker` Action](/docs/plugins/tinker)
- [Writing Plugins](/docs/plugins/writing)
- [Security Considerations](/docs/plugins/security)
- [Deploying](/docs/deploying/intro)
- [Server Deployment](/docs/deploying/serverful)
- [Serverless Deployment](/docs/deploying/serverless)
Expand Down
2 changes: 2 additions & 0 deletions docs/next/en-US/ejecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ The Perseus CLI is fantastic at enabling rapid and efficient development, but so

However, there are some things that are too advanced for the CLI to support, and, in those cases, you'll need to eject. Don't worry, you'll still be able to use the CLI itself for running your app, but you'll be given access to the engine that underlies it, and you'll be able to tweak basically anything you want.

Before you proceed though, you should know that Perseus supports modularizing the functionality of ejected code through [plugins](:plugins/intro), which let you modify the `.perseus/` directory in all sorts of ways (including arbitrary file modification), without needing to eject in the first place. In nearly all cases (even for smaller apps), plugins are a better way to go than ejecting. In future, you'll even be able to replace the entire `.perseus/` directory with a custom engine (planned for v0.4.0)!

*Note: ejecting from Perseus exposes the bones of the system, and you should be quite familiar with Rust before doing this. That said, if you're just doing it for fun, go right ahead!*

## Ejecting
Expand Down
22 changes: 22 additions & 0 deletions docs/next/en-US/plugins/control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Control Actions

Control actions in Perseus can only be taken by one plugin, unlike [functional actions](:plugins/functional), because, if multiple plugins took them, Perseus wouldn't know what to do. For example, if more than one plugin tried to replace the [immutable store](:stores), Perseus wouldn't know which alternative to use.

Control actions can be considered more powerful than functional actions because they allow a plugin to not only extend, but to replace engine functionality.

## List of Control Actions

Here's a list of all the control actions currently supported by Perseus, which will likely grow over time. You can see these in [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/plugins/control.rs) in the Perseus repository.

If you'd like to request that a new action, functional or control, be added, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose).

_Note: there are currently very few control actions, and this list will be expanded over time._

- `settings_actions` -- actions that can alter the settings provided by the user with [`define_app!`](:define-app)
- `set_immutable_store` -- sets an alternative [immutable store](:stores) (e.g. to store data somewhere other than the filesystem for some reason)
- `set_locales` -- sets the app's locales (e.g. to fetch locales from a database in a more convenient way)
- `set_app_root` -- sets the HTML `id` of the `div` in which to render Perseus (e.g. to fetch the app root from some other service)
- `build_actions` -- actions that'll be run when the user runs `perseus build` or `perseus serve` as part of the build process (these will not be run in [static exporting](:exporting))
- `export_actions` -- actions that'll be run when the user runs `perseus export`
- `server_actions` -- actions that'll be run as part of the Perseus server when the user runs `perseus serve` (or when a [serverful production deployment](:deploying/serverful) runs)
- `client_actions` -- actions that'll run in the browser when the user's app is accessed
32 changes: 32 additions & 0 deletions docs/next/en-US/plugins/functional.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Functional Actions

The first type of action that a Perseus plugin can take is a functional action, and a single functional action can be taken by many plugins. These are the more common type of Perseus action, and are extremely versatile in extending the capabilities of the Perseus engine. However, they don't have the ability to replace critical functionality on their own.

## List of Functional Actions

Here's a list of all the functional actions currently supported by Perseus, which will likely grow over time. You can see these in [this file](https://github.com/arctic-hen7/perseus/blob/main/packages/perseus/src/plugins/functional.rs) in the Perseus repository.

If you'd like to request that a new action, functional or control, be added, please [open an issue](https://github.com/arctic-hen7/perseus/issues/new/choose).

- `tinker` -- see [this section](:plugins/tinker)
- `settings_actions` -- actions that can alter the settings provided by the user with [`define_app!`](:define-app)
- `add_static_aliases` -- adds extra static aliases to the user's app (e.g. a [TailwindCSS](https://tailwindcss.com) stylesheet)
- `add_templates` -- adds extra templates to the user's app (e.g. a prebuilt documentation system)
- `add_error_pages` -- adds extra [error pages](:error-pages) to the user's app (e.g. a prebuilt 404 page)
- `build_actions` -- actions that'll be run when the user runs `perseus build` or `perseus serve` as part of the build process (these will not be run in [static exporting](:exporting))
- `before_build` -- runs arbitrary code just before the build process starts (e.g. to run a CSS preprocessor)
- `after_successful_build` -- runs arbitrary code after the build process has completed, if it was successful (e.g. copying custom files into `.perseus/dist/`)
- `after_failed_build` -- runs arbitrary code after the build process has completed, if it failed (e.g. to report the failed build to a server crash management system)
- `export_actions` -- actions that'll be run when the user runs `perseus export`
- `before_export` -- runs arbitrary code just before the export process starts (e.g. to run a CSS preprocessor)
- `after_successful_build` -- runs arbitrary code after the build process has completed (inside the export process), if it was successful (e.g. copying custom files into `.perseus/dist/`)
- `after_failed_build` -- runs arbitrary code after the build process has completed (inside the export process), if it failed (e.g. to report the failed export to a server crash management system)
- `after_failed_export` -- runs arbitrary code after the export process has completed, if it failed (e.g. to report the failed export to a server crash management system)
- `after_failed_static_copy` -- runs arbitrary code if the export process fails to copy the `static` directory (e.g. to report the failed export to a server crash management system)
- `after_failed_static_alias_dir_copy` -- runs arbitrary code if the export process fails to copy a static alias that was a directory (e.g. to report the failed export to a server crash management system)
- `after_failed_static_alias_file_copy` -- runs arbitrary code if the export process fails to copy a static alias that was a file (e.g. to report the failed export to a server crash management system)
- `after_successful_export` -- runs arbitrary code after the export process has completed, if it was successful (e.g. copying custom files into `.perseus/dist/`)
- `server_actions` -- actions that'll be run as part of the Perseus server when the user runs `perseus serve` (or when a [serverful production deployment](:deploying/serverful) runs)
- `before_serve` -- runs arbitrary code before the server starts (e.g. to spawn an API server)
- `client_actions` -- actions that'll run in the browser when the user's app is accessed
- `start` -- runs arbitrary code when the Wasm delivered to the browser is executed (e.g. to ping an analytics service)
9 changes: 9 additions & 0 deletions docs/next/en-US/plugins/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Plugins

Perseus is extremely versatile, but there are some cases where is needs to be modified a little under the hood to do something very advanced. For example, as you'll learn [here](:deploying/size), the common need for applying size optimizations requires modifying a file in the `.perseus/` directory, which requires [ejecting](:ejecting). This is a laborious process, and makes updating difficult, so Perseus support a system of _plugins_ to automatically apply common modifications under the hood!

First, a little bit of background. The `.perseus/` directory contains what's called the Perseus engine, which is basically the core of your app. The code you write is actually imported by this and used to invoke various methods from the `perseus` crate. If you had to build all this yourself, it would take a very long time! Because this directory can be automatically generated though, there's no need to check it into version control (like Git). However, this becomes problematic if you then want to change even a single file inside, because you'll then need to commit the whole directory, which can be unwieldy. More importantly, when updates come along that involve changes to that directory, you'll either have to delete it and re-apply your modifications to the updated directory, or apply the updates manually, either of which is overly tedious for simple cases.

Perseus has plugins to help with this. At various points in the engine, plugins have what are called _actions_ that they can take. Those actions are then executed by the engine at the appropriate time. For example, if a plugin needed to run some code before a Perseus app initialized, it could do that by taking a particular action, and then the engine would execute that action just before the app initialized.

There are two types of actions a plugin can take: _functional actions_, and _control actions_. A single functional action can be taken by many plugins, and they (usually) won't interfere with each other. For example, many plugins can add additional [static aliases](:static-content) to an app. A single control action can only be taken by one plugin, because otherwise Perseus would have conflicting data. For example, if multiple plugins all set their own custom [immutable stores](:stores), Perseus wouldn't know which one to use. Both types of actions are explained in detail in the following sections.
14 changes: 14 additions & 0 deletions docs/next/en-US/plugins/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Security Considerations of Plugins

Perseus' plugins system makes it phenomenally versatile, and allows you to reshape default behavior in ways that are possible in very few other frameworks (especially frameworks built in compiled languages like Rust). However, this comes with a major security risk to your system, because plugins have the power to execute arbitrary code.

## The Risks

If you enable a plugin in your app, it will have the opportunity to run arbitrary code. The actions that plugins take are just functions that they provide, so a plugin could easily be saying that it's adding an extra [static alias](:static-content) while simultaneously installing malware on your computer.

## Precautions

1. **Only ever use plugins that you trust!** Anyone can create a Perseus plugin, and some people may create plugins designed to install malware on your system. Optimally, you should review the code of every plugin that you install.
2. **Never run Perseus as root!** If you run Perseus and any plugins as the root user, a plugin can do literally anything on your computer, which could include installing privileged malware (by which point your computer would be owned by an attacker).

**TL;DR:** don't use shady code, and don't run things with unnecessary privileges in general.
Loading

0 comments on commit ca0aaa2

Please sign in to comment.