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

Support loading config files via @config #14239

Merged
merged 42 commits into from
Sep 2, 2024
Merged

Conversation

thecrypticace
Copy link
Contributor

In Tailwind v4 the CSS file is the main entry point to your project and is generally configured via @theme. However, given that all v3 projects were configured via a tailwind.config.js file we definitely need to support those. This PR adds support for loading existing Tailwind config files by adding an @config directive to the CSS — similar to how v3 supported multiple config files except that this is now required to use a config file.

You can load a config file like so:

@import "tailwindcss";
@config "./path/to/tailwind.config.js";

A few notes:

  • Both CommonJS and ESM config files are supported (loaded directly via import() in Node)
  • This is not yet supported in Intellisense or Prettier — should hopefully land next week
  • TypeScript is not yet supported in the config file — this will be handled in a future PR.

Copy link
Member

@adamwathan adamwathan left a comment

Choose a reason for hiding this comment

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

Amazing work getting this working dudes! 🤩 This is a beefy one so I think would be best to do a real review together on a call on Monday morning.

let resolvedPath = path.resolve(inputBasePath, pluginPath)
fullRebuildPaths.push(Promise.resolve([resolvedPath]))
fullRebuildPaths.push(getModuleDependencies(resolvedPath))
return import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()).then(
Copy link
Member

Choose a reason for hiding this comment

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

What does the query string stuff do 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.

It serves as a way to "bust" the cache for the file. ESM doesn't have the equivalent of a require cache in Node so we have to use this to ensure the file is loaded fresh.

Comment on lines 153 to 162
if (configPath[0] !== '.') {
return import(configPath).then((m) => m.default ?? m)
}

return import(pluginPath).then((m) => m.default ?? m)
let resolvedPath = path.resolve(inputBasePath, configPath)
fullRebuildPaths.push(Promise.resolve([resolvedPath]))
fullRebuildPaths.push(getModuleDependencies(resolvedPath))
return import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()).then(
(m) => m.default ?? m,
)
Copy link
Member

Choose a reason for hiding this comment

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

This looks identical to the code for loading plugins, makes me wonder if we should either extract this so we don't have to keep it in sync, or if we should replace loadPlugin and loadConfig with something else that makes more sense, like maybe that's not what we're actually doing here and it's more about loading JS or resolving a module or some other terminology? Not sure 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We talked about exactly this and were both wondering the same thing. I think we can pretty likely merge these two — only thing is if we want to handle errors differently or something.

@@ -128,17 +132,34 @@ export async function handle(args: Result<ReturnType<typeof options>>) {

let inputFile = args['--input'] && args['--input'] !== '-' ? args['--input'] : process.cwd()
let inputBasePath = path.dirname(path.resolve(inputFile))
let fullRebuildPaths: Promise<string[]>[] = [Promise.resolve(cssImportPaths)]
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 to make this an array of arrays just because we want to store these promises before they are resolved and we have no way to store it as a flat list in the first place? Seems fine if so but weird quirk if it's not actually necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah it's because we want to store the promises to be resolved later. Maybe there's something else we could do here though. 🤔

@@ -0,0 +1,44 @@
{
"name": "@tailwindcss/node",
Copy link
Member

Choose a reason for hiding this comment

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

Could this be an internal package like some of our others or do we need to actually publish it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it needs to be because of how the import works otherwise we'll have to duplicate these files into each package + configure the exports (which is something we could do — I just think it's probably a worse tradeoff)

packages/@tailwindcss-node/src/get-module-dependencies.ts Outdated Show resolved Hide resolved
Comment on lines +343 to +351
for (let file of resolvedConfig.content.files) {
if ('raw' in file) {
throw new Error(
`Error in the config file/plugin/preset. The \`content\` key contains a \`raw\` entry:\n\n${JSON.stringify(file, null, 2)}\n\nThis feature is not currently supported.`,
)
}

globs.push(file.pattern)
}
Copy link
Member

Choose a reason for hiding this comment

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

Note that this currently throws away file.base. We'll be fixing this up with another change briefly.

packages/@tailwindcss-node/package.json Outdated Show resolved Hide resolved
packages/@tailwindcss-node/src/get-module-dependencies.ts Outdated Show resolved Hide resolved
packages/@tailwindcss-node/src/require-cache.cts Outdated Show resolved Hide resolved
packages/@tailwindcss-node/src/get-module-dependencies.ts Outdated Show resolved Hide resolved
packages/tailwindcss/src/compat/config.test.ts Outdated Show resolved Hide resolved
packages/tailwindcss/src/compat/dark-mode.ts Outdated Show resolved Hide resolved
packages/tailwindcss/src/compat/dark-mode.ts Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
@RobinMalfait RobinMalfait merged commit 52012d9 into next Sep 2, 2024
3 checks passed
@RobinMalfait RobinMalfait deleted the feat/v4-config-files branch September 2, 2024 16:03
thecrypticace added a commit that referenced this pull request Sep 2, 2024
Builds on #14239 — that PR needs to be merged first.

This PR allows plugins defined with `plugin.withOptions` to receive
options in CSS when using `@plugin` as long as the options are simple
key/value pairs.

For example, the following is now valid and will include the forms
plugin with only the base styles enabled:

```css
@plugin "@tailwindcss/forms" {
  strategy: base;
}
```

We handle `null`, `true`, `false`, and numeric values as expected and
will convert them to their JavaScript equivalents. Comma separated
values are turned into arrays. All other values are converted to
strings.

For example, in the following plugin definition, the options that are
passed to the plugin will be the correct types:
- `debug` will be the boolean value `true`
- `threshold` will be the number `0.5`
- `message` will be the string `"Hello world"`
- `features` will be the array `["base", "responsive"]`

```css
@plugin "my-plugin" {
  debug: false;
  threshold: 0.5;
  message: Hello world;
  features: base, responsive;
}
```

If you need to pass a number or boolean value as a string, you can do so
by wrapping the value in quotes:

```css
@plugin "my-plugin" {
  debug: "false";
  threshold: "0.5";
  message: "Hello world";
}
```

When duplicate options are encountered the last value wins:

```css
@plugin "my-plugin" {
  message: Hello world;
  message: Hello plugin; /* this will be the value of `message` */
}
```

It's important to note that this feature is **only available for plugins
defined with `plugin.withOptions`**. If you try to pass options to a
plugin that doesn't support them, you'll get an error message when
building:

```css
@plugin "my-plugin" {
  debug: false;
  threshold: 0.5;
}

/* Error: The plugin "my-plugin" does not accept options */
```

Additionally, if you try to pass in more complex values like objects or
selectors you'll get an error message:

```css
@plugin "my-plugin" {
  color: { red: 100; green: 200; blue: 300 };
}

/* Error: Objects are not supported in `@plugin` options. */
```

```css
@plugin "my-plugin" {
  .some-selector > * {
    primary: "blue";
    secondary: "green";
  }
}

/* Error: `@plugin` can only contain declarations. */
```

---------

Co-authored-by: Philipp Spiess <hello@philippspiess.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
philipp-spiess added a commit that referenced this pull request Sep 4, 2024
…uilds (#14269)

Fixes #14205
Fixes #14106

This PR reworks the Vite extension in order to supprt `lightningcss` as
the pre-processor, enable faster rebuilds, and adds support for `vite
build --watch` mode. To make this change possible, we've done two major
changes to the extension that have caused the other changes.

## 1. Tailwind CSS is a preprocessor

We now run all of our modifications in `enforce: 'pre'`. This means that
Tailwind CSS now gets the untransformed CSS files rather than the one
already going through postcss or lightningcss. We do this because
Tailwind CSS _is_ a preprocessor at the same level as those tools and we
do sometimes use the language in ways that [creates problems when it's
the input for other
bundlers](#14269).

The correct solution here is to make Tailwind not depend on any other
transformations. The main reason we were not using the `enforce: 'pre'`
phase in Vite before was becuase we relied on the `@import` flattening
of postcss so we now have to do this manually. `@import` flattening is
now a concern that every Tailwind V4 client has to deal with so this
might actually be something we want to inline into tailwindcss in the
future.

## 2. A Vite config can have multiple Tailwind roots 

This is something that we have not made very explicit in the previous
iteration of the Vite plugin but we have to support multiple Tailwind
roots in a single Vite workspace. A Tailwind root is a CSS file that is
used to configure Tailwind. Technically, any CSS file can be the input
for `tailwindcss` but you have to add certain rules (e.g. `@import
"tailwindcss";`) to make the compiler do something.

A workspace can have multiple of these rules (e.g. by having different
Tailwind configures for different sub-pages). With the addition of
[support for `@source`
rules](#14078) and [JS
config files](#14239),
Tailwind roots become more complex and can have a custom list of
_dependencies_ (that is other JavaScript modules the compiler includes
as part of these new rules). In order to _only rebuild the roots we need
to_, we have to make this separation very clear.

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
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.

4 participants