Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): add unocss plugin (WIP) #1303

Closed
wants to merge 40 commits into from
Closed

feat(plugins): add unocss plugin (WIP) #1303

wants to merge 40 commits into from

Conversation

miguelrk
Copy link

@miguelrk miguelrk commented Jun 15, 2023

TLDR: unocss is an instant on-demand atomic CSS engine and a joy to use. I believe it can (and should) replace twind as the default atomic CSS engine for fresh.

Context

[email protected] introduced the renderAsync method, which allows calling the asynchronous uno.generate() method as first proposed by @Hladikes (thanks!) but without the workaround. I've tested it for routes, components and islands, and it's working for all (tests are pending).

Checklist

This PR is a WIP... I will continue tomorrow with the following:

  • enhance defaultConfig
  • add tests
  • add docs

Related Issues

#728
#290

miguelrk added a commit to netzo/fresh-netzo that referenced this pull request Jun 16, 2023
@miguelrk
Copy link
Author

miguelrk commented Jul 20, 2023

Hey @marvinhagemeister! From your comment in the August 2023 iteration plan issue (didn't want to pollute that conversation), I'm curious to ask what's your stance on unocss. I find unocss to be a better alternative to twind, as it is not only NOT tied to tailwindcss, but rather CSS framework agnostic. It is also much more flexible for building your own custom CSS framework. The plugin here uses const { css } = await uno.generate(htmlText) at the render step, so that aligns with fresh, but it even has the ability of using it at runtime by loading the unocss engine from a CDN. IMO, the most important advantage of unocss over tailwind-based alternatives is NOT being tied PostCSS, unocss is "pure javascript".

That being said, I haven't had the time to continue with the to-do items in this PR, but I've been using this plugin extensively and can confirm "it just works".

Eager to hear your opinon, I wonder why most people still try to work with twind in fresh despite the known issues, when unocss could very much be a plug-in replacement (by adding the @unocss/preset-uno which includes TailwindCSS support). I could then take this PR forward 👍🏼

@lino-levan
Copy link
Contributor

Super excited to have UnoCSS in Fresh. I have some decently sized apps to test this on for regressions (which I will do later on in the review process for this PR). This is much more "fresh"-y in nature and kind of avoids a whole bunch of twind-related issues. I haven't had the chance to look at code yet but huge +1

@marvinhagemeister
Copy link
Collaborator

I'm all for adding support for unocss. I don't have a horse in the race and don't mind which way to create CSS folks use, as long as it works for them and maintenance is low for me 👍

plugins/unocss.ts Outdated Show resolved Hide resolved
@lino-levan
Copy link
Contributor

Any chance you could push this forward @miguelrk? I'd love to pick this up if not.

@miguelrk
Copy link
Author

miguelrk commented Aug 8, 2023

Any chance you could push this forward @miguelrk? I'd love to pick this up if not.

Thanks for the interest and reminder @lino-levan ! I added some basic docs based on the existing using-twind-v1.md. Not sure how to proceed with testing though, happy if you could take that over.

In any case, do you think we should be defaulting to using the presetUno if no config is provided? This allows simply using the plugin like await start(manifest, { plugins: [unocssPlugin()] }), without having to provide your own config or having a uno.config.ts altogether.

I'm leaning towards removing the defaults, since the defaults are pretty simple an can be manually configured inline in a single line:

import unocssPlugin from "$fresh/plugins/unocss.ts";
import presetUno from "@unocss/preset-uno";

await start(manifest, { plugins: [unocssPlugin({ presets: [presetUno()] })] });```

and also very easily imported from another file:

import unocssPlugin from "$fresh/plugins/unocss.ts";
import unocssConfig from "./uno.config.ts";

await start(manifest, { plugins: [unocssPlugin(unocssConfig)] });```

What do you think @lino-levan ?

@lino-levan
Copy link
Contributor

Hey @miguelrk! I can't seem to get the VSCode extension to work, so my conclusion is that it likely doesn't matter. I think we should have a default config to make migration easy, but we should encourage people to use their own config by making a uno.config.ts. This could mean that they get intellisense on classes in the future which would be nice.

@miguelrk
Copy link
Author

miguelrk commented Aug 8, 2023

Hmm now that you mention it @lino-levan, the antfu.unocss vscode extension is also not working for me. In any case, if unocss is to replace twind as the default for the fresh init script, then I would agree we should scaffold an initial uno.config.ts. While that's not the case (while unocss is not the default for fresh init), I think its best to indeed remove the "sensible" default to use presetUno, since setup is literally one-line more to set it up yourself, encouraging users to do so, instead of "easing migration" for them.

@lino-levan
Copy link
Contributor

Hmm now that you mention it @lino-levan, the antfu.unocss vscode extension is also not working for me.

Yeah, we should look into that. I'm sure they'd be happy to accept a PR.

I think its best to indeed remove the "sensible" default to use presetUno, since setup is literally one-line more to set it up yourself, encouraging users to do so, instead of "easing migration" for them.

Sure. Let's do that instead. Having no default doesn't seem unreasonable.

@miguelrk
Copy link
Author

miguelrk commented Aug 8, 2023

Sure. Let's do that instead. Having no default doesn't seem unreasonable.

Great! I've done that in b397d6f and updated the docs. Something else that comes to mind?

plugins/unocss.ts Outdated Show resolved Hide resolved
plugins/unocss.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@lino-levan lino-levan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside of tests, this is looking pretty close to ready to me.

@lino-levan
Copy link
Contributor

Is there any reason you can't write a fixture test like we have for twindv1? It should be almost copy-paste.

@adamgreg
Copy link
Contributor

Raised #1701

@zhmushan
Copy link

unocss/unocss#3040 will make Fresh and UnoCSS happy together on VSCode.

@miguelrk
Copy link
Author

miguelrk commented Aug 28, 2023

unocss/unocss#3040 will make Fresh and UnoCSS happy together on VSCode.

Huge thanks for this @zhmushan ! From our conversation earlier:

I have completed a quick adaptation of the UnoCSS VSCode plugin with Fresh by directly serializing the Fresh configuration file. For the initial version of the UnoCSS plugin for Fresh, let's temporarily abandon support for uno.config.ts because it would require significant changes to the code of the UnoCSS VSCode plugin.

I still don't get why adding support for uno.config.ts would require significant changes. What specifically is preventing the ContextLoader class supporting uno.config like it already supports fresh.config?

@zhmushan
Copy link

Because we cannot be certain that uno.config is specifically designed for Fresh, separating its original logic from Deno's special handling requires some additional work (although it may not necessarily be complex, there might be boundary conditions to consider). On the other hand, for fresh.config, we can confidently determine that it can be handled use Deno, and the UnoCSS plugin has a designated position for handling framework configuration files. Therefore, only minimal effort is needed to achieve quick adaptation.

@miguelrk
Copy link
Author

@zhmushan I see... I'm still uncomfortable with removing support for uno.config just to get the vscode extension to work, but we can maybe find an alternative/solution...

Alternative 1

If I understand correctly, what prevents us from using uno.config is that the unocss ContextLoader requires a separate (config) file e.g. fresh.config in order to detect it should use the custom transform right? Wouldn't it be safe to assume that ALL deno fresh projects using the plugin will have a fresh.config file (as now required for ALL plugins)? Because if that's the case, the unocss ContextLoader would pick up the fresh.config file, but we could read the config from uno.config instead of from fresh.config. I think its safe to assume that deno fresh projects will have deno-compatible uno.config files, so serialization should still work (not sure what you meant with possible edge cases).

If the above is true, we could import from uno.config by default instead of fresh.config:

// unocss/packages/vscode/deno/fresh.ts
- const { default: freshConfig } = await import(Deno.args[0])
- const unoConfig = freshConfig.plugins.filter(it => it.name === 'unocss')
+ const { default: unoConfig } = await import(Deno.args[0])
console.log(JSON.stringify(unoConfig))

Alternative 2

A simpler workaround (for now) could be declaring the unocss config in uno.config and simply re-exporting from deno.fresh for unocss to import from.

// uno.config.ts
export default defineConfig({...})

// fresh.config.ts
export { default as unoConfig } from "./uno.config.ts"
export default defineConfig({...})

// unocss/packages/vscode/deno/fresh.ts
const { unoConfig } = await import(Deno.args[0])
const unoConfig = freshConfig.plugins.filter(it => it.name === 'unocss')
console.log(JSON.stringify(unoConfig))

@zhmushan
Copy link

@miguelrk

My PR does not mean permanently giving up on adapting uno.config in the Deno project. It is aimed at quickly enabling UnoCSS and Fresh to work together in the most efficient way possible, and to validate feasibility.

Ideally, uno.config should work in Deno projects. But it is not as straightforward as imagined. It involves the overall architecture of UnoCSS. The crucial code resides here.

The prerequisites for adapting Deno are as follows:

  1. @unocss/config and @unocss/shared-integration should not concern themselves with the runtime.
  2. Recognition of the Deno environment should only be done in @unocss/vscode.
  3. @unocss/vscode should still create the Context using @unocss/shared-integration.

@adamgreg
Copy link
Contributor

adamgreg commented Sep 2, 2023

For sending the config to the runtime, could we maybe avoid esbuild and the need for a selfUrl property by using a custom replacer argument to JSON.stringify() to handle functions? Something like this:

const configStr = JSON.stringify(config, (key, value) => (typeof value === "function")? value.toString() : value);

I believe that would allow standard Uno config files without the extra selfUrl property, or allow the config to be defined directly in a fresh.config.ts file or similar.

@adamgreg
Copy link
Contributor

For sending the config to the runtime, could we maybe avoid esbuild and the need for a selfUrl property by using a custom replacer argument to JSON.stringify() to handle functions? Something like this:

const configStr = JSON.stringify(config, (key, value) => (typeof value === "function")? value.toString() : value);

I believe that would allow standard Uno config files without the extra selfUrl property, or allow the config to be defined directly in a fresh.config.ts file or similar.

I've gone off this idea because of the possibility of functions including closures or referencing module-level variables, which would not be serialised.

Instead, I think a better alternative to selfURL would be to expect the config to be found in a uno.config.ts file at the root of the project. This reflects the recommended usage of UnoCS, and I can't think of a compelling reason for a project not to stick to it. Also, the VS Code plugin makes a similar assumption. I'm working on implementing this now. The config could still be passed into the plugin as an object, but without browser runtime support.

adamgreg and others added 7 commits September 13, 2023 15:14
- Import config from uno.config.ts if no config object is explicitly provided
- Always use uno.config.ts as the import source for the browser runtime config
- This avoids the complexity of selfURL, allowing the plugin to use standard Uno config files
It is the same as found in the unocss package, but that can not currently be safely imported in Deno (due to Node-specific code in the icons preset).
UnoCSS plugin use uno.config.ts
Make UnoCSS plugin function synchronous
@miguelrk
Copy link
Author

Hey @marvinhagemeister ! I see you've been working on build-hooks in relation maybe to #1700 and #1701 (for which there's also @adamgreg's PR #1769). This might just be what we were missing to better integrate UnoCSS to the plugin. However, there will still be some scenarios where shipping the unocss runtime to the client is inevitable, for which it might be best that this plugin provides the following 3 independent options (proposed by @adamgreg):

  • "generate CSS on SSR"
  • "transform HTML on SSR"
  • "send browser runtime"

Other than that, I've merged @adamgreg's latest changes enforcing the use of the standard uno.config file (avoiding having to pass selfURL to the plugin options). I think the related unocss transformers mentioned above could be tracked in another issue, in order to move this PR forward, or how do you suggest proceeding?

@adamgreg
Copy link
Contributor

adamgreg commented Oct 4, 2023

I've found a problem with running UnoGenerator over the rendered HTML (other than performance) - any & in the source have been replaced by &. This means that UnoCSS can not pick up classes like that contain arbitrary variants such as [&>div]:bg-red.

Any generation of CSS during SSR should be done via a Preact hook, like the twind plugin does.

@adamgreg
Copy link
Contributor

adamgreg commented Oct 4, 2023

I've found a problem with running UnoGenerator over the rendered HTML (other than performance) - any & in the source have been replaced by &. This means that UnoCSS can not pick up classes like that contain arbitrary variants such as [&>div]:bg-red.

Any generation of CSS during SSR should be done via a Preact hook, like the twind plugin does.

I've made a PR to improve SSR into this branch: https://github.com/miguelrk/fresh/pull/4

@adamgreg
Copy link
Contributor

Based on the latest discussion of this with @marvinhagemeister on the Discord channel, it sounds like we should generally be targeting AOT usage, not JIT. I think we just need a single JIT/AOT option:

  • AOT mode (enabled by default): Run UnoCSS over the codebase in an after-build hook. In the render hook the plugin can insert a link to the generated CSS file (is this possible currently?). For a decent developer experience, the plugin would also have to detect during initialisation when the server is running in "dev" mode, and run UnoCSS over the codebase immediately - hopefully that is not too slow.
  • JIT mode (disabled by default): Extract classes during SSR and send runtime to the client (basically the default mode that currently exists). Use cases:
    1. During development to allow experimentation with the classes in the browser DevTools
    2. Possibly to save time when the server restarts, if UnoCSS extraction from source code takes more than a couple of seconds.

@marvinhagemeister @miguelrk thoughts?

@adamgreg
Copy link
Contributor

Transformer support is perhaps not essential... however... I believe the only way to support transformers would be by plugging into the Preact render, JIT-style. Before running class extraction over the source code, we could run transformers to make sure that all the necessary classes are generated. However, the HTML/DOM rendered JIT by Preact would itself still need to be transformed in order to actually use those generated classes. A disabled-by-default plugin option could enable transformer support, with the added render performance cost.

Maybe at some point UnoCSS will add an extractor which works with un-transformed variant group classes, as an alternative to the variant group transformer. That seems to me the only really compelling use for transformers at the moment.

@marvinhagemeister
Copy link
Collaborator

@adamgreg Oh does UnoCSS only do variants through transpilation?

@adamgreg
Copy link
Contributor

adamgreg commented Oct 11, 2023

@adamgreg Oh does UnoCSS only do variants through transpilation?

Afraid so: https://unocss.dev/transformers/variant-group. It seems to me like it should be possible to extract directly, but maybe the parentheses or spaces would be a problem for class names

@adamgreg
Copy link
Contributor

adamgreg commented Oct 12, 2023

I've just checked, and WindiCSS also transpiles the HTML to make variant groups work.

I think it's inevitable - there's no way to escape spaces in CSS class names. The parentheses aren't a problem though, and it would be possible to implement a variant 🙄 of this feature in a pure extractor, using a different delimiter instead of spaces. I've experimented a little here: https://jsfiddle.net/kypjLdc8/ . I think commas are very understandable. It's more of a UnoCSS feature request than a Fresh one though.

In any case, I think it would be good if the plugin allowed opt-in to transformers by patching into Preact. AOT support is the priority though.

@miguelrk
Copy link
Author

miguelrk commented Nov 3, 2023

As discussed with @adamgreg, closing this in favor of the following with full support for AOT builds:

Linking these other relevant PRs as well:

@miguelrk miguelrk closed this Nov 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants