- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 4.8k
          Resolve @import in core
          #14446
        
          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
  
    Resolve @import in core
  
  #14446
              
            Conversation
95d9682    to
    c3f7963      
    Compare
  
    There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dude this looks 💯 — a handful of questions and comments about stuff but dang this is some epic work.
        
          
                packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css
              
                Outdated
          
            Show resolved
            Hide resolved
        
      | let firstThemeRule: Rule | null = null | ||
| let keyframesRules: Rule[] = [] | ||
| let globs: { origin?: string; pattern: string }[] = [] | ||
| let globs: { base: string; pattern: string }[] = [] | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes me happy 🥳
| Also was thinking but you can eliminate the 2nd walk by making use of object identity. Basically you'll eagerly create the context node, store functions that load the stylesheet, parse it, etc… and then replace the nodes / metadata of the context node when it's done loading. Then you call all the promises after you've done the walk and once they've resolved the AST has been updated.     let ctx = context({ base: '.' }, [])
    replaceWith(ctx)
    if (promises.has(uri)) return
    promises.set(uri, async () => {
      const imported = await loadStylesheet(uri, base)
      let root = CSS.parse(imported.content)
      await substituteAtImports(root, imported.base, loadStylesheet)
      if (layer !== null) {
        root = [rule('@layer ' + layer, root)]
      }
      if (media !== null) {
        root = [rule('@media ' + media, root)]
      }
      if (supports !== null) {
        root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)]
      }
      ctx.context = { base: imported.base }
      ctx.nodes = root
    }) | 
| A realization I just had while prepping Prettier for this change — the current definition of  For example, for a missing plugin I can provide an empty function and for a missing config just an empty object will do. This is necessary because I need intellisense to actually boot even in the face of errors. Once it's up and running we can offer diagnostics pointing out that something is broken / missing. The Prettier plugin is a similar situation — I think it'd be useful for the majority of things to work even if something can't be found — though I could be convinced that for Prettier at least it doesn't matter as much. But at the very least this is pretty critical for Intellisense DX. | 
| 
 @thecrypticace Ah this makes sense, what if we pass a third argument that is either  | 
| 
 Ahh good idea with the mutation! yep this makes sense and I think would make things a bit simpler 👍 | 
| 
 I think  | 
| @thecrypticace regarding this comment: 
 Is this a concern for Intellisense as well? i.e do you introspect the CSS file to determine if it's a likely Tailwind root? Might make it a higher priority to add this sooner rather than later. I guess nothing will break just now though since we still use  | 
| 
 Yep it is but nothing will break because it uses  | 
70bd9bb    to
    2f1dbcc      
    Compare
  
    671e234    to
    f463ead      
    Compare
  
    1188963    to
    60ed8bb      
    Compare
  
    a499e52    to
    dac6cd5      
    Compare
  
    4b92247    to
    9dd29d0      
    Compare
  
    …1058) Related to tailwindlabs/tailwindcss#14446 We'll be handling `@import` resolution in core with the appropriate hooks to ensure that all I/O is done outside of the core package. This PR preps for that.
This PR brings
@importresolution into Tailwind CSS core. This means that our clients (PostCSS, Vite, and CLI) no longer need to depend onpostcssandpostcss-importto resolve@import. Furthermore this simplifies the handling of relative paths for@source,@plugin, or@configin transitive CSS files (where the relative root should always be relative to the CSS file that contains the directive). This PR also fixes a plugin resolution bug where non-relative imports (e.g. directly importing node modules like@plugin '@tailwindcss/typography';) would not work in CSS files that are based in a different npm package.Resolving
@importThe core of the
@importresolution is insidepackages/tailwindcss/src/at-import.ts. There, to keep things performant, we do a two-step process to resolve imports. Imagine the following input CSS file:Since our AST walks are synchronous, we will do a first traversal where we start a loading request for each
@importdirective. Once all loads are started, we will await the promise and do a second walk where we actually replace the AST nodes with their resolved stylesheets. All of this is recursive, so that@import-ed files can again@importother files.The core
@importresolver also includes extensive test cases for various combinations of media query and supports conditionals as well als layered imports.When the same file is imported multiple times, the AST nodes are duplicated but duplicate I/O is avoided on a per-file basis, so this will only load one file, but include the
@themerules twice:Adding a new
contextnode to the ASTOne limitation we had when working with the
postcss-importplugin was the need to do an additional traversal to rewrite relative@source,@plugin, and@configdirectives. This was needed because we want these paths to be relative to the CSS file that defines the directive but when flattening a CSS file, this information is no longer part of the stringifed CSS representation. We worked around this by rewriting the content of these directives to be relative to the input CSS file, which resulted in added complexity and caused a lot of issues with Windows paths in the beginning.Now that we are doing the
@importresolution in core, we can use a different data structure to persist this information. This PR adds a newcontextnode so that we can store arbitrary context like this inside the Ast directly. This allows us to share information with the sub tree while doing the Ast walk.Here's an example of how the new
contextnode can be used to share information with subtrees:In core, we use this new Ast node specifically to persist the
basepath of the current CSS file. We put the input CSS filebaseat the root of the Ast and then overwrite thebaseon every@importsubstitution.Removing the dependency on
postcss-importNow that we support
@importresolution in core, our clients no longer need a dependency onpostcss-import. Furthermore, most dependencies also don't need to know aboutpostcssat all anymore (except the PostCSS client, of course!).This also means that our workaround for rewriting
@source, thepostcss-fix-relative-pathsplugin, can now go away as a shared dependency between all of our clients. Note that we still have it for the PostCSS plugin only, where it's possible that users already havepostcss-importrunning before the@tailwindcss/postcssplugin.Here's an example of the changes to the dependencies for our Vite client ✨ :
Performance
Since our Vite and CLI clients now no longer need to use
postcssat all, we have also measured a significant improvement to the initial build times. For a small test setup that contains only a hand full of files (nothing super-complex), we measured an improvement in the 3.5x range:The code for this is in the commit history if you want to reproduce the results. The test was based on the Vite client.
Caveats
One thing to note is that we previously relied on finding specific symbols in the input CSS to bail out of Tailwind processing completely. E.g. if a file does not contain a
@tailwindor@applydirective, it can never be a Tailwind file.Since we no longer have a string representation of the flattened CSS file, we can no longer do this check. However, the current implementation was already inconsistent with differences on the allowed symbol list between our clients. Ideally, Tailwind CSS should figure out wether a CSS file is a Tailwind CSS file. This, however, is left as an improvement for a future API since it goes hand-in-hand with our planned API changes for the core
tailwindcsspackage.