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(svelte-scoped)!: introduce@unocss/svelte-scoped package #2552

Merged
merged 56 commits into from
May 21, 2023

Conversation

jacob-8
Copy link
Contributor

@jacob-8 jacob-8 commented Apr 28, 2023

See discussion starting at #2530 (comment) for context.

Adds @unocss/svelte-scoped with two options:

  • @unocss/svelte-scoped/vite
  • @unocss/svelte-scoped/preprocess

Status:

@netlify
Copy link

netlify bot commented Apr 28, 2023

Deploy Preview for unocss ready!

Name Link
🔨 Latest commit 3baae08
🔍 Latest deploy log https://app.netlify.com/sites/unocss/deploys/6469a5bb79c845000750c79b
😎 Deploy Preview https://deploy-preview-2552--unocss.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@antfu
Copy link
Member

antfu commented May 5, 2023

Thanks a lot for linking the issues. Looking forward to it!

btw @jacob-8 we have an internal channel on Discord, you can join via chat.antfu.me, and let me know who you are so I can give you the role.

alias.ts Outdated Show resolved Hide resolved
vitest.workspace.ts Outdated Show resolved Hide resolved
@@ -0,0 +1,364 @@
/* Tailwind Reset */
Copy link
Member

Choose a reason for hiding this comment

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

Can we reuse @unocss/reset and avoid duplication here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, though I need help to do that. SvelteKit provides no options to import styles in a specific location in the sense that we would normally place the resets at the top of some main.ts like file. So I'm placing them at the start of the global css that will be injected into the head above SvelteKit styles.

In global.ts in my vite plugin I've tried all these iterations with no success:

image

They all incur this error:

error when starting dev server:      
C:\dev\unocss\packages\reset\tailwind.css:6
*,
^

SyntaxError: Unexpected token '*'    
    at Object.compileFunction (node:vm:360:18)

Copy link
Member

Choose a reason for hiding this comment

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

Do we need it to be a string? Can we directly import '@unocss/reset/tailwind.css'?

Copy link
Contributor Author

@jacob-8 jacob-8 May 16, 2023

Choose a reason for hiding this comment

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

Yes, it needs to be a string that I can inject directly into the head along with preflights/safelist (in dev it's inlined, in build it's a turned into a separate hashed css file) because there is no place in SvelteKit into which we can directly import CSS such that it will come first. If user's manually import into it, say into their root +layout.svelte file, the reset could end up being placed into the head tag anywhere amongst other imported styles.

So it looks like the only way we can reuse @unocss/reset/tailwind.css is if a Vite plugin can take a CSS file and give us a string to use in our function. I don't know how to do that. And the only other alternative is to just let users handle resets on their own, separate from Uno. I really, really like the convenience of Svelte Scoped easily providing a reset.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also like the simplicity for adding a reset. Couldn't you just read the file from the filesystem to get the contents?

Copy link
Contributor Author

@jacob-8 jacob-8 May 17, 2023

Choose a reason for hiding this comment

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

the css is always loaded and always before other css from child layouts afaik

EDIT: Not sure how it interacts with other layouts, but the +layout/+page styles ordering is not consistent. See https://stackblitz.com/edit/sveltejs-kit-template-default-ceqnem?file=src%2Froutes%2Fstyles.css,src%2Froutes%2F%2Blayout.svelte,src%2Froutes%2FHeader.svelte

In dev mode, loading from server has this order:

  1. root +layout.svelte style block: breaks things for us
  2. styles.css imported by root +layout.svelte (represents resets/preflights/safelist imported in the root layout)
  3. Header.svelte style block: this component is imported into root layout above global styles simulating a user error - the server handles this in our "desired" manner and imports after stylesheet imports
  4. +page.svelte style block: desired behavior

Then when client hydrates it has this order:

  1. +page.svelte style block: this breaks things for us
  2. Header.svelte style block: the user error of placing this above the stylesheet import now bites us
  3. styles.css
  4. +layout.svelte: now it's correct on client

In each case we want styles.css to come first. The client has an issue with styles placed in the +page.svelte style block and the server has an issue of placing +layout.svelte style block first.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@antfu custom module resolution for injectReset is implemented:

/**
* Inject reset to the beginning of the global stylesheet.
*
* You can pass one of the included resets from [@unocss/reset](https://unocss.dev/guide/style-reset):
* - `@unocss/reset/normalize.css`
* - `@unocss/reset/eric-meyer.css`
* - `@unocss/reset/sanitize/sanitize.css`
* - `@unocss/reset/tailwind.css`
* - `@unocss/reset/tailwind-compat.css`
*
* You can pass your own custom reset by passing the filepath relative to your project root as in `./src/reset.css`
*
* You can install a package then pass a path to the CSS file in your node_modules as in `@bob/my-tools/reset.css`.
* @default undefined
*/
injectReset?: string
export function getReset(injectReset: string): string {
if (injectReset.startsWith('@unocss/reset'))
return readFileSync(resolve(_dirname, `../node_modules/${injectReset}`), 'utf-8')
if (injectReset.startsWith('.')) {
const resolved = resolve(process.cwd(), injectReset)
if (!isFile(resolved))
throw new Error(`"${injectReset}" given as your injectReset value is not a valid file path relative to the root of your project, where your vite config file sits. To give an example, if you placed a reset.css in your src directory, "./src/reset.css" would work.`)
return readFileSync(resolved, 'utf-8')
}
if (injectReset.startsWith('/'))
throw new Error(`Your injectReset value: "${injectReset}" is not a valid file path. To give an example, if you placed a reset.css in your src directory, "./src/reset.css" would work.`)
const resolvedFromNodeModules = resolve(process.cwd(), 'node_modules', injectReset)
if (!isFile(resolvedFromNodeModules))
throw new Error(`"${injectReset}" given as your injectReset value is not a valid file path relative to your project's node_modules folder. Can you confirm that you've installed "${injectReset}"?`)
return readFileSync(resolvedFromNodeModules, 'utf-8')
}
.

If that looks good, this review note can be resolved.

@dominikg, please forgive me if I've come across as argumentative. I admire your contributions to Svelte (literally every time I use the Svelte inspector) and am just happy to be in conversation with you and others who make great tools.

The svelte-scoped mode as it's seen here is the result of me using various iterations of wind+svelte-scoping for almost 2 years. I started with https://github.com/windicss/svelte-windicss-preprocess, then added a preprocessor to run before that preprocessor to overcome some missing features I wanted that are available out of the box with a normal Tailwind setup. I still didn't have feature parity however, and when I came upon UnoCSS, it finally gave a platform where we could build something that had feature parity.

Due to the various complexities of what we are trying to accomplish, I struggle with how to properly explain how it works. I gave another attempt by adding a bit on how it works to the docs and hope that gives more clarity. If not, all I can say is that this is the best way to accomplish utility styles in a distributed fashion across Svelte components that I've used. I've also not seen any other solution that supports all the use cases. It's the only way I've found to be able to do extensive styling in a large and complex app without feeling any friction. It's a wonderful DX in my opinion, and I hope others will like it too.

Choose a reason for hiding this comment

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

don't worry, I like to argument a lot, and it seems you have put a lot of thought into it. I still believe there should be better, less duplicating ways to achieve unocss codesplitting (from what I understand is that you want to contain rarely used tokens to separate .css files that are only loaded on some routes. eg. some specific icons or these complex custom things that tend to creep into projects over time.

As long as there is a way to use unocss+svelte without this svelte-scoped mode and unocss sees a value in it, i'm ok with this feature.

For the technical aspects there might be some misconceptions going on either on my part or on yours. The way you are using :global to escape sveltes own scoping is not really recommended.

:global is meant as the occasional escape hatch and it is recommended to only use it with a prefix like div :global(.something) so that it is still scoped to the component, but applies to anything inside.

So would there be a way where you use svelte's own scoping instead? This should also avoid the ordering issue you described (apart from users putting imports in the wrong order)

Copy link
Member

Choose a reason for hiding this comment

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

So would there be a way where you use svelte's own scoping instead? This should also avoid the ordering issue you described (apart from users putting imports in the wrong order)

Our PostCSS plugin will be doing exactly that. From my comment at #2530 (comment)

I'm working on some sort of scoped feature for our PostCSS plugin that can also be supported by the Vite plugin in the future. I have already created a POC locally. No transformation, no Vite plugin, vite-plugin-svelte does the scoping and hashing for us. It works great. I will make a proposal about it ASAP.

Copy link
Contributor Author

@jacob-8 jacob-8 May 19, 2023

Choose a reason for hiding this comment

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

I have already created a POC locally. No transformation, no Vite plugin, vite-plugin-svelte does the scoping and hashing for us. It works great. I will make a proposal about it ASAP.

Thanks for adding in. I'm looking forward to it.

In your proposal, please address the ability to pass classes to children components for which I've found no solution in Svelte's default hashing. Here's the problem:

To allow for this usage you will need to detect when the class="..." attribute is part of a Component as opposed to an element. Then you will need to generate the css for those classes separately and wrap them in :global() to allow them to have effect when placed into the child component.

That will then lead to bugs in a situation where A passes classes to B that conflict with the classes C passes to D. In this scenario, if C loads later then B's intended behavior will be flipped:

A > B
.hidden .sm:block

C > D
.block .sm:hidden

The only 2 ways around this are to place all styles in a global stylesheet (what everyone does) or to compile the class names into unique names for each component so they don't conflict (what Svelte Scoped does).

So our choices are:

  1. Don't compile class names. Use Svelte's default hashing and adjust Uno's generated CSS to have :global() wrappers only around the parts of rule selectors which are added in generation (e.g. [dir="rtl"]). There is still a good amount of footwork required for this as seen in the apply-directives feature. This means cutting out the passing classes to children feature.
  2. Do all of the above + compile class names just when they are passed to children to be able to keep the feature.
  3. Compile all class names and let them be global.

The first kills a feature. The latter two still require injecting anything that needs to come first, like resets, safelist, etc., into the head before sveltekit styles.

Some like this passing classes feature, and some don't like it, but for my usage Svelte Scoped does not have feature parity with regular global styles setups without it. Who knows, maybe I'm the odd duck though. 😳

packages/svelte-scoped/preprocessor/vitest.config.ts Outdated Show resolved Hide resolved
@BlankParticle
Copy link

As svelte-scoped is being moved to a new package so I am commenting here instead of creating a new issue.
@unocss/preset-web-fonts doesn't work with svelte-scoped, I have tried all providers and tried in both dev and preview mode. It works with @unocss/extractor-svelte. According to my investigation font imports are not added to the generated css.
Can you please check and update me on the issue

@jacob-8
Copy link
Contributor Author

jacob-8 commented May 21, 2023

@unocss/preset-web-fonts doesn't work with svelte-scoped

@BlankParticle, yes, @unocss/preset-web-fonts adds to the preflights which did not have a robust solution previously. This PR solves that issue as you can see in the stackblitz example of svelte-scoped-uno. That package is the same code as this PR and is where the code found in this PR was cultivated. The only difference being the package name and global styles placeholder strings so if you want to try out this PR's features in advance you can follow the install instructions of that package and learn more about usage by using this PR's docs preview.

@antfu antfu changed the title refactor(svelte-scoped)!: transition svelte-scoped mode into its own Vite plugin and Svelte preprocessor feat(svelte-scoped)!: introduce@unocss/svelte-scoped package May 21, 2023
@antfu
Copy link
Member

antfu commented May 21, 2023

Merging this for now to move forward, thanks a lot for the great work!

@antfu antfu merged commit 3358c0f into main May 21, 2023
10 checks passed
@antfu antfu deleted the svelte-scoped-packages branch May 21, 2023 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants