Fresh 1.6: Tailwind CSS plugin, simplified typings and more
In this cycle we’ve worked on making first class Tailwind CSS support a reality and expanding our plugin API. Apart from that we spent time on various developer experience improvements.
Remember: you can start a new Fresh project by running
deno run -Ar https://fresh.deno.dev
or update an existing project by running
deno run -Ar https://fresh.deno.dev/update
in your project folder.
Here’s an overview of what’s new in 1.6. Read on for all the details, plus what to expect in the next iteration of Fresh.
Fresh 1.6 at a glance
- 🌊 First class Tailwind CSS plugin We’re moving from Twind to Tailwind CSS. It features better Editor integrations and moves the CSS generation out of the rendering path.
- 📨 Partials with forms Partials are now supported on Form elements.
- 🧯 Partials on error pages Partials can now be used to render error pages.
- 🕵️♀️ Closable error overlay The custom error overlay that is shown during development can now be closed, so that you can see the projects 500 page.
- ⚡️ Improved islands bundling strategy Islands are now bundled together in a smarter way that leads to less files and allows you to group related islands together.
- ⚔️ Less manifest merge conflicts The identifiers in the manifest file are now derived from the file name instead of being an incremented number. This reduces the chance for merge conflicts greatly.
-
📦
Support for pre-generated assets
Fresh will give generated assets priority when serving them when it encouters the same file in
_fresh/static
. -
🧩
Plugin API enhancements
The Plugin API now supports adding islands from plugins, adding
<link>
-elements and has a newconfigResolved
hook. - 🏁 Faster route matching The route matching performance has been improved, especially when a project has many static files.
- 🛣️ Serving from a sub path Fresh projects can now be served under a sub path like `https://example.com/foo/bar` instead of the root of the domain.
- 🏆 Simplified typings The number of different context types has been reduced from 6 down to 2 and there is now only one props type instead of four.
First class Tailwind CSS plugin
It has been a long time coming and Fresh now ships with a proper Tailwind CSS plugin. It’s using the exact same npm package as you would do in Node. This means we’re putting our Twind plugin on life support and mark it as deprecated.
Tailwind CSS has many benefits over Twind, with the most important one being that Tailwind CSS is actively maintained and supports a much more polished editor experience. You’ll get some nice performance benefits too by switching over to Tailwind CSS as it only needs to generate CSS once on deployment, vs on the fly on every request with Twind.
To switch from Twind to Tailwind CSS, follow this guide in our documentation. Note that this plugin requires ahead of time builds to be set up for your project.
Partials with forms
We’ve expanded the capabilities for Partials to work with <form>
-elements.
Similar to the existing Partials support for <a>
-elements, Fresh will opt into
partials with forms when the parent element of the submitter has a “truthy”
f-client-nav
attribute. This even works when the submit button is outside of
the enclosing form element.
<form id="foo">
<Partial name="slot-1">
<input type="text" value={value} name="name" />
</Partial>
</form>
<button
type="submit"
form="foo"
formaction="/form"
f-partial="/form"
formmethod="POST"
>
submit
</button>
Partials on error pages
With Fresh 1.6 you can now use Partials for error pages too. Previously, a partial navigation would error when the response status code was not ok. This limitation has been removed and we only check if the response content type is HTML.
While we were working on that part of the code, we also added support for following redirects automatically for partial navigations.
Closable error overlay
With Fresh 1.5 we added an error overlay during development that shows detailed
information about where the error occurred. Problem was that this overrode the
user’s _500.tsx
error page. We fixed that and now the error overlay is a
proper error overlay that can be closed.
Improved islands bundling strategy
In the browser we noticed that we could improve the way we serve the JavaScript
code for islands. Previously, every island component would be moved into a
separate bundle, which would lead to many .js
files being generated that are
less than <1kb. The new strategy adheres to the original files to give you much
more control on how best to bundle islands for your project. By respecting the
original files you can group related islands together and include them in the
same bundle.
Less manifest merge conflicts
Whilst we want to get rid of the manifest in the long term, we felt that we can
improve the current pain points of frequently running into merge conflicts when
adding new routes or renaming them. Previously, identifiers would be composed of
$<number>
where the number part was merely incrementing. The new approach
converts the file name to a valid JavaScript identifier and only appends a
number if an existing identifier with the same name is found.
Thanks to Reed von Redwitz for bringing this over the finish line!
Support for pre-generated assets
There are many scenarios where you’d want to generate asset files for
deployment. These could include file optimisations or generating CSS like for
the Tailwind CSS plugin. The way it works is that Fresh gives static files
residing in <project>/_fresh/static
higher priority over the default
<project>/static
directory.
Plugin API enhancements
The plugin API was an area we always wanted to improve but didn’t have the time for. With this release we set aside dedicated time to expand it with some new features users have been asking for.
Islands from plugins
Among the most exciting of them is the ability to add islands from plugins. By
specifying file paths to your island file, Fresh will treat them the same way as
if they had been placed inside the islands/
directory.
import { Plugin } from "$fresh/server.ts";
import * as path from "https://deno.land/std@0.208.0/path/mod.ts";
const __dirname = path.dirname(path.fromFileUrl(import.meta.url));
export default function myIslandPlugin(): Plugin {
return {
name: "my-island-plugin",
islands: {
baseLocation: import.meta.url,
paths: [
"./plugin-islands/MyIsland.tsx",
"./plugin-islands/OtherPluginIsland.tsx",
],
},
};
}
Thanks to Reed von Redwitz for working out all the details.
Adding link-elements from plugins
Plugins can now add <link>
-elements to add additional stylesheets or similar
things.
import { Plugin } from "$fresh/server.ts";
function MyPlugin(): Plugin {
return {
name: "link-inject",
render(ctx) {
ctx.render();
return {
links: [{ rel: "stylesheet", href: "styles.css" }],
};
},
};
}
Thanks to Adam Gregory for enabling this feature!
New configResolved hook
There are often scenarios where your plugin needs to apply different logic based
on the Fresh configuration. We already pass the config to onBuildStart(config)
but that is too late for other sections of the plugin API. Therefore we’ve added
a new configResolved
hook similar to vite
which allows you to grab the fully
resolved Fresh configuration.
import { Plugin, ResolvedFreshConfig } from "$fresh/server.ts";
function MyPlugin(): Plugin {
let config: ResolvedFreshConfig;
return {
name: "my-cool-plugin",
configResolved(resolvedConfig) {
config = resolvedConfig;
},
};
}
Faster route matching
Another area we looked at to make Fresh faster, was our router. We’ve noticed some inefficiencies in the way we process and match routes, especially when a project has many static files. We added another optimisation that detects if the route has no dynamic parts and will fall back to a simple string comparisons.
Overall, with all optimisations applied we see a 4-10x speedup in route matching times on deno.com .
Serving from a sub path
Not every project is served at the root domain address and a popular request
among users was to be able to serve Fresh from a sub path. Instead of hosted at
https://example.com/
it should be available under
https://example.com/foo/bar
for example. This is now possible with the new
router.basePath
config option.
// fresh.config.ts
import { defineConfig } from "$fresh/server.ts";
export default defineConfig({
router: {
basePath: "/foo/bar",
},
});
Thanks again to Reed von Redwitz for bringing this over the finish line!
Simplified typings
One thing that I always felt we could do better has to do with how we type our API that users interface this. Take a look at the various middleware and route context types for example:
MiddlewareHandlerContext
AppContext
ErrorHandlerContext
HandlerContext
LayoutContext
UnknownHandlerContext
That’s quite a lot of types to be aware of. With recent code cleanups and simplifications of our internals we realised that they are mostly the same type. So we reduced the number of context types down to just two.
FreshContext
RouteContext
(for async routes/layouts/app wrapper)
In the future we might be even able to reduce it further to just one. Note, that we’re still keeping the old types around as aliases to the new ones to not make this a breaking change.
In the midst of simplifying our core we noticed that our props typings were similarly complex, more than they should’ve been.
ErrorPageProps
AppProps
UnknownPageProps
LayoutProps
Those four types have been reduced to a single type:
PageProps
We’re pretty happy with the simplifications so far as it eases the mental burden of working with Fresh.
Together with the type simplifications we made sure that the Fresh context
object is exposed everywhere. It’s now passed to middlewares and routes as well.
On top of we now pass the fully resolved Fresh config
inside context, so that
you can apply different logic based on it.
What’s on the horizon?
As with every release there is more that I would’ve loved to be able to include in this release. The truth is that these features need a little more time to polish. Curious Deno readers might have noticed the new “precompile” JSX transform in recent Deno release notes. It’s something I spent quite a bit of time on during this cycle and something I want to bring to Fresh as soon as possible. It’s mostly a performance change in how JSX is transpiled that will speed up most Fresh sites by 2-4x.
Another thing that I’ve worked on with Bartek is HMR for Deno. The initial prototype landed in Deno 1.38 as well, but there is more work to be done to wire it up in Fresh and make it feel smooth.
And of course, there is more work to be done surrounding Partials! I really want to have a proper way to specify pending states, do error handling and expose the functionality so that you can call it from an event listener. I don’t know exactly how all of this will look like, but it’s something I really want to have a good answer for in Fresh.
The December cycle will be a bit shorter than usual due to vacations around Christmas. It’s likely that we’ll skip a feature release and only do another patch release at the end of the year. The current iteration plan can be seen in the December iteration plan