Compare commits

...
Sign in to create a new pull request.

85 commits

Author SHA1 Message Date
Robin Malfait
eb7ba4f44a
bump experimental version 2023-06-19 20:20:23 -04:00
Robin Malfait
54ce83bb86
drop detect nesting 2023-06-19 20:20:07 -04:00
Robin Malfait
ad014a2ec4
disableColorOpacityUtilitiesByDefault but true 2023-06-19 20:20:07 -04:00
Robin Malfait
1797de2042
do not alias shadow to box-shadow 2023-06-19 20:20:07 -04:00
Robin Malfait
cb4fd4fcec
hardocde oxide version 2023-06-19 20:20:06 -04:00
Jordan Pittman
d58afc60d2
Support CSS-based config in the CLI 2023-06-19 17:36:14 -04:00
Jordan Pittman
ed7c448441
Push CSS-based theme resolution inside getTailwindConfig 2023-06-19 17:36:14 -04:00
Jordan Pittman
d88fd1e6e7
Do proper CSS unescaping for variables 2023-06-19 17:36:14 -04:00
Jordan Pittman
42699086c3
Fix test 2023-06-19 17:36:14 -04:00
Jordan Pittman
a347719dc8
wip 2023-06-19 17:36:14 -04:00
Jordan Pittman
526276cb97
wip 2023-06-19 17:36:14 -04:00
Jordan Pittman
bf583334a2
wip 2023-06-19 17:36:13 -04:00
Jordan Pittman
e83435295d
Simplify 2023-06-19 17:36:13 -04:00
Jordan Pittman
67fc3c30e5
Refactor 2023-06-19 17:36:13 -04:00
Jordan Pittman
7573cc7d2a
Fix stuff 2023-06-19 17:36:13 -04:00
Jordan Pittman
971253b1cf
wip 2023-06-19 17:36:13 -04:00
Jordan Pittman
b57d3d1bc5
wip 2023-06-19 17:36:13 -04:00
Jordan Pittman
665569d626
Don’t break tools using custom build 2023-06-19 17:36:13 -04:00
Jordan Pittman
dfb0958147
wip 2023-06-19 17:36:13 -04:00
Jordan Pittman
4ae6f06466
Refactor a bit 2023-06-19 17:36:13 -04:00
Jordan Pittman
8809c6a7fb
Work on implementation 2023-06-19 17:36:13 -04:00
Jordan Pittman
3c38f0a391
Expose theme variables to CSS 2023-06-19 17:36:13 -04:00
Jordan Pittman
ba7d3be00e
Refactor 2023-06-19 17:36:13 -04:00
Adam Wathan
a5305aa84e
Super hacky POC 2023-06-19 17:36:13 -04:00
depfu[bot]
776d5bf614 Update rollup to version 3.25.1 2023-06-19 06:30:22 +00:00
depfu[bot]
ac6e8733e9 Update eslint to version 8.42.0 2023-06-16 21:17:40 +00:00
depfu[bot]
a7a1f9347c Update sass to version 1.63.4 2023-06-16 17:58:29 +00:00
Robin Malfait
86f9c6f092
Simplify browserslist setup by relying on defaults (#11412)
* use `defaults` for `browserslist`

+ some features that we require for Tailwind to work.

* simplify Lightning CSS features

Always transpile `Nesting`, never transpile `LogicalProperties`.

* bump caniuse-lite (`npx browserslist@latest --update-db`)

* reflect changes in the tests

* update changelog
2023-06-12 22:07:06 +02:00
cunzaizhuyi
3be258c218
chore: make sentence smooth (#11407)
* chore: make sentence smooth

* chore: everything typo

* chore: its

* feat: e.g.

* chore: a => an; remove repeated `to`

* Update offsets.js

* Update generateRules.js

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2023-06-11 11:06:03 -04:00
Robin Malfait
ef2ebb28d5
Explicitly configure Lightning CSS features, and prefer user browserslist over default browserslist (#11402)
* exclude `Features.LogicalProperties` lightningcss feature

We don't want this to automatically apply since we have dedicated
logical property based utilities already.

We also have a feature flag to toggle some utilities to make use of
logical property based ones.

This feature should be opt-in to prevent backwards compatibility (e.g.:
RTL sites that _did_ use a mr-4 explicitly).

* update default browserslist

* explicitly include/exclude Lightning CSS features

* update tests based on new browserslist and include/exclude features from Lightning CSS

* update integration tests

* prefer user browserslist, fallback to built-in browserslist

* always include Nesting

If a custom browserslist config is used, then we don't explicitly set
the include/exclude features from Lightning CSS (except for nesting, we
always want to process nesting)

* ensure to fallback to the current working directory

* update changelog
2023-06-09 20:37:36 +02:00
Robin Malfait
ea3d9cd57a
compare the correct values in Jest tests 2023-06-09 16:04:24 +02:00
Jordan Pittman
e51b09ca65
Move unknown pseudo elements outside of :is (#11345)
* More pseudo elements outside of `:is` by default

* Update changelog
2023-06-08 15:00:58 -04:00
Robin Malfait
eb8d9294c5
Make the Rust based parser the default (#11394)
* add failling test using array syntax without space delimiter

* add Rust test with candidates in array without spaces

* Fix JS arrays without spaces

* make the `oxideParser` the default

* sync feature flags with reality

* use better example in tests to be more real

* skip failing tests in the RegEx parser

* update changelog

* make `clippy` happy

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-06-08 14:13:21 +02:00
Robin Malfait
55daf8e0f5
Ensure the oxide parser has feature parity with the stable RegEx parser (#11389)
* WIP

* use `parse` instead of `defaultExtractor`

* skip `Vue` describe block

* add a few more dedicated arbitrary values/properties tests

* use parallel parsing

* splitup Vue tests

* add some Rust specific tests

* setup parse candidate strings test system

These tests will run against the `Regex` and `Rust` based parsers. We
have groups of classes of various shapes and forms + variants and
rendered in various template situation (plain, html, Vue, ...)

+ enable all skipped tests

* ensure we also validate the classes with variants

The classes with variants are built in the `templateTable` function, so
we get them out again by using the potional arguments of the `test.each`
cb function.

* cleanup test suite

* add "anti-test" tests

To make sure that we are _not_ parsing out certain values given a
certain input.

* Add ParseAction enum

* Restart parsing following an arbitrary parse failure

* Split variants off before validating the uility part

* Collapse candidate from the end when validation fails

* Support `<`, and `>` in variant position

* fix error

* format parser.rs

* Refactor

* Update editorconfig

* wip

* wip

* Refactor

* Refactor

* Simplify

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* run `cargo clippy --fix`

* run `cargo fmt`

* implement `cargo clippy` suggestions

These were not applied using `cargo clippy --fix`

* only allow `.` in the candidate part when surrounded by 0-9

This is only in the candidate part, not the arbitrary part.

* % characters can only appear at the end after digits

* > and < should only be part of variants (start OR end)

It can technically be inside the candidate when we have stacked
variants:
```
dark:<sm:underline
dark:md>:underline
```

* handle parsing utilities within quotes, parans or brackets

* mark `pt-1.5` as an expected value sliced out from `["pt-1.5"]`

* Add cursor abstraction

* wip

* disable the oxideParser if using a custom `prefix` or `separator`

* update tests

* Use cursor abstraction

* Refactor more code toward use of global cursor

* wip

* simplify

* Simplify

* Simplify

* Simplify

* Cleanup

* wip

* Simplify

* wip

* Simplify

* Handle candidates ending with % sign

* Tweak code a bit

* fmt

* Simplify

* Add cursor details to trace

* cargo fmt

* use preferred `zoom-0.5` name instead of `zoom-.5`

* drop over-extracted utilities in oxide parser

The RegEx parser does extract `underline` from

```html
<div class="peer-aria-[labelledby='a_b']:underline"></div>
```
... but that's not needed and is not happening in the oxide parser

This means that we have to make the output check a little bit different
but they are explicit based on the feature flag.

* allow extracting variants+utilities inside `{}` for the oxide parser

* characters in candidates such as `group-${id}` should not be allowed

* do not extract any of the following candidate `w-[foo-bar]w-[bar-baz]`

* ensure we can consume the full candidate and discard it

* Add fast skipping of whitespace

* Use fast skipping whenever possible

* Add fast skipping to benchmark

* Hand-tune to generate more optimized assembly

* Move code around a bit

This makes sure all the fancy SIMD stuff is as early as possible. This results in an extremely minor perf increase.

* Undo tweak

no meaningful perf difference in real world scenarios

* Disable fast skipping for now

It needs to be done in a different spot so it doesn’t affect how things are returned

* Change test names

* Fix normalize config error

* cleanup a bit

* Cleanup

* Extract validation result enum

* Cleanup comments

* Simplify

* Fix formatting

* Run clippy

* wip

* add `md>` under the special characters test set

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-06-07 17:44:35 +02:00
Robin Malfait
e572dc645a
Bump lightningcss dependency (#11388)
* bump `lightningcss`

* allow for non-standard selector combinators such as `::deep`

* just use Prettier when comparing CSS results

We were using 2 different engines where the stable one was not using
Lightning CSS and the Oxide one was using Lightning CSS.

To ensure that we didn't have to rewrite every single test expectation,
the `toMatchFormattedCss` parsed both the actual and expected value
using Lightning CSS (to make the result similar), then it used Prettier
to make it... pretty.

Right now we _only_ use Lightning CSS, which means that we can drop the
additional lightningcss format step and just use Prettier on both the
actual and expected values.

Pretty also only prettifies the CSS, it doesn't rewrite it. E.g.:
`@media (min-width: 768px)` will not be optimized to `@media (width >=
768px)`, that's something that Lightning CSS does for us.

This will require some changes in our test output, but it will be
consistent afterwards because there won't be hidden transformation steps
anymore. Because up until now it could be that the actual result was
`color: black` but the tests showed `color: #000` (because it is
shorter).

This change will reflect reality.

* update tests based on previous commit

* only use `toMatchFormattedCss` instead of `toMatchCss`

They both do the exact same thing right now. While `toMatchCss` is
shorter, `toMatchFormattedCss` makes a bit more sense since we are
comparing the Prettier results.

* update integration tests
2023-06-07 15:56:32 +02:00
depfu[bot]
571c27e1f1 Update @swc/core to version 1.3.62 2023-06-06 08:00:26 +00:00
depfu[bot]
fd74f04437 Update vite to version 4.3.9 2023-06-06 04:12:11 +00:00
depfu[bot]
f661c0371c Update browserslist to version 4.21.7 2023-06-04 10:31:31 +00:00
depfu[bot]
729dafeb82 Update postcss to version 8.4.24 2023-06-04 10:00:20 +00:00
Robin Malfait
b05b617601
Bump Rust version to 1.70.0 (#11344)
* `cargo clippy --fix`

* use `map_while` instead of `filter_map`

- `filter_map()` will run forever if the iterator repeatedly produces an `Err`

```
❯ cargo clippy
    Checking tailwindcss-oxide v0.1.0 (/Users/robin/github.com/tailwindlabs/tailwindcss/oxide/crates/cli)
warning: `filter_map()` will run forever if the iterator repeatedly produces an `Err`
   --> crates/cli/src/main.rs:108:14
    |
108 |             .filter_map(Result::ok)
    |              ^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `map_while(Result::ok)`
    |
note: this expression returning a `std::io::Lines` may produce an infinite number of `Err` in case of a read error
   --> crates/cli/src/main.rs:106:9
    |
106 | /         read_lines(path)
107 | |             .unwrap()
    | |_____________________^
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok
    = note: `#[warn(clippy::lines_filter_map_ok)]` on by default

warning: `tailwindcss-oxide` (bin "tailwindcss-oxide") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.13s
```

* log used versions of all the tools we are using

* ensure we install Rust
2023-06-02 15:24:40 +02:00
depfu[bot]
9794b83985 Update parcel to version 2.9.1 2023-06-02 08:44:39 +00:00
Robin Malfait
5ddd9c4db6
Fallback to RegEx based parser when using custom transformers or extractors (#11335)
* handle custom `transformer` and `extractor` implementations

Right now the Rust based parser can't work with custom `transfomers` or
`extractors`.

In a perfect world we can implement as many custom parsers/extractors in
Rust such that we don't need this at all. In an almost perfect world we
can pass the transformer and extractor to the Rust based parser and call
the callback functions to handle all of this. This is probably what we
are going to do in the future but this requires more work to make sure
that:

1. It works as expected
2. Doesn't result in (major) performance issues

Since it currently doesn't work with the Rust based parser, we can
implement a fix for this in the meantime before we reach the "perfect"
solution.

One solution to this problem is to check if we do have a custom
transformer or a custom extractor and if we do, then we can bail on the
Rust parser completely and just use the current regex based parser.

An alternative solution, the solution implemented here, is that we group
the `changedContent` into 2 buckets. The bucket where we rely on the
default transformer and extractor and a bucket where a custom
transformer or extractor is used.

Then, the bucket where we use the default transformer and extractor can
still rely on the way faster Rust based parser. For the other bucket we
fallback to the regex based parser.

The nice part about this is that we can use both parsers at the same
time, and the majority of the use cases should use the faster Rust based
parser.

* update changelog
2023-06-01 22:37:12 +02:00
Robin Malfait
5c7a8e98ea
use parseCandidateStrings directly (#11332)
+ drop the `parseCandidateStringsFromFiles` since this can be
  implemented using `parseCandidateStrings` already.
2023-06-01 13:24:53 +02:00
Robin Malfait
352c563d99
Fix source maps issue resulting in a crash (#11319)
* only try to pretty print `err.source` if it exists

* fix small typo

* drop unnecessary path resolving

* add `from` and `to` to `run` test helper

* enable all source map tests

* ensure source maps are generated properly

* update annotation values in tests

* handle `map: true`

* add test for `map: { inline: true }`

* force Lightning CSS to handle an inline source map

* base64 encode the test name

This will get rid of special characters that can cause issues /
confusion when debugging tests.

* ensure utilities and components with variants are pointing to the correct source

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* update changelog

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-05-31 18:28:11 +02:00
Robin Malfait
ecebde1d52
bump napi-rs in Cargo.toml 2023-05-31 15:31:38 +02:00
Robin Malfait
c9138b2e0b
bump napi-rs (#11328) 2023-05-31 13:44:28 +02:00
Adam Wathan
cb1a0e1a95
Add support for text-wrap property (#11320)
Update changelog

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2023-05-30 15:26:27 -04:00
Adam Wathan
e1d4955431 Move entry in changelog 2023-05-30 12:21:51 -04:00
Adam Wathan
69d86873c3
Add has-* variants for :has(...) pseudo-class (#11318)
* Add `has-*` variants for `:has(...)` pseudo-class

* Update changelog

* Fix mistake in test

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2023-05-30 12:21:08 -04:00
Adam Wathan
7248f2121e
Add svh, lvh, and dvh values to default theme (#11317)
* Add svh, lvh, and dvh values to default theme

* Update changelog

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2023-05-30 11:58:16 -04:00
Robin Malfait
820815be0b
Remove autoprefixer dependency (#11315)
* remove `autoprefixer` dependency

This was only used in the CLI and we are not using it anymore. The
functionality of `autoprefixer` is baked into `lightningcss`.

* update changelog
2023-05-30 13:27:15 +02:00
depfu[bot]
8719be3944 Update rollup to version 3.23.0 2023-05-29 06:30:31 +00:00
Adam Wathan
fd500fcd0c Exclude variantPlugins from corePluginList
This change avoids unnecessarily registering variantPlugins twice.
2023-05-26 21:10:02 -04:00
Adam Wathan
0917fcaa33 Don't differentiate between oxide/stable engines in CHANGELOG 2023-05-25 10:12:18 -04:00
Robin Malfait
04c4de1997
Deprecate --no-autoprefixer flag in the CLI (#11280)
* enable `--no-autoprefixer` test

* handle `--no-autoprefixer` using lightningcss

* bump default chrome version for lightningcss plugin

* handle `map` being nullable

* add `--no-autoprefixer` test including nesting

* set Chrome to version 111 as a default value

* resolve the output message when resolving the promise

* deprecate `--no-autoprefixer` for the CLI

* update changelog
2023-05-25 16:06:00 +02:00
Robin Malfait
88ab01c7f8
update build-linux steps in release workflows 2023-05-25 12:15:55 +02:00
Robin Malfait
49400f90f1
fix CI badge in README 2023-05-25 12:01:03 +02:00
Robin Malfait
581cf6abcc
Merge engines (#11275)
* Simplify CI, make oxide engine the default (#11281)

* remove integration tests for the `stable` engine

* make `integration-tests-oxide` the default integration tests workflow

* remove unnecessary CACHE_PREFIX

* drop `ci-stable.yml` workflow

* make `CI — Oxide` the default

+ drop testing against 3.3 branch (since this is stable only)
+ remove unnecessary CACHE_PREFIX

* drop `release-insiders-stable.yml` workflow

* make `Release Insiders — Oxide` the default

+ change release channel to just `insiders` instead of `oxide-insders`

* drop 3.3 branch from integration tests

* change job name for insiders release

This makes it consistent with the other job names

* prep `release-oxide.yml` workflow to be the default workflow

* add Tailwind Play update

Currently commented out until we figure out how this will work exactly.

* use `env.VERSION` for the version we want to release

* drop `release-stable.yml` workflow

* make `release-oxide.yml` the default `release.yml` workflow

* inline "Calculate environment variables" step

* cache node_modules and cargo related files/folders in the release step

* always use the default CLI

* drop oxide specific implementation

* ensure we test `--postcss` in the CLI

Even for the Oxide engine

* always process `@import` rules

* remove `generalizedModifiers` flag

This was already enabled by default. This commits removes some of the
outstanding references to it.

* remove built-in peer dependencies for the CLI

* drop unused esbuild dependency

* fix type for `configBag`

* setup Lightning CSS for the CLI

* drop `cssnano` dependency

We only used this in the CLI and we now use Lighting CSS which has this
built in already. Therefore we can get rid of this as a dependency.

* ensure `imported.css` file exists in integration tests

* passthrough `options` to lightningcss

Ideally we can read this from `result.map` instead. However this doesn't
get filled in for whatever reason even though we use `map: {inline:true}`.

You can run the `tests/cli.test.js`, and more specifically the
`--postcss supports process options with custom config` test.

* Always process CSS with Lightning CSS

Some tests temporarily skipped. Autoprefixer removed from `postcss.config.js` stubs since vendor prefixing handled by Lightning.

* Remove code for normalizing `@import "tailwindcss/*"` statements

* ensure tailwind doesn't crash when a `tailwind.config.js` file is not present

* add `log.group` to group multiple messages together with different types

* remove engine specific checks in integration tests

* remove unused imports/variables

Make linting check happy

* support `content: ['auto']`

This will allow us to still use auto content detection, but since this
is part of the `content` array, it also allows you to add custom paths
if you want.

E.g.:
```js
content: ['auto', './node_modules/my-library/*.{jsx,tsx}']
```

* ensure `content.files` is always an array

This allows us to simplify some checks because 'auto' will now be part
of the array instead of checking if `content.files` is auto or if it is
an array containing 'auto'.

* fix tests, ensure default config is not injected

* simplify `auto` normalization

* always use the defaultFullConfig

No need to override the defaultFullConfig with `{content: 'auto'}`
because this will be included in the default config already.

* drop condition which ensured `files` was an empty array

This is not needed anymore because when `content` is omitted it behaves
as if it was set to `auto`.

* drop removal of `content`

We used to remove the `content` from the actual configuration files when
using `tailwindcss init` for the oxide engine. This was to ensure that
`auto content detection` was used instead (by default).

But now it is always enabled by default therefore this is not needed
anymore.

* ensure setting `content: 'auto'` works

* improve file cache handling

We have to make sure that we get into the previous state whenver a test
is done.
We were using some `!fileCache[filePath]` checks, however we also used
`null` as a sentinel value when something wasn't found. But, `null` is
falsey as well so some checks where incorrect.

Using a dedicated value and a `Map` makes this safer and more correct
because we can now use `.has()`.

* drop `cleanupFile`, `removeFile` will already take care of it

* drop oxide check in `resolvedChangedContent`

At this point the `context.tailwindConfig.content.files` is already
fully resolved regardless of the engine we are in.

* refactor `parseCandidateStringsFromFiles` strategy, to make use of a proper feature flag

* use feature flag for sorting classes

Once the Oxide engine is the default default, then we can drop this
entirely because the result from the Rust parser will already be sorted.

* drop `crosscheck` from tests where it is safe

First pass of deleting `crosscheck`. These are all the places where we
didn't have any differences between the Oxide and Stable engine.

* replace `__OXIDE__` in corePlugins with feature flag

* use `globalThis.__OXIDE__ ?? false`

This will ensure that in places where we don't have `__OXIDE__` that we
can still properly fallback to `false`.

* use `flex` as the go-to utility when testing features

* prefer `space-utilities` tests instead of `oxide` test

* drop crosscheck check

Since the `oxideParser` is disabled by default, all the tests can use
the `stable` output.

* use simple `false` default values for feature flag defaults

* drop `__OXIDE__` injections in build scripts

* use stable `flex` and `z-{...}` utilities instead of color utilities

flex and z-index related utilities are more stable than the color
utilities right now because the color utilities will use a raw color
value or a combination with a css variable depending on a feature flag.

* drop unused `ENGINE` environment variable

* drop stable engine manifest files

* drop `swap-engines` tooling

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
2023-05-24 22:10:00 -04:00
Daniel Kouznetsov
0af64ee3e8
fixed a type in generateRules.js (#11264) 2023-05-22 08:34:10 -04:00
Robin Malfait
9ebaaaccb3
Explicitly list published CSS files (#11260)
* explicitly list all css files

This also omits the oxide engine only `index.css`

* update changelog
2023-05-19 18:13:02 +02:00
Robin Malfait
f49b054b7d
[Oxide] Add built-in @import processing support (#11239)
* run PostCSS relative to test file

The PostCSS `from` was always set to the `tests/util/run` file with the
current test name as a query param to make it unique. However, that
results in issues with relative file paths because they are now relative
to this file instead of the actual test file.

Luckily, `expect.getState()` exposes the `testPath` value that points to
the current test file itself.

* add built in `@import` processing

+ additional features such that you can use `@tailwind` and `@config`
  directives before the `@import` at rule which isn't normally allowed.
  However "@tailwind" in a way is an `@import` and `@config` in a way is
  a `@charset` which can be used before the `@import` at rule.

* drop partial normalizeTailwindDirectives when in the Oxide engine

This will now be handled by Tailwind from a higher level, so the core
doesn't have to worry about normalizing this anymore since it will
always be normalized right now.

* add `handleImportAtRules` to CLI

* update changelog

* scope `import processing` to the oxide engine

* update Changelog

* use direct comparison

We don't need the `startsWith` check anymore because the contents of the
comment will be the actual value we expect.
2023-05-15 19:29:57 +02:00
Robin Malfait
0e171fd2dd
[Oxide] Ensure generated tailwind.config.js file is formatted properly (#11221)
* ensure beginning `\n` is not removed

The `\s` regex includes newlines `\n`, but we only want leading spaces
to be removed.

* update changelog
2023-05-13 00:15:30 +02:00
Robin Malfait
764f6a19d4
add missing ^ to package-lock.json 2023-05-12 19:35:23 +02:00
Robin Malfait
2f21b22049
update CHANGELOG 2023-05-12 18:58:21 +02:00
Adam Wathan
dce8280d76
[Oxide] Automatic content detection (#11173)
* resolve all _existing_ content paths

* pin `@napi-rs/cli`

* WIP: Log all resolved content files/globs

* only filter out raw changed content in non-auto mode

* skip parseCandidateFiles cache in `auto` mode

* improve algorithm of detecting content paths

1. Files in the root should be listed statically instead of using globs.
2. Files and folders in special known direct child folders should be
   listed statically instead of using globs (e.g.: `public`). This is
   because these special folders are often used to store generated AND
   source files at the same time. Using globs could trigger infinite
   loops because we are watching and acting upon dist files.
3. All file extensions found in the project, should be used in the globs
   in addition to a known set of extensions.
4. Direct folders seen from the root, can use the glob syntax
   `<root>/src/**/*.{...known-extensions}`

* inline wanted-extensions

Not 100% convinced yet, but seems cleaner so far.

* ensure writing an file also makes the parent folder(s)

* add integration tests for the auto content feature

* add pnpm and bun lock files

* Revert "inline wanted-extensions"

This reverts commit 879c1248524e84216125f4a24e0160b40736333a.

* sort binary-extensions and add lockb

* sort + add `lock` to ignored extensions

* drop `yarn.lock`, because lock extensions are already covered

* group template extensions

This will make it a bit easier to organize in the future.

* drop empty lines and commented lines from template-extensions

* skip the config path when resolving template files

The config file will automatically trigger a rebuild when this file is
changed. However, this should not be part of the template files because
that could cause additional css that's not being used.

* make `auto content` the default in the oxide engine

- In the oxide engine, the default `content: []` will be dropped from
  the default configuration (config.simple.js, config.full.js).
- If you have `content: []` or `content: { files: [] }` then the auto
  content feature won't be active. However if those arrays are empty a
  warning will still be shown. Adding files/globs or dropping the
  `content` section completely will enable auto content.

* only test the auto content integration test in the oxide engine

* set `content.files` to `auto` instead of using `auto: boolean`

This way we don't run into the issue where the `config.content.files` is
set and the `config.content.auto` is set to true.

* drop log

* ensure we validate the config in the CLI

* show experimental warning for automatic content detection

* use cached version of the getCandidateFiles instead of bypassing it

* use `is_empty()` shorthand

Thanks, Clippy!

* add test to ensure nested ignored folders are not scanned

* add `tempfile` for tests

* add auto content tests in Rust

* refactor auto content detection

This will also make sure that if we have (deeply) nested ignored
folders, then we won't use deeply nested globs (**/*.{js,html}) for the
parent(s) of the nested ignored folders but instead use a shallow glob
for each directory (*/*.{js,html}).

Then each sibling directory of the parent can use deeply nested globs
again except for the direct parent.

* use consistent comments

* ensure ignored static listed files are not present

* improve performance by ~30x

On a big test project this goes from ~6s to ~200ms

* improve performance by ~5x

We started with a ~6s duration
Then in the previous commit, we improved it by ~30x and it went down to
~200ms
Now with this change, it takes about ~40ms. That's another ~5x
improvement.

Or in total a ~150x improvement.

* ensure nested folders in `public/` are also explicitly listed

* add shortcut for normalizing files

This is only called once so won't do anything to the main performance of
Tailwind CSS. But always nice to make small performance improvements!

* run Rust tests in CI

* fix lint warnings

* update changelog

* Update CHANGELOG.md

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-05-12 18:49:35 +02:00
Robin Malfait
80415ece5e
Support @import "tailwindcss" using top-level index.css file (#11205)
* make `@import "tailwindcss"` work out of the box

* update changelog
2023-05-12 18:49:35 +02:00
depfu[bot]
ae5d746226
Update sass to version 1.62.1 2023-05-12 18:49:35 +02:00
depfu[bot]
8af8e0d1e6
Update cssnano to version 6.0.1 2023-05-12 18:49:35 +02:00
depfu[bot]
3e015818d6
Update postcss-selector-parser to version 6.0.12 2023-05-12 18:49:34 +02:00
depfu[bot]
9b6047d639
Update vite to version 4.3.4 2023-05-12 18:49:33 +02:00
depfu[bot]
9799e88244
Update @swc/core to version 1.3.56 2023-05-12 18:48:57 +02:00
Robin Malfait
a4314c052b
use older version of centos/nodejs-12 2023-05-12 18:47:41 +02:00
Robin Malfait
3309d3f635
[oxide] Expose experimental Rust parser setup (#11116)
* make `sequential` and `parallel` version of a new (tmp) `parse_candidate_strings`

* use bitmasks for the strategy

Only sending a number over the wire instead of a serialized objects.

* use cleaner match syntax
2023-05-12 18:47:41 +02:00
depfu[bot]
225a3fa3c6
Update resolve to version 1.22.3 2023-05-12 18:47:38 +02:00
Jordan Pittman
71205d8fcb
Revert "Update CODE_OF_CONDUCT.md (#10999)"
This reverts commit ba5454543e.
2023-05-12 18:45:43 +02:00
vjdevra
f8506be208
Update CODE_OF_CONDUCT.md (#10999) 2023-05-12 18:45:43 +02:00
Robin Malfait
7dbb59c9db
Revert prepare of v3.3 2023-05-12 18:45:43 +02:00
Robin Malfait
251cc37d7d
Ensure repeating-conic-gradient is detected as an image (#11180)
* ensure `repeating-conic-gradient` is detected as an image

* update changelog
2023-05-08 09:32:58 +02:00
Jordan Pittman
960cb40c6b
Fix parsing of theme() inside calc() when there are no spaces around operators (#11157)
* Refactor

* Don’t resolve functions for anything not using theme or screen

* Normalize math operators inside calc when handling functions

* Inline postcss-value-parser

* Treat all functions the same as calc

* Remove workaround for calc + operators without spaces

* Remove `postcss-value-parser` dependency

* Update lockfile

* Update sourcemaps

* Update changelog

* Update `value-parser` formatting

* Stop prettier from complaining
2023-05-04 10:20:01 -04:00
Adam Wathan
cdca9cbcfe
Update preflight.css 2023-04-28 22:39:08 -04:00
Jordan Pittman
9a57badc9f Update changelog 2023-04-28 12:54:09 -04:00
Eran Hirsch
7b3fe8de0f
Reset dialog element styles (#11069)
* disable useragent styling for dialog

* nits

* Update src/css/preflight.css

* Simplify dialog reset

We don’t want to reset everything here. Just the padding should be enough.

* Update test

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-04-28 12:51:52 -04:00
Anthony Ricaud
1f74b14513
Add aria-busy utility (#10966)
* Add aria-busy variant

This is useful to show a spinner or dim a region that is updating.

Very useful in conjunction with [Turbo](https://turbo.hotwired.dev/reference/attributes#automatically-added-attributes)

* update changelog

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-04-28 12:26:13 +02:00
Dany Castillo
1f1a6faf2c
Make font settings propagate into buttons, inputs, etc. (#10940)
* Make font settings propagate into buttons, inputs, etc.

* update source-maps test due to preflight change

* update changelog

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-04-28 10:06:04 +02:00
Jordan Pittman
ef056a57d3
Fix issues with some pseudo-elements (#11111)
* Fix issues with some pseudo-elements

We’ve included pseudo elements for backdrop, marker, placeholder, and selection and they were all “jumpable” before we made changes in v3.3.2. Ideally they wouldn’t be because if they ever eventually have any interactivity that could become a problem.

* Update changelog
2023-04-27 12:21:28 -04:00
217 changed files with 33625 additions and 51610 deletions

View file

@ -7,3 +7,11 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.rs]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View file

@ -1,59 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: CI — Stable
on:
push:
branches: [master]
pull_request:
branches: [master, 3.3]
permissions:
contents: read
env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: stable
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 18]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm install
- name: Build Tailwind CSS
run: npm run build
- name: Test
run: |
npm run test || \
npm run test || \
npm run test || exit 1
- name: Lint
run: npm run style

View file

@ -1,10 +1,10 @@
name: CI — Oxide
name: CI
on:
push:
branches: [master, 3.3]
branches: [master]
pull_request:
branches: [master, 3.3]
branches: [master]
permissions:
contents: read
@ -13,7 +13,6 @@ env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: oxide
jobs:
build:
@ -34,7 +33,7 @@ jobs:
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
@ -62,14 +61,36 @@ jobs:
- name: Install dependencies
run: npm install
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Check versions
run: |
echo "Node:" `node --version`
echo "NPM:" `npm --version`
echo "Rust:" `rustc --version`
echo "Cargo:" `cargo --version`
- name: Build Tailwind CSS
run: npx turbo run build --filter=//
- name: Test
- name: Test (Node.js)
run: |
npx turbo run test --filter=// || \
npx turbo run test --filter=// || \
npx turbo run test --filter=// || exit 1
- name: Test (Rust)
run: |
cd ./oxide
cargo test || \
cargo test || \
cargo test || exit 1
cd -
- name: Lint
run: npx turbo run style --filter=//

View file

@ -1,83 +0,0 @@
name: Integration Tests — Stable
on:
push:
branches: [master, 3.3]
pull_request:
branches: [master, 3.3]
permissions:
contents: read
env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: stable
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
integration:
- content-resolution
- parcel
- postcss-cli
- rollup
- rollup-sass
- tailwindcss-cli
- vite
- webpack-4
- webpack-5
node-version: [18]
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Cache node_modules (tailwindcss)
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('package-lock.json') }}
- name: Cache node_modules (integrations)
uses: actions/cache@v3
with:
path: ./integrations/node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-integrations-${{ hashFiles('./integrations/package-lock.json') }}
- name: Cache node_modules (integration)
uses: actions/cache@v3
with:
path: ./integrations/${{ matrix.integration }}/node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-integrations-${{ matrix.integration }}-${{ hashFiles('./integrations/${{ matrix.integration }}/package-lock.json') }}
- name: Install dependencies
run: npm install
- name: Install dependencies (shared integrations)
run: npm install --prefix ./integrations
- name: Install dependencies (integration)
run: npm install --prefix ./integrations/${{ matrix.integration }}
- name: Build Tailwind CSS
run: npm run build
- name: Test ${{ matrix.integration }}
run: |
npm run test --prefix ./integrations/${{ matrix.integration }} || \
npm run test --prefix ./integrations/${{ matrix.integration }} || \
npm run test --prefix ./integrations/${{ matrix.integration }} || exit 1

View file

@ -1,10 +1,10 @@
name: Integration Tests — Oxide
name: Integration Tests
on:
push:
branches: [master, 3.3]
branches: [master]
pull_request:
branches: [master, 3.3]
branches: [master]
permissions:
contents: read
@ -13,7 +13,6 @@ env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: oxide
jobs:
test:
@ -46,7 +45,7 @@ jobs:
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
@ -74,6 +73,20 @@ jobs:
- name: Install dependencies
run: npm install
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Check versions
run: |
echo "Node:" `node --version`
echo "NPM:" `npm --version`
echo "Rust:" `rustc --version`
echo "Cargo:" `cargo --version`
- name: Build Tailwind CSS
run: npx turbo run build --filter=//

View file

@ -53,6 +53,20 @@ jobs:
- name: Install dependencies
run: npm install
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Check versions
run: |
echo "Node:" `node --version`
echo "NPM:" `npm --version`
echo "Rust:" `rustc --version`
echo "Cargo:" `cargo --version`
- name: Build Tailwind CSS
run: npm run build

View file

@ -1,78 +0,0 @@
name: Release Insiders — Stable
on:
push:
branches: [master]
permissions:
contents: read
env:
CI: true
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
CACHE_PREFIX: stable
NODE_VERSION: 16
RELEASE_CHANNEL: insiders
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-${{ env.CACHE_PREFIX }}-node_modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm install
- name: Build Tailwind CSS
run: npm run build
- name: Test
run: |
npm run test || \
npm run test || \
npm run test || exit 1
- name: Resolve version
id: vars
run: |
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: 'Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
run: npm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version
- name: Publish
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Trigger Tailwind Play update
uses: actions/github-script@v6
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'tailwindlabs',
repo: 'play.tailwindcss.com',
ref: 'master',
workflow_id: 'upgrade-tailwindcss.yml',
inputs: {
insidersVersion: '0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}'
}
})

View file

@ -1,4 +1,4 @@
name: Release Insiders — Oxide
name: Release Insiders
on:
push:
@ -8,7 +8,7 @@ env:
APP_NAME: tailwindcss-oxide
NODE_VERSION: 16
OXIDE_LOCATION: ./oxide/crates/node
RELEASE_CHANNEL: oxide-insiders
RELEASE_CHANNEL: insiders
jobs:
build:
@ -170,15 +170,13 @@ jobs:
include:
- target: x86_64-unknown-linux-gnu
strip: strip
image: docker.io/centos/nodejs-14-centos7
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
- target: aarch64-unknown-linux-gnu
strip: aarch64-linux-gnu-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
setup: apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y
strip: llvm-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
- target: armv7-unknown-linux-gnueabihf
strip: arm-linux-gnueabihf-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
setup: apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
strip: llvm-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
- target: aarch64-unknown-linux-musl
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
strip: aarch64-linux-musl-strip
@ -353,7 +351,7 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
tailwind_oxide_release:
tailwind-release:
runs-on: ubuntu-latest
name: Build and release Tailwind CSS

View file

@ -1,68 +0,0 @@
name: Release — Stable
on:
release:
types: [published]
permissions:
contents: read
env:
CI: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- name: Use the `stable` engine
run: |
node ./scripts/swap-engines.js
- name: Install dependencies
run: npm install
- name: Build Tailwind CSS
run: npm run build
- name: Test
run: npm run test
- name: Calculate environment variables
run: |
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
- name: Publish
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Trigger Tailwind Play update
if: env.RELEASE_CHANNEL == 'latest'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'tailwindlabs',
repo: 'play.tailwindcss.com',
ref: 'master',
workflow_id: 'upgrade-tailwindcss.yml',
inputs: {
version: '${{ env.TAILWINDCSS_VERSION }}'
}
})

View file

@ -1,15 +1,19 @@
name: Release — Oxide
name: Release
on:
workflow_dispatch:
release:
types: [published]
permissions:
contents: read
env:
APP_NAME: tailwindcss-oxide
NODE_VERSION: 14
NODE_VERSION: 16
OXIDE_LOCATION: ./oxide/crates/node
jobs:
oxide-build:
build:
strategy:
fail-fast: false
matrix:
@ -27,6 +31,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install Node.JS
uses: actions/setup-node@v3
with:
@ -42,18 +75,8 @@ jobs:
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
# - name: Use cached oxide node_modules
# id: cache-oxide
# uses: actions/cache@v3
# with:
# path: ${{ env.OXIDE_LOCATION }}/node_modules
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-${{ matrix.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}
# restore-keys: |
# nodeModules-
- name: Oxide — npm install
# if: steps.cache-oxide.outputs.cache-hit != 'true'
run: cd ${{ env.OXIDE_LOCATION }} && npm install
- name: Install dependencies
run: npm install
- name: Build release
run: cd ${{ env.OXIDE_LOCATION }} && npm run build
@ -72,7 +95,7 @@ jobs:
${{ env.OXIDE_LOCATION }}/*.node
${{ matrix.binary }}
oxide-build-apple-silicon:
build-apple-silicon:
name: Build Apple Silicon (OXIDE)
runs-on: macos-latest
steps:
@ -82,7 +105,7 @@ jobs:
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.node-version }}-node_modules-${{ hashFiles('**/package-lock.json') }}
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
@ -94,7 +117,18 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('./oxide/**/Cargo.lock') }}
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install Node.JS
uses: actions/setup-node@v3
@ -131,22 +165,20 @@ jobs:
${{ env.OXIDE_LOCATION }}/*.node
${{ env.APP_NAME }}
oxide-build-linux:
build-linux:
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
strip: strip
image: docker.io/centos/nodejs-14-centos7
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
- target: aarch64-unknown-linux-gnu
strip: aarch64-linux-gnu-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
setup: apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y
strip: llvm-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
- target: armv7-unknown-linux-gnueabihf
strip: arm-linux-gnueabihf-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
setup: apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf -y
strip: llvm-strip
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-zig
- target: aarch64-unknown-linux-musl
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine
strip: aarch64-linux-musl-strip
@ -162,6 +194,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install Node.JS
uses: actions/setup-node@v3
with:
@ -181,18 +242,8 @@ jobs:
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
# - name: Use cached oxide node_modules
# id: cache-oxide
# uses: actions/cache@v3
# with:
# path: ${{ env.OXIDE_LOCATION }}/node_modules
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-linux-${{ matrix.target }}-${{ env.NODE_VERSION }}
# restore-keys: |
# nodeModules-
- name: Oxide — npm install
# if: steps.cache-oxide.outputs.cache-hit != 'true'
run: cd ${{ env.OXIDE_LOCATION }} && npm install
- name: Install dependencies
run: npm install
- name: Build release
run: cd ${{ env.OXIDE_LOCATION }} && npm run build
@ -211,28 +262,52 @@ jobs:
${{ env.OXIDE_LOCATION }}/*.node
${{ env.APP_NAME }}
oxide-release:
release:
runs-on: ubuntu-latest
name: Build and release
needs:
- oxide-build
- oxide-build-linux
- oxide-build-apple-silicon
- build
- build-linux
- build-apple-silicon
steps:
- uses: actions/checkout@v3
# - name: Use cached oxide node_modules
# id: cache-oxide
# uses: actions/cache@v3
# with:
# path: ${{ env.OXIDE_LOCATION }}/node_modules
# key: nodeModules-${{ hashFiles('${{ env.OXIDE_LOCATION }}/package-lock.json') }}-ubuntu-latest-${{ env.NODE_VERSION }}
# restore-keys: |
# nodeModules-
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
- name: Oxide — npm install
# if: steps.cache-oxide.outputs.cache-hit != 'true'
run: cd ${{ env.OXIDE_LOCATION }} && npm install
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Install dependencies
run: npm install
- name: Calculate environment variables
run: |
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
echo "VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
- name: Download artifacts
uses: actions/download-artifact@v3
@ -255,11 +330,6 @@ jobs:
cp bindings-x86_64-unknown-linux-gnu/oxide/crates/node/* ./npm/linux-x64-gnu/
cp bindings-x86_64-unknown-linux-musl/oxide/crates/node/* ./npm/linux-x64-musl/
- name: Resolve version
id: vars
run: |
echo "NPM_VERSION=0.0.0-insiders.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Generate entry point
run: npm run build --prefix ${{ env.OXIDE_LOCATION }}
@ -268,50 +338,74 @@ jobs:
for pkg in ${{ env.OXIDE_LOCATION }}/npm/*; do
echo "Publishing $pkg..."
cd $pkg
npm version ${{ env.NPM_VERSION }} --force --no-git-tag-version
npm publish --tag insiders --access public
npm version ${{ env.VERSION }} --force --no-git-tag-version
npm publish --tag ${{ env.RELEASE_CHANNEL }} --access public
cd -
done
echo "Publishing ${{ env.APP_NAME }}...";
cd ${{ env.OXIDE_LOCATION }}
npm version ${{ env.NPM_VERSION }} --force --no-git-tag-version
sed "s#\"0.0.0\"#\"${{ env.NPM_VERSION }}\"#g" package.json > package_updated.json
npm version ${{ env.VERSION }} --force --no-git-tag-version
sed "s#\"0.0.0\"#\"${{ env.VERSION }}\"#g" package.json > package_updated.json
mv package_updated.json package.json
npm publish --tag insiders --access public
npm publish --tag ${{ env.RELEASE_CHANNEL }} --access public
cd -
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
tailwind_oxide_release:
tailwind-release:
runs-on: ubuntu-latest
name: Build and release Tailwind CSS
needs:
- oxide-release
strategy:
matrix:
node-version: [14]
- release
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-${{ matrix.target }}-${{ env.NODE_VERSION }}-node_modules-${{ hashFiles('**/package-lock.json') }}
# Cargo already skips downloading dependencies if they already exist
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the `oxide` Rust build
- name: Cache oxide build
uses: actions/cache@v3
with:
path: |
./oxide/target/
./oxide/crates/node/*.node
./oxide/crates/node/index.js
./oxide/crates/node/index.d.ts
key: ${{ runner.os }}-${{ matrix.target }}-oxide-${{ hashFiles('./oxide/crates/**/*') }}
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'
- name: Resolve version
id: vars
- name: Calculate environment variables
run: |
echo "SHA_SHORT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OXIDE_NPM_VERSION=0.0.0-insiders.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV
echo "VERSION=$(node -e 'console.log(require(`./package.json`).version);')" >> $GITHUB_ENV
- name: Setup `@tailwindcss/oxide` version
run: |
sed "s#\"@tailwindcss/oxide\": \".*\"#\"@tailwindcss/oxide\": \"${{ env.OXIDE_NPM_VERSION }}\"#" package.json > package_updated.json
sed "s#\"@tailwindcss/oxide\": \".*\"#\"@tailwindcss/oxide\": \"${{ env.VERSION }}\"#" package.json > package_updated.json
mv package_updated.json package.json
- name: Install dependencies
@ -321,12 +415,31 @@ jobs:
run: npm run build
- name: Test
run: npm test
run: |
npm test || \
npm test || \
npm test || exit 1
- name: 'Version based on commit: 0.0.0-oxide.${{ env.SHA_SHORT }}'
run: npm version 0.0.0-oxide.${{ env.SHA_SHORT }} --force --no-git-tag-version
- name: 'Version: ${{ env.VERSION }}'
run: npm version ${{ env.VERSION }} --force --no-git-tag-version
- name: Publish
run: npm publish --tag oxide
run: npm publish --tag ${{ env.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# - name: Trigger Tailwind Play update
# if: env.RELEASE_CHANNEL == 'latest'
# uses: actions/github-script@v6
# with:
# github-token: ${{ secrets.TAILWIND_PLAY_TOKEN }}
# script: |
# await github.rest.actions.createWorkflowDispatch({
# owner: 'tailwindlabs',
# repo: 'play.tailwindcss.com',
# ref: 'master',
# workflow_id: 'upgrade-tailwindcss.yml',
# inputs: {
# version: '${{ env.TAILWINDCSS_VERSION }}'
# }
# })

View file

@ -7,7 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- Nothing yet!
### Fixed
- Fix issue where some pseudo-element variants generated the wrong selector ([#10943](https://github.com/tailwindlabs/tailwindcss/pull/10943), [#10962](https://github.com/tailwindlabs/tailwindcss/pull/10962))
- Make font settings propagate into buttons, inputs, etc. ([#10940](https://github.com/tailwindlabs/tailwindcss/pull/10940))
- Fix parsing of `theme()` inside `calc()` when there are no spaces around operators ([#11157](https://github.com/tailwindlabs/tailwindcss/pull/11157))
- Ensure `repeating-conic-gradient` is detected as an image ([#11180](https://github.com/tailwindlabs/tailwindcss/pull/11180))
- Remove `autoprefixer` dependency ([#11315](https://github.com/tailwindlabs/tailwindcss/pull/11315))
- Fix source maps issue resulting in a crash ([#11319](https://github.com/tailwindlabs/tailwindcss/pull/11319))
- Fallback to RegEx based parser when using custom transformers or extractors ([#11335](https://github.com/tailwindlabs/tailwindcss/pull/11335))
- Move unknown pseudo-elements outside of `:is` by default ([#11345](https://github.com/tailwindlabs/tailwindcss/pull/11345))
### Added
- Add `aria-busy` utility ([#10966](https://github.com/tailwindlabs/tailwindcss/pull/10966))
- Support `@import "tailwindcss"` using top-level `index.css` file ([#11205](https://github.com/tailwindlabs/tailwindcss/pull/11205), ([#11260](https://github.com/tailwindlabs/tailwindcss/pull/11260)))
- Use `lightningcss` for nesting and vendor prefixes in PostCSS plugin ([#10399](https://github.com/tailwindlabs/tailwindcss/pull/10399))
- Automatically detect content paths when no `content` configuration is provided ([#11173](https://github.com/tailwindlabs/tailwindcss/pull/11173), [#11221](https://github.com/tailwindlabs/tailwindcss/pull/11221))
- Process and inline `@import` at-rules natively ([#11239](https://github.com/tailwindlabs/tailwindcss/pull/11239))
- Add `svh`, `lvh`, and `dvh` values to default `height`/`min-height`/`max-height` theme ([#11317](https://github.com/tailwindlabs/tailwindcss/pull/11317))
- Add `has-*` variants for `:has(...)` pseudo-class ([#11318](https://github.com/tailwindlabs/tailwindcss/pull/11318))
- Add `text-wrap` utilities including `text-balance` ([#11320](https://github.com/tailwindlabs/tailwindcss/pull/11320))
- Explicitly configure Lightning CSS features, and prefer user browserslist over default browserslist ([#11402](https://github.com/tailwindlabs/tailwindcss/pull/11402), [#11412](https://github.com/tailwindlabs/tailwindcss/pull/11412))
### Changed
- Reset padding for `<dialog>` elements in preflight ([#11069](https://github.com/tailwindlabs/tailwindcss/pull/11069))
- Deprecate `--no-autoprefixer` flag in the CLI ([#11280](https://github.com/tailwindlabs/tailwindcss/pull/11280))
- Make the Rust based parser the default ([#11394](https://github.com/tailwindlabs/tailwindcss/pull/11394))
## [3.3.2] - 2023-04-25

View file

@ -14,7 +14,7 @@
<p align="center">
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci-stable.yml?branch=master" alt="Build Status"></a>
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=master" alt="Build Status"></a>
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
<a href="https://github.com/tailwindcss/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
<a href="https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>

5
index.css Normal file
View file

@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -18,7 +18,7 @@
}
},
"devDependencies": {
"postcss": "^8.4.23",
"postcss": "^8.4.24",
"postcss-cli": "^10.1.0"
}
}

View file

@ -60,7 +60,7 @@ it('looks in the CWD by default', async () => {
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-static-positive\] {
--tw-content: real-static-positive;
content: var(--tw-content);
@ -69,7 +69,7 @@ it('looks in the CWD by default', async () => {
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(``)
expect(result.css).toMatchFormattedCss(``)
})
it('looks in the CWD for non-config-relative paths', async () => {
@ -88,7 +88,7 @@ it('looks in the CWD for non-config-relative paths', async () => {
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-static-positive\] {
--tw-content: real-static-positive;
content: var(--tw-content);
@ -97,7 +97,7 @@ it('looks in the CWD for non-config-relative paths', async () => {
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(``)
expect(result.css).toMatchFormattedCss(``)
})
it('can look for content files relative to the config', async () => {
@ -113,12 +113,12 @@ it('can look for content files relative to the config', async () => {
// Here `./real` doesn't exist next to the config in the root directory
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(css``)
expect(result.css).toMatchFormattedCss(css``)
// But here it `./real` does exist next to the config in the `./src` directory!
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-static-positive\] {
--tw-content: real-static-positive;
content: var(--tw-content);
@ -143,7 +143,7 @@ it('it handles ignored globs correctly when not relative to the config', async (
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-dynamic-positive\] {
--tw-content: real-dynamic-positive;
content: var(--tw-content);
@ -157,7 +157,7 @@ it('it handles ignored globs correctly when not relative to the config', async (
// But here it `./real` does exist next to the config in the `./src` directory!
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(``)
expect(result.css).toMatchFormattedCss(``)
})
it('it handles ignored globs correctly when relative to the config', async () => {
@ -177,12 +177,12 @@ it('it handles ignored globs correctly when relative to the config', async () =>
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(``)
expect(result.css).toMatchFormattedCss(``)
// But here it `./real` does exist next to the config in the `./src` directory!
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-dynamic-positive\] {
--tw-content: real-dynamic-positive;
content: var(--tw-content);
@ -215,7 +215,7 @@ it('it can resolve symlinks for files when not relative to the config', async ()
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-dynamic-positive\] {
--tw-content: real-dynamic-positive;
content: var(--tw-content);
@ -237,7 +237,7 @@ it('it can resolve symlinks for files when not relative to the config', async ()
// But here it `./real` does exist next to the config in the `./src` directory!
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(``)
expect(result.css).toMatchFormattedCss(``)
})
it('it can resolve symlinks for files when relative to the config', async () => {
@ -261,12 +261,12 @@ it('it can resolve symlinks for files when relative to the config', async () =>
let result = await build({ cwd: path.resolve(__dirname, '..') })
expect(result.css).toMatchCss(``)
expect(result.css).toMatchFormattedCss(``)
// But here it `./real` does exist next to the config in the `./src` directory!
result = await build({ cwd: path.resolve(__dirname, '../src') })
expect(result.css).toMatchCss(css`
expect(result.css).toMatchFormattedCss(css`
.content-\[real-dynamic-positive\] {
--tw-content: real-dynamic-positive;
content: var(--tw-content);

View file

@ -64,7 +64,7 @@ module.exports = function $(command, options = {}) {
messages.splice(0, idx + 1)
let actorIdx = actors.indexOf(next)
actors.splice(actorIdx, 1)
next.resolve()
next.resolve(message)
break
}
}

View file

@ -5,6 +5,9 @@ let fs = require('fs/promises')
let chokidar = require('chokidar')
let resolveToolRoot = require('./resolve-tool-root')
let FILE_STATE = {
NotFound: Symbol(),
}
function getWatcherOptions() {
return {
@ -28,7 +31,7 @@ module.exports = function ({
cleanup = true,
} = {}) {
let toolRoot = resolveToolRoot()
let fileCache = {}
let fileCache = new Map()
let absoluteOutputFolder = path.resolve(toolRoot, output)
let absoluteInputFolder = path.resolve(toolRoot, input)
@ -41,9 +44,9 @@ module.exports = function ({
// Restore all written files
afterEach(async () => {
await Promise.all(
Object.entries(fileCache).map(async ([file, content]) => {
Array.from(fileCache.entries()).map(async ([file, content]) => {
try {
if (content === null) {
if (content === FILE_STATE.NotFound) {
return await fs.unlink(file)
} else {
return await fs.writeFile(file, content, 'utf8')
@ -89,7 +92,7 @@ module.exports = function ({
return {
cleanupFile(file) {
let filePath = path.resolve(toolRoot, file)
fileCache[filePath] = null
fileCache.set(filePath, FILE_STATE.NotFound)
},
async fileExists(file) {
let filePath = path.resolve(toolRoot, file)
@ -98,8 +101,11 @@ module.exports = function ({
async removeFile(file) {
let filePath = path.resolve(toolRoot, file)
if (!fileCache[filePath]) {
fileCache[filePath] = await fs.readFile(filePath, 'utf8').catch(() => null)
if (!fileCache.has(filePath)) {
fileCache.set(
filePath,
await fs.readFile(filePath, 'utf8').catch(() => FILE_STATE.NotFound)
)
}
await fs.unlink(filePath).catch(() => null)
@ -114,20 +120,31 @@ module.exports = function ({
},
async appendToInputFile(file, contents) {
let filePath = path.resolve(absoluteInputFolder, file)
if (!fileCache[filePath]) {
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
if (!fileCache.has(filePath)) {
fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
}
return fs.appendFile(filePath, contents, 'utf8')
},
async writeInputFile(file, contents) {
let filePath = path.resolve(absoluteInputFolder, file)
if (!fileCache[filePath]) {
// Ensure the parent folder of the file exists
if (
!(await fs
.stat(filePath)
.then(() => true)
.catch(() => false))
) {
await fs.mkdir(path.dirname(filePath), { recursive: true })
}
if (!fileCache.has(filePath)) {
try {
fileCache[filePath] = await fs.readFile(filePath, 'utf8')
fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
} catch (err) {
if (err.code === 'ENOENT') {
fileCache[filePath] = null // Sentinel value to `delete` the file afterwards. This also means that we are writing to a `new` file inside the test.
fileCache.set(filePath, FILE_STATE.NotFound)
} else {
throw err
}

View file

@ -19,6 +19,6 @@
}
},
"devDependencies": {
"parcel": "^2.8.3"
"parcel": "^2.9.1"
}
}

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let {
appendToInputFile,
@ -40,7 +39,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await writeInputFile(
@ -58,8 +57,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -74,26 +73,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
@ -102,7 +88,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await writeInputFile(
@ -122,8 +108,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -138,26 +124,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -199,41 +172,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange(/index\.\w+\.css$/, async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -271,41 +225,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange(/index\.\w+\.css$/, async () => {
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -328,7 +263,7 @@ describe('watcher', () => {
.font-bold {
font-weight: 700;
}
@media (min-width: 768px) {
@media (width >= 768px) {
.md\:font-medium {
font-weight: 500;
}
@ -366,7 +301,7 @@ describe('watcher', () => {
.font-bold {
font-weight: bold;
}
@media (min-width: 800px) {
@media (width >= 800px) {
.md\:font-medium {
font-weight: 500;
}
@ -417,11 +352,11 @@ describe('watcher', () => {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
/* prettier-ignore */
.btn {
border-radius: .25rem;
padding: .25rem .5rem;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
@ -438,43 +373,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.btn {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile(/index\.\w+\.css$/)).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -18,7 +18,7 @@
}
},
"devDependencies": {
"postcss": "^8.4.23",
"postcss": "^8.4.24",
"postcss-cli": "^10.1.0"
}
}

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
output: 'dist',
@ -58,41 +57,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -125,41 +105,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -187,23 +148,23 @@ describe('watcher', () => {
await writeInputFile(
'../tailwind.config.js',
javascript`
module.exports = {
content: ['./src/index.html'],
theme: {
extend: {
screens: {
md: '800px'
},
fontWeight: {
bold: 'bold'
}
module.exports = {
content: ['./src/index.html'],
theme: {
extend: {
screens: {
md: '800px'
},
fontWeight: {
bold: 'bold'
}
},
corePlugins: {
preflight: false,
},
plugins: [],
}
},
corePlugins: {
preflight: false,
},
plugins: [],
}
`
)
await runningProcess.onStderr(ready)
@ -277,47 +238,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
/* prettier-ignore */
.btn {
border-radius: 0.25rem;
background-color: #ef4444;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -18,8 +18,8 @@
}
},
"devDependencies": {
"rollup": "^3.21.0",
"rollup": "^3.25.1",
"rollup-plugin-postcss": "^4.0.2",
"sass": "^1.62.0"
"sass": "^1.63.4"
}
}

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
output: 'dist',
@ -58,41 +57,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -125,41 +105,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -275,43 +236,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
@ -370,43 +313,25 @@ describe('watcher', () => {
css`
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -18,7 +18,7 @@
}
},
"devDependencies": {
"rollup": "^3.21.0",
"rollup": "^3.25.1",
"rollup-plugin-postcss": "^4.0.2"
}
}

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile, removeFile } = require('../../io')({
output: 'dist',
@ -30,7 +29,7 @@ describe('static build', () => {
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -46,8 +45,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -62,31 +61,18 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -104,8 +90,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -120,26 +106,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -172,41 +145,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -239,41 +193,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -389,46 +324,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStderr(ready)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
background-color: #ef4444;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('index.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1 @@
<div class="font-bold"></div>

View file

@ -2,7 +2,6 @@ let path = require('path')
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let resolveToolRoot = require('../../resolve-tool-root')
let { env } = require('../../../lib/lib/sharedState')
let version = require('../../../package.json').version
@ -19,7 +18,6 @@ let {
})
let EXECUTABLE = 'node ../../lib/cli.js'
let testStable = env.ENGINE === 'stable' ? test : test.skip
function dedent(input) {
let lines = input.split('\n')
@ -98,26 +96,35 @@ describe('Build command', () => {
expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
})
testStable('--no-autoprefixer', async () => {
// Deprecated
test('--no-autoprefixer (should produce a warning)', async () => {
await writeInputFile('index.html', html`<div class="select-none"></div>`)
await writeInputFile(
'index.css',
css`
@tailwind utilities;
`
)
await $(`${EXECUTABLE} --output ./dist/main.css`)
let withAutoprefixer = await readOutputFile('main.css')
let runningProcess = $(
`${EXECUTABLE} --input ./src/index.css --output ./dist/main.css --no-autoprefixer`
)
let warning = runningProcess.onStderr((message) => message.includes('--no-autoprefixer'))
await runningProcess
expect(await warning).toMatchInlineSnapshot(`
"[deprecation] The --no-autoprefixer flag is deprecated and has no effect.
"
`)
let withoutAutoprefixer = await readOutputFile('main.css')
expect(withAutoprefixer).toIncludeCss(css`
.select-none {
// This contains --webkit-user-select which may be strange, but it is expected because we are
// not handling the `--no-autoprefixer` flag anymore at all.
expect(withoutAutoprefixer).toMatchInlineSnapshot(`
".select-none {
-webkit-user-select: none;
user-select: none;
}
`)
await $(`${EXECUTABLE} --output ./dist/main.css --no-autoprefixer`)
let withoutAutoprefixer = await readOutputFile('main.css')
expect(withoutAutoprefixer).toIncludeCss(css`
.select-none {
user-select: none;
}
"
`)
})
@ -143,7 +150,7 @@ describe('Build command', () => {
theme: {
extend: {
fontWeight: {
bold: 'BOLD',
bold: 'bold',
},
},
},
@ -164,7 +171,7 @@ describe('Build command', () => {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold {
font-weight: BOLD;
font-weight: bold;
}
`
)
@ -184,7 +191,7 @@ describe('Build command', () => {
)
})
testStable('--postcss (postcss.config.js)', async () => {
test('--postcss (postcss.config.js)', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -196,7 +203,7 @@ describe('Build command', () => {
function before(root, result) {
// Inject a custom component with @apply rules to prove that we run
// this _before_ the actual tailwind plugin.
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
root.append(btn.nodes)
},
function tailwindcss() {
@ -217,45 +224,21 @@ describe('Build command', () => {
await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
background-color: #ef4444;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
.btn-after {
padding: 0.25rem 0.5rem;
display: flex;
}
`
)
})
testStable('--postcss (custom.postcss.config.js)', async () => {
test('--postcss (custom.postcss.config.js)', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -267,7 +250,7 @@ describe('Build command', () => {
function before(root, result) {
// Inject a custom component with @apply rules to prove that we run
// this _before_ the actual tailwind plugin.
let btn = postcss.parse('.btn { @apply bg-red-500 px-2 py-1 }')
let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
root.append(btn.nodes)
},
function tailwindcss() {
@ -288,45 +271,21 @@ describe('Build command', () => {
await $(`${EXECUTABLE} --output ./dist/main.css --postcss ./custom.postcss.config.js`)
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold-after {
font-weight: 700;
}
.btn-after {
background-color: #ef4444;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
`
)
}
.btn-after {
padding: 0.25rem 0.5rem;
display: flex;
}
`
)
})
testStable('--postcss supports process options', async () => {
test('--postcss supports process options', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -360,7 +319,7 @@ describe('Build command', () => {
expect(contents).toContain(`/*# sourceMappingURL`)
})
testStable('--postcss supports process options with custom config', async () => {
test('--postcss supports process options with custom config', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
@ -472,7 +431,7 @@ describe('Build command', () => {
css`
@media (min-width: 768px) {
.md\:something-cool {
color: blue;
color: #00f;
}
}
`
@ -481,10 +440,18 @@ describe('Build command', () => {
return runningProcess.stop()
})
testStable('postcss-import is included when using a custom postcss configuration', async () => {
test('postcss-import is included when using a custom postcss configuration', async () => {
cleanupFile('src/test.css')
await writeInputFile('index.html', html`<div class="md:something-cool"></div>`)
await writeInputFile(
'imported.css',
css`
.foo {
color: white;
}
`
)
await writeInputFile(
'test.css',
css`
@ -501,7 +468,9 @@ describe('Build command', () => {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
@import './imported.css';
.foo {
color: #fff;
}
`
)
})
@ -509,28 +478,8 @@ describe('Build command', () => {
test('--help', async () => {
let { combined } = await $(`${EXECUTABLE} --help`)
if (env.ENGINE === 'oxide') {
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
tailwindcss build [options]
Options:
-i, --input Input file
-o, --output Output file
-w, --watch Watch for changes and rebuild as needed
-p, --poll Use polling instead of filesystem events when watching
--content Content paths to use for removing unused classes
-m, --minify Minify the output
-c, --config Path to a custom config file
-h, --help Display usage information
`)
)
} else if (env.ENGINE === 'stable') {
expect(dedent(combined)).toEqual(
dedent(`
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
@ -545,11 +494,9 @@ describe('Build command', () => {
--postcss Load custom PostCSS configuration
-m, --minify Minify the output
-c, --config Path to a custom config file
--no-autoprefixer Disable autoprefixer
-h, --help Display usage information
`)
)
}
)
})
})
@ -562,7 +509,6 @@ describe('Init command', () => {
{ flags: ['--ts', '--full'], name: 'tailwind.config.ts' },
{ flags: ['--esm', '--full'], name: 'tailwind.config.js' },
])('works with all these flags: %j', async ({ flags, name }) => {
cleanupFile(name)
await removeFile(name)
let { combined } = await $(`${EXECUTABLE} init ${flags.join(' ')}`)
@ -611,7 +557,7 @@ describe('Init command', () => {
expect((await readOutputFile('../full.config.js')).split('\n').length).toBeGreaterThan(50)
})
testStable('--postcss', async () => {
test('--postcss', async () => {
expect(await fileExists('postcss.config.js')).toBe(true)
await removeFile('postcss.config.js')
expect(await fileExists('postcss.config.js')).toBe(false)
@ -631,24 +577,8 @@ describe('Init command', () => {
test('--help', async () => {
let { combined } = await $(`${EXECUTABLE} init --help`)
if (env.ENGINE === 'oxide') {
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
tailwindcss init [options]
Options:
--esm Initialize configuration file as ESM
--ts Initialize configuration file as TypeScript
-f, --full Include the default values for all options in the generated configuration file
-h, --help Display usage information
`)
)
} else if (env.ENGINE === 'stable') {
expect(dedent(combined)).toEqual(
dedent(`
expect(dedent(combined)).toEqual(
dedent(`
tailwindcss v${version}
Usage:
@ -661,12 +591,10 @@ describe('Init command', () => {
-f, --full Include the default values for all options in the generated configuration file
-h, --help Display usage information
`)
)
}
)
})
test('ESM config is created by default in an ESM project', async () => {
cleanupFile('tailwind.config.js')
await removeFile('tailwind.config.js')
let pkg = await readOutputFile('../package.json')
@ -696,7 +624,6 @@ describe('Init command', () => {
})
test('CJS config is created by default in a non-ESM project', async () => {
cleanupFile('tailwind.config.js')
await removeFile('tailwind.config.js')
let pkg = await readOutputFile('../package.json')

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,6 @@
},
"devDependencies": {
"isomorphic-fetch": "^3.0.0",
"vite": "^4.3.2"
"vite": "^4.3.9"
}
}

View file

@ -2,7 +2,6 @@ require('isomorphic-fetch')
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let { readOutputFile, appendToInputFile, writeInputFile, removeFile } = require('../../io')({
output: 'dist',
@ -48,7 +47,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await removeFile('tailwind.config.js')
@ -59,8 +58,8 @@ describe('static build', () => {
content: ['index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -75,26 +74,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production', NO_COLOR: '1' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
@ -102,7 +88,7 @@ describe('static build', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="bg-primary"></div>
<div class="z-primary"></div>
`
)
await removeFile('tailwind.config.js')
@ -115,8 +101,8 @@ describe('static build', () => {
content: ['index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -131,26 +117,13 @@ describe('static build', () => {
env: { NODE_ENV: 'production', NO_COLOR: '1' },
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile(/index.\w+\.css$/)).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -191,41 +164,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
await runningProcess.onStdout((message) => message.includes('page reload'))
if (env.ENGINE === 'stable') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await fetchCSS()).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -262,41 +216,22 @@ describe('watcher', () => {
`
)
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
await runningProcess.onStdout((message) => message.includes('page reload'))
if (env.ENGINE === 'stable') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await fetchCSS()).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await fetchCSS()).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -428,49 +363,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
await runningProcess.onStdout((message) => message.includes('hmr update /index.css'))
if (env.ENGINE === 'stable') {
expect(await fetchCSS()).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await fetchCSS()).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
background-color: #ef4444;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await fetchCSS()).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let {
appendToInputFile,
@ -28,7 +27,7 @@ describe('static build', () => {
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -44,8 +43,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -58,31 +57,18 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -100,8 +86,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -114,26 +100,13 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -169,41 +142,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -239,41 +193,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('glob/index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('glob/index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -394,46 +329,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let { env } = require('../../../lib/lib/sharedState')
let {
appendToInputFile,
@ -28,7 +27,7 @@ describe('static build', () => {
it('can use a tailwind.config.js configuration file with ESM syntax', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -44,8 +43,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -58,31 +57,18 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
it('can use a tailwind.config.ts configuration file', async () => {
await removeFile('tailwind.config.js')
await writeInputFile('index.html', html`<div class="bg-primary"></div>`)
await writeInputFile('index.html', html`<div class="z-primary"></div>`)
await writeInputFile(
'index.css',
css`
@ -100,8 +86,8 @@ describe('static build', () => {
content: ['./src/index.html'],
theme: {
extend: {
colors: {
primary: 'black',
zIndex: {
primary: 0,
},
},
},
@ -114,26 +100,13 @@ describe('static build', () => {
await $('webpack --mode=production')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-primary {
background-color: black;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-primary {
z-index: 0;
}
`
)
})
})
@ -169,41 +142,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -239,41 +193,22 @@ describe('watcher', () => {
)
await waitForOutputFileChange('main.css', async () => {
await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
await appendToInputFile('index.html', html`<div class="z-0"></div>`)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.font-bold {
font-weight: 700;
}
.font-normal {
font-weight: 400;
}
`
)
return runningProcess.stop()
})
@ -394,46 +329,25 @@ describe('watcher', () => {
@layer components {
.btn {
@apply rounded bg-red-500 px-2 py-1;
@apply rounded z-0 px-2 py-1;
}
}
`
)
})
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
border-radius: 0.25rem;
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
background-color: #ef4444;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.btn {
z-index: 0;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
@ -447,7 +361,7 @@ describe('watcher', () => {
content: {
files: ['./src/index.html'],
},
safelist: ['bg-red-500','bg-red-600'],
safelist: ['z-0', 'z-10'],
theme: {
extend: {
},
@ -464,43 +378,21 @@ describe('watcher', () => {
await waitForOutputFileCreation('main.css')
if (env.ENGINE === 'stable') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.z-0 {
z-index: 0;
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.z-10 {
z-index: 10;
}
.font-bold {
font-weight: 700;
}
`
)
}
if (env.ENGINE === 'oxide') {
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
background-color: #ef4444;
}
.bg-red-600 {
background-color: #dc2626;
}
.font-bold {
font-weight: 700;
}
`
)
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})

View file

@ -1,6 +1,5 @@
const prettier = require('prettier')
const { diff } = require('jest-diff')
const lightningcss = require('lightningcss')
const log = require('../src/util/log').default
let warn
@ -22,46 +21,7 @@ function formatPrettier(input) {
}
function format(input) {
try {
return lightningcss
.transform({
filename: 'input.css',
code: Buffer.from(input),
minify: false,
targets: { chrome: 106 << 16 },
drafts: {
nesting: true,
customMedia: true,
},
})
.code.toString('utf8')
} catch (err) {
try {
// Lightning CSS is pretty strict, so it will fail for `@media screen(md) {}` for example,
// in that case we can fallback to prettier since it doesn't really care. However if an
// actual syntax error is made, then we still want to show the proper error.
return formatPrettier(input.replace(/\n/g, ''))
} catch {
let lines = err.source.split('\n')
let e = new Error(
[
'Error formatting using Lightning CSS:',
'',
...[
'```css',
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
...lines.slice(err.loc.line, err.loc.line + 2),
'```',
],
].join('\n')
)
if (Error.captureStackTrace) {
Error.captureStackTrace(e, toMatchFormattedCss)
}
throw e
}
}
return formatPrettier(input).replace(/\n{2,}/g, '\n')
}
function toMatchFormattedCss(received = '', argument = '') {
@ -86,10 +46,7 @@ function toMatchFormattedCss(received = '', argument = '') {
)
}
: () => {
let actual = formatPrettier(formattedReceived).replace(/\n\n/g, '\n')
let expected = formatPrettier(formattedArgument).replace(/\n\n/g, '\n')
let diffString = diff(expected, actual, {
let diffString = diff(formattedArgument, formattedReceived, {
expand: this.expand,
})
@ -98,8 +55,8 @@ function toMatchFormattedCss(received = '', argument = '') {
'\n\n' +
(diffString && diffString.includes('- Expect')
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(actual)}`)
: `Expected: ${this.utils.printExpected(formattedArgument)}\n` +
`Received: ${this.utils.printReceived(formattedReceived)}`)
)
}
@ -107,13 +64,10 @@ function toMatchFormattedCss(received = '', argument = '') {
}
expect.extend({
// Compare two CSS strings with all whitespace removed
// This is probably naive but it's fast and works well enough.
toMatchCss: toMatchFormattedCss,
toMatchFormattedCss: toMatchFormattedCss,
toIncludeCss(received, argument) {
let options = {
comment: 'stripped(received).includes(stripped(argument))',
comment: 'formatCSS(received).includes(formatCSS(argument))',
isNot: this.isNot,
promise: this.promise,
}

687
oxide/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -105,7 +105,7 @@ fn main() -> Result<(), std::io::Error> {
let candidates = content_paths.par_bridge().flat_map(|path| {
read_lines(path)
.unwrap()
.filter_map(Result::ok)
.map_while(Result::ok)
.par_bridge()
.flat_map_iter(|line| {
Extractor::unique(line.as_bytes(), Default::default())

View file

@ -12,14 +12,17 @@ fxhash = "0.2.1"
crossbeam = "0.8.2"
tracing = { version = "0.1.37", features = [] }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
walkdir = "2.3.3"
ignore = "0.4.20"
[dev-dependencies]
criterion = { version = "0.3", features = ['html_reports'] }
tempfile = "3.5.0"
[[bench]]
name = "parse_candidates"
harness = false
[[bench]]
name = "parse_candidate_strings_from_files"
name = "parse_candidate_strings"
harness = false

View file

@ -1,6 +1,6 @@
use criterion::{criterion_group, criterion_main, Criterion};
use std::path::PathBuf;
use tailwindcss_core::{parse_candidate_strings_from_files, ChangedContent};
use tailwindcss_core::{parse_candidate_strings, ChangedContent, Parsing, IO};
pub fn criterion_benchmark(c: &mut Criterion) {
// current_dir will be set to ./crates/core
@ -11,7 +11,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let mut all_files: Vec<(u64, PathBuf)> = std::fs::read_dir(fixtures_path)
.unwrap()
.into_iter()
.filter_map(Result::ok)
.map(|dir_entry| dir_entry.path())
.filter(|path| path.is_file())
@ -51,8 +50,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
})
.collect();
c.bench_function("parse_candidate_strings_from_files", |b| {
b.iter(|| parse_candidate_strings_from_files(changed_content.clone()))
c.bench_function("parse_candidate_strings", |b| {
b.iter(|| {
parse_candidate_strings(
changed_content.clone(),
Parsing::Parallel as u8 | IO::Parallel as u8,
)
})
});
}

View file

@ -31,6 +31,18 @@ pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("parse_candidate_strings (real world)", |b| {
b.iter(|| parse(include_bytes!("./fixtures/template-499.html")))
});
let mut group = c.benchmark_group("sample-size-example");
group.sample_size(10);
group.bench_function("parse_candidate_strings (fast space skipping)", |b| {
let count = 10_000;
let crazy1 = format!("{}underline", " ".repeat(count));
let crazy2 = crazy1.repeat(count);
let crazy3 = crazy2.as_bytes();
b.iter(|| parse(black_box(crazy3)))
});
}
criterion_group!(benches, criterion_benchmark);

View file

@ -22,7 +22,6 @@ impl Candidate {
x == ':' && !in_arbitrary
}
})
.into_iter()
.collect::<Vec<_>>();
let utility = match parts.pop() {

View file

@ -0,0 +1,159 @@
use std::{ascii::escape_default, fmt::Display};
#[derive(Debug, Clone)]
pub struct Cursor<'a> {
// The input we're scanning
pub input: &'a [u8],
// The location of the cursor in the input
pub pos: usize,
/// Is the cursor at the start of the input
pub at_start: bool,
/// Is the cursor at the end of the input
pub at_end: bool,
/// The previously consumed character
/// If `at_start` is true, this will be NUL
pub prev: u8,
/// The current character
pub curr: u8,
/// The upcoming character (if any)
/// If `at_end` is true, this will be NUL
pub next: u8,
}
impl<'a> Cursor<'a> {
pub fn new(input: &'a [u8]) -> Self {
let mut cursor = Self {
input,
pos: 0,
at_start: true,
at_end: false,
prev: 0x00,
curr: 0x00,
next: 0x00,
};
cursor.move_to(0);
cursor
}
pub fn rewind_by(&mut self, amount: usize) {
self.move_to(self.pos.saturating_sub(amount));
}
pub fn advance_by(&mut self, amount: usize) {
self.move_to(self.pos.saturating_add(amount));
}
pub fn move_to(&mut self, pos: usize) {
let len = self.input.len();
let pos = pos.clamp(0, len);
self.pos = pos;
self.at_start = pos == 0;
self.at_end = pos + 1 >= len;
self.prev = if pos > 0 { self.input[pos - 1] } else { 0x00 };
self.curr = if pos < len { self.input[pos] } else { 0x00 };
self.next = if pos + 1 < len {
self.input[pos + 1]
} else {
0x00
};
}
}
impl<'a> Display for Cursor<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let len = self.input.len().to_string();
let pos = format!("{: >len_count$}", self.pos, len_count = len.len());
write!(f, "{}/{} ", pos, len)?;
if self.at_start {
write!(f, "S ")?;
} else if self.at_end {
write!(f, "E ")?;
} else {
write!(f, "M ")?;
}
fn to_str(c: u8) -> String {
if c == 0x00 {
"NUL".into()
} else {
format!("{:?}", escape_default(c).to_string())
}
}
write!(
f,
"[{} {} {}]",
to_str(self.prev),
to_str(self.curr),
to_str(self.next)
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_cursor() {
let mut cursor = Cursor::new(b"hello world");
assert_eq!(cursor.pos, 0);
assert!(cursor.at_start);
assert!(!cursor.at_end);
assert_eq!(cursor.prev, 0x00);
assert_eq!(cursor.curr, b'h');
assert_eq!(cursor.next, b'e');
cursor.advance_by(1);
assert_eq!(cursor.pos, 1);
assert!(!cursor.at_start);
assert!(!cursor.at_end);
assert_eq!(cursor.prev, b'h');
assert_eq!(cursor.curr, b'e');
assert_eq!(cursor.next, b'l');
// Advancing too far should stop at the end
cursor.advance_by(10);
assert_eq!(cursor.pos, 11);
assert!(!cursor.at_start);
assert!(cursor.at_end);
assert_eq!(cursor.prev, b'd');
assert_eq!(cursor.curr, 0x00);
assert_eq!(cursor.next, 0x00);
// Can't advance past the end
cursor.advance_by(1);
assert_eq!(cursor.pos, 11);
assert!(!cursor.at_start);
assert!(cursor.at_end);
assert_eq!(cursor.prev, b'd');
assert_eq!(cursor.curr, 0x00);
assert_eq!(cursor.next, 0x00);
cursor.rewind_by(1);
assert_eq!(cursor.pos, 10);
assert!(!cursor.at_start);
assert!(cursor.at_end);
assert_eq!(cursor.prev, b'l');
assert_eq!(cursor.curr, b'd');
assert_eq!(cursor.next, 0x00);
cursor.rewind_by(10);
assert_eq!(cursor.pos, 0);
assert!(cursor.at_start);
assert!(!cursor.at_end);
assert_eq!(cursor.prev, 0x00);
assert_eq!(cursor.curr, b'h');
assert_eq!(cursor.next, b'e');
}
}

View file

@ -0,0 +1,89 @@
use crate::cursor::Cursor;
const STRIDE: usize = 16;
type Mask = [bool; STRIDE];
#[inline(always)]
pub fn fast_skip(cursor: &Cursor) -> Option<usize> {
// If we don't have enough bytes left to check then bail early
if cursor.pos + STRIDE >= cursor.input.len() {
return None;
}
if !cursor.curr.is_ascii_whitespace() {
return None;
}
let mut offset = 1;
// SAFETY: We've already checked (indirectly) that this index is valid
let remaining = unsafe { cursor.input.get_unchecked(cursor.pos..) };
// NOTE: This loop uses primitives designed to be auto-vectorized
// Do not change this loop without benchmarking the results
// And checking the generated assembly using godbolt.org
for (i, chunk) in remaining.chunks_exact(STRIDE).enumerate() {
let value = load(chunk);
let is_whitespace = is_ascii_whitespace(value);
let is_all_whitespace = all_true(is_whitespace);
if is_all_whitespace {
offset = (i + 1) * STRIDE;
} else {
break;
}
}
Some(cursor.pos + offset)
}
#[inline(always)]
fn load(input: &[u8]) -> [u8; STRIDE] {
let mut value = [0u8; STRIDE];
value.copy_from_slice(input);
value
}
#[inline(always)]
fn eq(input: [u8; STRIDE], val: u8) -> Mask {
let mut res = [false; STRIDE];
for n in 0..STRIDE {
res[n] = input[n] == val
}
res
}
#[inline(always)]
fn or(a: [bool; STRIDE], b: [bool; STRIDE]) -> [bool; STRIDE] {
let mut res = [false; STRIDE];
for n in 0..STRIDE {
res[n] = a[n] | b[n];
}
res
}
#[inline(always)]
fn all_true(a: [bool; STRIDE]) -> bool {
let mut res = true;
for item in a.iter().take(STRIDE) {
res &= item;
}
res
}
#[inline(always)]
fn is_ascii_whitespace(value: [u8; STRIDE]) -> [bool; STRIDE] {
let whitespace_1 = eq(value, b'\t');
let whitespace_2 = eq(value, b'\n');
let whitespace_3 = eq(value, b'\x0C');
let whitespace_4 = eq(value, b'\r');
let whitespace_5 = eq(value, b' ');
or(
or(
or(or(whitespace_1, whitespace_2), whitespace_3),
whitespace_4,
),
whitespace_5,
)
}

View file

@ -0,0 +1,259 @@
3dm
3ds
3g2
3gp
7z
DS_Store
a
aac
adp
ai
aif
aiff
alz
ape
apk
appimage
ar
arj
asf
au
avi
bak
baml
bh
bin
bk
bmp
btif
bz2
bzip2
cab
caf
cgm
class
cmx
cpio
cr2
cur
dat
dcm
deb
dex
djvu
dll
dmg
dng
doc
docm
docx
dot
dotm
dra
dsk
dts
dtshd
dvb
dwg
dxf
ecelp4800
ecelp7470
ecelp9600
egg
eol
eot
epub
exe
f4v
fbs
fh
fla
flac
flatpak
fli
flv
fpx
fst
fvt
g3
gh
gif
graffle
gz
gzip
h261
h263
h264
icns
ico
ief
img
ipa
iso
jar
jpeg
jpg
jpgv
jpm
jxr
key
ktx
lha
lib
lockb
lvp
lz
lzh
lzma
lzo
m3u
m4a
m4v
mar
mdi
mht
mid
midi
mj2
mka
mkv
mmr
mng
mobi
mov
movie
mp3
mp4
mp4a
mpeg
mpg
mpga
mxu
nef
npx
numbers
nupkg
o
odp
ods
odt
oga
ogg
ogv
otf
ott
pages
pbm
pcx
pdb
pdf
pea
pgm
pic
png
pnm
pot
potm
potx
ppa
ppam
ppm
pps
ppsm
ppsx
ppt
pptm
pptx
psd
pya
pyc
pyo
pyv
qt
rar
ras
raw
resources
rgb
rip
rlc
rmf
rmvb
rpm
rtf
rz
s3m
s7z
scpt
sgi
shar
sil
sketch
slk
smv
snap
snk
so
stl
sub
suo
swf
tar
tbz
tbz2
tga
tgz
thmx
tif
tiff
tlz
ttc
ttf
txz
udf
uvh
uvi
uvm
uvp
uvs
uvu
viv
vob
war
wav
wax
wbmp
wdp
weba
webm
webp
whl
wim
wm
wma
wmv
wmx
woff
woff2
wrm
wvx
xbm
xif
xla
xlam
xls
xlsb
xlsm
xlsx
xlt
xltm
xltx
xm
xmind
xpi
xpm
xwd
xz
z
zip
zipx

View file

@ -0,0 +1,6 @@
css
less
lock
sass
scss
styl

View file

@ -0,0 +1,3 @@
package-lock.json
pnpm-lock.yaml
bun.lockb

View file

@ -0,0 +1,57 @@
# HTML
html
pug
# JS
astro
cjs
cts
jade
js
jsx
mjs
mts
svelte
ts
tsx
vue
# Markdown
md
mdx
# ASP
aspx
razor
# Handlebars
handlebars
hbs
mustache
# PHP
php
twig
# Ruby
erb
haml
liquid
rb
rhtml
slim
# Elixir / Phoenix
eex
heex
# Nunjucks
njk
nunjucks
# Python
py
tpl
# Rust
rs

View file

@ -12,7 +12,6 @@ pub fn fast_glob(
.follow_links(true)
.build()
.unwrap()
.into_iter()
.filter_map(Result::ok)
.map(|file| file.path().to_path_buf())
}))

View file

@ -1,9 +1,16 @@
use crate::parser::Extractor;
use fxhash::FxHashSet;
use ignore::WalkBuilder;
use rayon::prelude::*;
use std::cmp::Ordering;
use std::path::Path;
use std::path::PathBuf;
use tracing::event;
use walkdir::WalkDir;
pub mod candidate;
pub mod cursor;
pub mod fast_skip;
pub mod glob;
pub mod location;
pub mod modifier;
@ -11,14 +18,7 @@ pub mod parser;
pub mod utility;
pub mod variant;
#[derive(Debug, Clone)]
pub struct ChangedContent {
pub file: Option<PathBuf>,
pub content: Option<String>,
pub extension: String,
}
pub fn parse_candidate_strings_from_files(changed_content: Vec<ChangedContent>) -> Vec<String> {
fn init_tracing() {
if matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind"))
{
tracing_subscriber::fmt()
@ -27,8 +27,325 @@ pub fn parse_candidate_strings_from_files(changed_content: Vec<ChangedContent>)
.compact()
.init();
}
}
parse_all_blobs(read_all_files(changed_content))
#[derive(Debug, Clone)]
pub struct ChangedContent {
pub file: Option<PathBuf>,
pub content: Option<String>,
pub extension: String,
}
#[derive(Debug, Clone)]
pub struct ContentPathInfo {
pub base: String,
}
pub fn resolve_content_paths(args: ContentPathInfo) -> Vec<String> {
let root = Path::new(&args.base);
let allowed_paths = FxHashSet::from_iter(
WalkBuilder::new(root)
.hidden(false)
.filter_entry(|entry| match entry.file_type() {
Some(file_type) if file_type.is_dir() => entry
.file_name()
.to_str()
.map(|s| s != ".git")
.unwrap_or(false),
_ => true,
})
.build()
.filter_map(Result::ok)
.map(|x| x.into_path()),
);
// A list of directory names where we can't use globs, but we should track each file
// individually instead. This is because these directories are often used for both source and
// destination files.
let mut forced_static_directories = vec![root.join("public")];
// A list of known extensions + a list of extensions we found in the project.
let mut found_extensions = FxHashSet::from_iter(
include_str!("fixtures/template-extensions.txt")
.trim()
.lines()
.filter(|x| !x.starts_with('#')) // Drop commented lines
.filter(|x| !x.is_empty()) // Drop empty lines
.map(|x| x.to_string()),
);
// A list of static files that we should track directly.
let mut static_files = vec![];
// All root directories.
let mut root_directories = FxHashSet::from_iter(vec![root.to_path_buf()]);
// All directories where we can safely use deeply nested globs to watch all files.
// In other comments we refer to these as "deep glob directories" or similar.
//
// E.g.: ./src/**/*.{html,js}
let mut deep_globable_directories: FxHashSet<PathBuf> = FxHashSet::default();
// All directories where we can only use shallow globs to watch all direct files but not
// folders.
// In other comments we refer to these as "shallow glob directories" or similar.
//
// E.g.: ./src/*/*.{html,js}
let mut shallow_globable_directories: FxHashSet<PathBuf> = FxHashSet::default();
// Collect all valid paths from the root. This will already filter out ignored files, unknown
// extensions and binary files.
let mut it = WalkDir::new(root)
// Sorting to make sure that we always see the directories before the files. Also sorting
// alphabetically by default.
.sort_by(
|a, z| match (a.file_type().is_dir(), z.file_type().is_dir()) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => a.file_name().cmp(z.file_name()),
},
)
.into_iter();
loop {
// We are only interested in valid entries
let entry = match it.next() {
Some(Ok(entry)) => entry,
_ => break,
};
// Ignore known directories that we don't want to traverse into.
if entry.file_type().is_dir() && entry.file_name() == ".git" {
it.skip_current_dir();
continue;
}
if entry.file_type().is_dir() {
// If we are in a directory where we know that we can't use any globs, then we have to
// track each file individually.
if forced_static_directories.contains(&entry.path().to_path_buf()) {
forced_static_directories.push(entry.path().to_path_buf());
root_directories.insert(entry.path().to_path_buf());
continue;
}
// If we are in a directory where the parent is a forced static directory, then this
// will become a forced static directory as well.
if forced_static_directories.contains(&entry.path().parent().unwrap().to_path_buf()) {
forced_static_directories.push(entry.path().to_path_buf());
root_directories.insert(entry.path().to_path_buf());
continue;
}
// If we are in a directory, and the directory is git ignored, then we don't have to
// descent into the directory. However, we have to make sure that we mark the _parent_
// directory as a shallow glob directory because using deep globs from any of the
// parent directories will include this ignored directory which should not be the case.
//
// Another important part is that if one of the ignored directories is a deep glob
// directory, then all of its parents (until the root) should be marked as shallow glob
// directories as well.
if !allowed_paths.contains(&entry.path().to_path_buf()) {
let mut parent = entry.path().parent();
while let Some(parent_path) = parent {
// If the parent is already marked as a valid deep glob directory, then we have
// to mark it as a shallow glob directory instead, because we won't be able to
// use deep globs for this directory anymore.
if deep_globable_directories.contains(parent_path) {
deep_globable_directories.remove(parent_path);
shallow_globable_directories.insert(parent_path.to_path_buf());
}
// If we reached the root, then we can stop.
if parent_path == root {
break;
}
// Mark the parent directory as a shallow glob directory and continue with its
// parent.
shallow_globable_directories.insert(parent_path.to_path_buf());
parent = parent_path.parent();
}
it.skip_current_dir();
continue;
}
// If we are in a directory that is not git ignored, then we can mark this directory as
// a valid deep glob directory. This is only necessary if any of its parents aren't
// marked as deep glob directories already.
else {
let mut found_deep_glob_parent = false;
let mut parent = entry.path().parent();
while let Some(parent_path) = parent {
// If we reached the root, then we can stop.
if parent_path == root {
break;
}
// If the parent is already marked as a deep glob directory, then we can stop
// because this glob will match the current directory already.
if deep_globable_directories.contains(parent_path) {
found_deep_glob_parent = true;
break;
}
parent = parent_path.parent();
}
// If we didn't find a deep glob directory parent, then we can mark this directory
// as a deep glob directory (unless it is the root).
if !found_deep_glob_parent && entry.path() != root {
deep_globable_directories.insert(entry.path().to_path_buf());
}
}
}
// Handle allowed content paths
if is_allowed_content_path(entry.path())
&& allowed_paths.contains(&entry.path().to_path_buf())
{
let path = entry.path();
// Collect the extension for future use when building globs.
if let Some(extension) = path.extension().and_then(|x| x.to_str()) {
found_extensions.insert(extension.to_string());
}
// If the current path is a file inside any of the root directories, then we have to
// track it directly as a static file.
if let Some(parent_path) = path.parent() {
if root_directories.contains(parent_path) {
static_files.push(format!("{}", path.display()));
}
}
}
}
// Build the globs for all globable directories.
let shallow_globs = shallow_globable_directories
.iter()
.map(|path| format!("{}", path.display()));
let deep_globs = deep_globable_directories
.iter()
.map(|path| format!("{}/**", path.display()));
let globs = shallow_globs
.chain(deep_globs)
.flat_map(|path| match found_extensions.len() {
0 => None, // This should never happen
1 => Some(format!(
"{}/*.{}",
path,
found_extensions.iter().next().unwrap()
)),
_ => Some(format!(
"{}/*.{{{}}}",
path,
found_extensions
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(","),
)),
})
.collect::<Vec<_>>();
static_files.extend(globs);
static_files
}
pub fn is_git_ignored_content_path(base: &Path, path: &Path) -> bool {
!WalkBuilder::new(base)
.hidden(false)
.filter_entry(|entry| match entry.file_type() {
Some(file_type) if file_type.is_dir() => entry
.file_name()
.to_str()
.map(|s| s != ".git")
.unwrap_or(false),
_ => true,
})
.build()
.filter_map(Result::ok)
.any(|e| e.path() == path)
}
pub fn is_allowed_content_path(path: &Path) -> bool {
let binary_extensions = include_str!("fixtures/binary-extensions.txt")
.trim()
.lines()
.collect::<Vec<_>>();
let ignored_extensions = include_str!("fixtures/ignored-extensions.txt")
.trim()
.lines()
.collect::<Vec<_>>();
let ignored_files = include_str!("fixtures/ignored-files.txt")
.trim()
.lines()
.collect::<Vec<_>>();
let path = PathBuf::from(path);
// Skip known ignored files
if path
.file_name()
.unwrap()
.to_str()
.map(|s| ignored_files.contains(&s))
.unwrap_or(false)
{
return false;
}
// Skip known ignored extensions
return path
.extension()
.map(|s| s.to_str().unwrap_or_default())
.map(|ext| !ignored_extensions.contains(&ext) && !binary_extensions.contains(&ext))
.unwrap_or(false);
}
#[derive(Debug)]
pub enum IO {
Sequential = 0b0001,
Parallel = 0b0010,
}
impl From<u8> for IO {
fn from(item: u8) -> Self {
match item & 0b0011 {
0b0001 => IO::Sequential,
0b0010 => IO::Parallel,
_ => unimplemented!("Unknown 'IO' strategy"),
}
}
}
#[derive(Debug)]
pub enum Parsing {
Sequential = 0b0100,
Parallel = 0b1000,
}
impl From<u8> for Parsing {
fn from(item: u8) -> Self {
match item & 0b1100 {
0b0100 => Parsing::Sequential,
0b1000 => Parsing::Parallel,
_ => unimplemented!("Unknown 'Parsing' strategy"),
}
}
}
pub fn parse_candidate_strings(input: Vec<ChangedContent>, options: u8) -> Vec<String> {
init_tracing();
match (IO::from(options), Parsing::from(options)) {
(IO::Sequential, Parsing::Sequential) => parse_all_blobs_sync(read_all_files_sync(input)),
(IO::Sequential, Parsing::Parallel) => parse_all_blobs_sync(read_all_files(input)),
(IO::Parallel, Parsing::Sequential) => parse_all_blobs(read_all_files_sync(input)),
(IO::Parallel, Parsing::Parallel) => parse_all_blobs(read_all_files(input)),
}
}
#[tracing::instrument(skip(changed_content))]
@ -41,6 +358,30 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
changed_content
.into_par_iter()
.map(|c| match (c.file, c.content) {
(Some(file), None) => match std::fs::read(file) {
Ok(content) => content,
Err(e) => {
event!(tracing::Level::ERROR, "Failed to read file: {:?}", e);
Default::default()
}
},
(None, Some(content)) => content.into_bytes(),
_ => Default::default(),
})
.collect()
}
#[tracing::instrument(skip(changed_content))]
fn read_all_files_sync(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
event!(
tracing::Level::INFO,
"Reading {:?} file(s)",
changed_content.len()
);
changed_content
.into_iter()
.map(|c| match (c.file, c.content) {
(Some(file), None) => std::fs::read(file).unwrap(),
(None, Some(content)) => content.into_bytes(),
@ -72,3 +413,27 @@ fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
result.sort();
result
}
#[tracing::instrument(skip(blobs))]
fn parse_all_blobs_sync(blobs: Vec<Vec<u8>>) -> Vec<String> {
let input: Vec<_> = blobs.iter().map(|blob| &blob[..]).collect();
let input = &input[..];
let mut result: Vec<String> = input
.iter()
.map(|input| Extractor::unique(input, Default::default()))
.fold(FxHashSet::default(), |mut a, b| {
a.extend(b);
a
})
.into_iter()
.map(|s| {
// SAFETY: When we parsed the candidates, we already guaranteed that the byte slices
// are valid, therefore we don't have to re-check here when we want to convert it back
// to a string.
unsafe { String::from_utf8_unchecked(s.to_vec()) }
})
.collect();
result.sort();
result
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,248 @@
#[cfg(test)]
mod auto_content {
use std::fs;
use std::process::Command;
use tailwindcss_core::*;
use tempfile::tempdir;
fn test(paths_with_content: &[(&str, Option<&str>)]) -> Vec<String> {
// Create a temporary working directory
let dir = tempdir().unwrap().into_path();
// Initialize this directory as a git repository
let _ = Command::new("git").arg("init").current_dir(&dir).output();
// Create the necessary files
for (path, contents) in paths_with_content {
let path = dir.join(path);
let parent = path.parent().unwrap();
if !parent.exists() {
fs::create_dir_all(parent).unwrap();
}
match contents {
Some(contents) => fs::write(path, contents).unwrap(),
None => fs::write(path, "").unwrap(),
}
}
let base = format!("{}", dir.display());
// Resolve all content paths for the (temporary) current working directory
let mut paths: Vec<_> = resolve_content_paths(ContentPathInfo { base: base.clone() })
.into_iter()
.map(|x| x.replace(&format!("{}/", &base), ""))
.collect();
// Sort the output for easier comparison (depending on internal datastructure the order
// _could_ be random)
paths.sort();
paths
}
#[test]
fn it_should_work_with_a_set_of_root_files() {
let globs = test(&[
("index.html", None),
("a.html", None),
("b.html", None),
("c.html", None),
]);
assert_eq!(globs, vec!["a.html", "b.html", "c.html", "index.html"]);
}
#[test]
fn it_should_work_with_a_set_of_root_files_and_ignore_ignored_files() {
let globs = test(&[
(".gitignore", Some("b.html")),
("index.html", None),
("a.html", None),
("b.html", None),
("c.html", None),
]);
assert_eq!(globs, vec!["a.html", "c.html", "index.html"]);
}
#[test]
fn it_should_list_all_files_in_the_public_folder_explicitly() {
let globs = test(&[
("index.html", None),
("public/a.html", None),
("public/b.html", None),
("public/c.html", None),
]);
assert_eq!(
globs,
vec![
"index.html",
"public/a.html",
"public/b.html",
"public/c.html",
]
);
}
#[test]
fn it_should_list_nested_folders_explicitly_in_the_public_folder() {
let globs = test(&[
("index.html", None),
("public/a.html", None),
("public/b.html", None),
("public/c.html", None),
("public/nested/a.html", None),
("public/nested/b.html", None),
("public/nested/c.html", None),
("public/nested/again/a.html", None),
("public/very/deeply/nested/a.html", None),
]);
assert_eq!(
globs,
vec![
"index.html",
"public/a.html",
"public/b.html",
"public/c.html",
"public/nested/a.html",
"public/nested/again/a.html",
"public/nested/b.html",
"public/nested/c.html",
"public/very/deeply/nested/a.html",
]
);
}
#[test]
fn it_should_list_all_files_in_the_public_folder_explicitly_except_ignored_files() {
let globs = test(&[
(".gitignore", Some("public/b.html\na.html")),
("index.html", None),
("public/a.html", None),
("public/b.html", None),
("public/c.html", None),
]);
assert_eq!(globs, vec!["index.html", "public/c.html",]);
}
#[test]
fn it_should_use_a_glob_for_top_level_folders() {
let globs = test(&[
("index.html", None),
("src/a.html", None),
("src/b.html", None),
("src/c.html", None),
]);
assert_eq!(globs, vec!["index.html", "src/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}"]);
}
#[test]
fn it_should_ignore_binary_files() {
let globs = test(&[
("index.html", None),
("a.mp4", None),
("b.png", None),
("c.lock", None),
]);
assert_eq!(globs, vec!["index.html"]);
}
#[test]
fn it_should_ignore_known_extensions() {
let globs = test(&[
("index.html", None),
("a.css", None),
("b.sass", None),
("c.less", None),
]);
assert_eq!(globs, vec!["index.html"]);
}
#[test]
fn it_should_ignore_known_files() {
let globs = test(&[
("index.html", None),
("package-lock.json", None),
("yarn.lock", None),
]);
assert_eq!(globs, vec!["index.html"]);
}
#[test]
fn it_should_ignore_and_expand_nested_ignored_folders() {
let globs = test(&[
// Explicitly listed root files
("foo.html", None),
("bar.html", None),
("baz.html", None),
// Nested folder A, using glob
("nested-a/foo.html", None),
("nested-a/bar.html", None),
("nested-a/baz.html", None),
// Nested folder B, with deeply nested files, using glob
("nested-b/deeply-nested/foo.html", None),
("nested-b/deeply-nested/bar.html", None),
("nested-b/deeply-nested/baz.html", None),
// Nested folder C, with ignored sub-folder
("nested-c/foo.html", None),
("nested-c/bar.html", None),
("nested-c/baz.html", None),
// Ignored folder
("nested-c/.gitignore", Some("ignored-folder/")),
("nested-c/ignored-folder/foo.html", None),
("nested-c/ignored-folder/bar.html", None),
("nested-c/ignored-folder/baz.html", None),
// Deeply nested, without issues
("nested-c/sibling-folder/foo.html", None),
("nested-c/sibling-folder/bar.html", None),
("nested-c/sibling-folder/baz.html", None),
// Nested folder D, with deeply nested ignored folder
("nested-d/foo.html", None),
("nested-d/bar.html", None),
("nested-d/baz.html", None),
("nested-d/.gitignore", Some("deep/")),
("nested-d/very/deeply/nested/deep/foo.html", None),
("nested-d/very/deeply/nested/deep/bar.html", None),
("nested-d/very/deeply/nested/deep/baz.html", None),
("nested-d/very/deeply/nested/foo.html", None),
("nested-d/very/deeply/nested/bar.html", None),
("nested-d/very/deeply/nested/baz.html", None),
("nested-d/very/deeply/nested/directory/foo.html", None),
("nested-d/very/deeply/nested/directory/bar.html", None),
("nested-d/very/deeply/nested/directory/baz.html", None),
("nested-d/very/deeply/nested/directory/again/foo.html", None),
]);
assert_eq!(
globs,
vec![
// Listed explicitly because they are in the root
"bar.html",
"baz.html",
"foo.html",
// Listed using a deep glob, because nothing inside is ignored
"nested-a/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
// Listed using a deep glob, because nothing inside is ignored (but contains deeply
// nested folders)
"nested-b/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
// Listed using a shallow glob, because `nested-c` contains ignored folders, deeply
// nested globs can't be used anymore
"nested-c/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
// Deeply nested folder can use a deep glob again
"nested-c/sibling-folder/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
// The deeply nested folder can use a deep glob, however all of its parents can
// only contain a shallow glob because of the ignored folder
"nested-d/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
"nested-d/very/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
"nested-d/very/deeply/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
"nested-d/very/deeply/nested/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
"nested-d/very/deeply/nested/directory/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
]
);
}
}

View file

@ -8,8 +8,8 @@ crate-type = ["cdylib"]
[dependencies]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.10.0", default-features = false, features = ["napi4"] }
napi-derive = "2.11.1"
napi = { version = "2.13.1", default-features = false, features = ["napi4"] }
napi-derive = "2.13.0"
tailwindcss-core = { path = "../core" }
rayon = "1.5.3"

View file

@ -19,7 +19,7 @@
},
"license": "MIT",
"devDependencies": {
"@napi-rs/cli": "^2.15.2"
"@napi-rs/cli": "^2.16.1"
},
"engines": {
"node": ">= 10"

View file

@ -1,3 +1,4 @@
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use std::path::PathBuf;
#[macro_use]
@ -11,16 +12,42 @@ pub struct ChangedContent {
pub extension: String,
}
#[napi]
pub fn parse_candidate_strings_from_files(changed_content: Vec<ChangedContent>) -> Vec<String> {
tailwindcss_core::parse_candidate_strings_from_files(
changed_content
.into_iter()
.map(|changed_content| tailwindcss_core::ChangedContent {
file: changed_content.file.map(PathBuf::from),
content: changed_content.content,
extension: changed_content.extension,
})
.collect(),
)
impl From<ChangedContent> for tailwindcss_core::ChangedContent {
fn from(changed_content: ChangedContent) -> Self {
tailwindcss_core::ChangedContent {
file: changed_content.file.map(PathBuf::from),
content: changed_content.content,
extension: changed_content.extension,
}
}
}
#[derive(Debug, Clone)]
#[napi(object)]
pub struct ContentPathInfo {
pub base: String,
}
#[napi]
pub fn resolve_content_paths(args: ContentPathInfo) -> Vec<String> {
tailwindcss_core::resolve_content_paths(tailwindcss_core::ContentPathInfo { base: args.base })
}
#[derive(Debug)]
#[napi]
pub enum IO {
Sequential = 0b0001,
Parallel = 0b0010,
}
#[derive(Debug)]
#[napi]
pub enum Parsing {
Sequential = 0b0100,
Parallel = 0b1000,
}
#[napi]
pub fn parse_candidate_strings(input: Vec<ChangedContent>, strategy: u8) -> Vec<String> {
tailwindcss_core::parse_candidate_strings(input.into_iter().map(Into::into).collect(), strategy)
}

5049
package-lock.json generated

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "tailwindcss",
"version": "3.3.2",
"version": "0.0.0-experimental.4",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"main": "lib/index.js",
@ -16,12 +16,9 @@
"integrations/*",
"oxide/crates/node"
],
"tailwindcss": {
"engine": "oxide"
},
"scripts": {
"dev": "concurrently -n tailwind,oxide-node -c green,yellow 'npm run dev:js' 'npm run dev:rust'",
"build": "npm run build:rust && npm run build:js && npm run build:peers",
"build": "npm run build:rust && npm run build:js",
"test": "jest",
"test:integrations": "npm run test -w ./integrations",
"style": "eslint .",
@ -29,8 +26,7 @@
"dev:rust": "npm run --prefix ./oxide dev:node",
"dev:js": "npm run build:js -- --watch",
"build:rust": "npm run --prefix ./oxide build:node",
"build:js": "npm run generate:plugin-list && swc src --out-dir lib --copy-files --delete-dir-on-start --config jsc.transform.optimizer.globals.vars.__OXIDE__='\"true\"'",
"build:peers": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
"build:js": "npm run generate:plugin-list && swc src --out-dir lib --copy-files --delete-dir-on-start",
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
"generate:types": "node -r @swc/register scripts/generate-types.js"
},
@ -38,7 +34,6 @@
"src/*",
"cli/*",
"lib/*",
"peers/*",
"scripts/*.js",
"stubs/*",
"nesting/*",
@ -49,14 +44,11 @@
],
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.55",
"@swc/core": "^1.3.62",
"@swc/jest": "^0.2.26",
"@swc/register": "^0.1.10",
"autoprefixer": "^10.4.14",
"concurrently": "^8.0.1",
"cssnano": "^6.0.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
@ -68,9 +60,9 @@
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/oxide": "file:oxide/crates/node",
"@tailwindcss/oxide": "0.0.0-insiders.776d5bf",
"arg": "^5.0.2",
"browserslist": "^4.21.5",
"browserslist": "^4.21.7",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
@ -78,27 +70,25 @@
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"lightningcss": "^1.18.0",
"lightningcss": "^1.21.0",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss": "^8.4.24",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"postcss-selector-parser": "^6.0.12",
"postcss-value-parser": "^4.2.0",
"resolve": "^1.22.2",
"resolve": "^1.22.3",
"sucrase": "^3.32.0"
},
"browserslist": [
"> 1%",
"not edge <= 18",
"not ie 11",
"not op_mini all"
"defaults and supports css-variables and supports css-matches-pseudo",
"safari >= 14"
],
"jest": {
"testTimeout": 30000,

View file

@ -1,122 +0,0 @@
{
"name": "tailwindcss",
"version": "3.3.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
"license": "MIT",
"main": "lib/index.js",
"types": "types/index.d.ts",
"repository": "https://github.com/tailwindlabs/tailwindcss.git",
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
"homepage": "https://tailwindcss.com",
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"tailwindcss": {
"engine": "stable"
},
"scripts": {
"prebuild": "npm run generate && rimraf lib",
"build": "swc src --out-dir lib --copy-files --config jsc.transform.optimizer.globals.vars.__OXIDE__='\"false\"'",
"postbuild": "esbuild lib/cli-peer-dependencies.js --bundle --platform=node --outfile=peers/index.js --define:process.env.CSS_TRANSFORMER_WASM=false",
"rebuild-fixtures": "npm run build && node -r @swc/register scripts/rebuildFixtures.js",
"style": "eslint .",
"pretest": "npm run generate",
"test": "jest",
"test:integrations": "npm run test --prefix ./integrations",
"install:integrations": "node scripts/install-integrations.js",
"generate:plugin-list": "node -r @swc/register scripts/create-plugin-list.js",
"generate:types": "node -r @swc/register scripts/generate-types.js",
"generate": "npm run generate:plugin-list && npm run generate:types",
"release-channel": "node ./scripts/release-channel.js",
"release-notes": "node ./scripts/release-notes.js",
"prepublishOnly": "npm install --force && npm run build"
},
"files": [
"src/*",
"cli/*",
"lib/*",
"peers/*",
"scripts/*.js",
"stubs/*",
"nesting/*",
"types/**/*",
"*.d.ts",
"*.css",
"*.js"
],
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.55",
"@swc/jest": "^0.2.26",
"@swc/register": "^0.1.10",
"autoprefixer": "^10.4.14",
"browserslist": "^4.21.5",
"concurrently": "^8.0.1",
"cssnano": "^6.0.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
"jest-diff": "^29.5.0",
"lightningcss": "1.18.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.0",
"source-map-js": "^1.0.2",
"turbo": "^1.9.3"
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.23",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11",
"postcss-value-parser": "^4.2.0",
"resolve": "^1.22.2",
"sucrase": "^3.32.0"
},
"browserslist": [
"> 1%",
"not edge <= 18",
"not ie 11",
"not op_mini all"
],
"jest": {
"testTimeout": 30000,
"setupFilesAfterEnv": [
"<rootDir>/jest/customMatchers.js"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/integrations/",
"/standalone-cli/",
"\\.test\\.skip\\.js$"
],
"transformIgnorePatterns": [
"node_modules/(?!lightningcss)"
],
"transform": {
"\\.js$": "@swc/jest",
"\\.ts$": "@swc/jest"
}
},
"engines": {
"node": ">=14.0.0"
}
}

View file

@ -1,40 +0,0 @@
let fs = require('fs')
let path = require('path')
let engines = {
stable: {
files: [
path.resolve(__dirname, '..', 'package.stable.json'),
path.resolve(__dirname, '..', 'package-lock.stable.json'),
],
},
oxide: {
files: [
path.resolve(__dirname, '..', 'package.oxide.json'),
path.resolve(__dirname, '..', 'package-lock.oxide.json'),
],
},
}
// Find out what the current engine is that we are using:
let [otherEngine, info] = Object.entries(engines).find(([, info]) =>
info.files.every((file) => fs.existsSync(file))
)
let currentEngine = otherEngine === 'oxide' ? 'stable' : 'oxide'
console.log(`Current engine: \`${currentEngine}\`, swapping to \`${otherEngine}\``)
// Swap the engines
for (let file of info.files) {
fs.renameSync(
file.replace(`.${otherEngine}`, ''),
file.replace(`.${otherEngine}`, `.${currentEngine}`)
)
}
for (let file of engines[otherEngine].files) {
fs.renameSync(file, file.replace(`.${otherEngine}`, ''))
}
console.log(
'Engines have been swapped. Make sure to run `npm install` to update your dependencies.'
)

View file

@ -1,15 +0,0 @@
export function lazyPostcss() {
return require('postcss')
}
export function lazyPostcssImport() {
return require('postcss-import')
}
export function lazyAutoprefixer() {
return require('autoprefixer')
}
export function lazyCssnano() {
return require('cssnano')
}

View file

@ -1,7 +1,3 @@
#!/usr/bin/env node
if (__OXIDE__) {
module.exports = require('./oxide/cli')
} else {
module.exports = require('./cli/index')
}
module.exports = require('./cli/index')

View file

@ -1,56 +0,0 @@
// @ts-check
import {
// @ts-ignore
lazyPostcss,
// @ts-ignore
lazyPostcssImport,
// @ts-ignore
lazyCssnano,
// @ts-ignore
lazyAutoprefixer,
} from '../../../peers/index.js'
/**
* @returns {import('postcss')}
*/
export function loadPostcss() {
// Try to load a local `postcss` version first
try {
return require('postcss')
} catch {}
return lazyPostcss()
}
export function loadPostcssImport() {
// Try to load a local `postcss-import` version first
try {
return require('postcss-import')
} catch {}
return lazyPostcssImport()
}
export function loadCssNano() {
let options = { preset: ['default', { cssDeclarationSorter: false }] }
// Try to load a local `cssnano` version first
try {
return require('cssnano')
} catch {}
return lazyCssnano()(options)
}
export function loadAutoprefixer() {
// Try to load a local `autoprefixer` version first
try {
return require('autoprefixer')
} catch {}
return lazyAutoprefixer()
}

View file

@ -25,6 +25,10 @@ export async function build(args) {
process.exit(9)
}
if (args['--no-autoprefixer']) {
console.error('[deprecation] The --no-autoprefixer flag is deprecated and has no effect.')
}
// TODO: Reference the @config path here if exists
let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath()

View file

@ -1,14 +1,17 @@
// @ts-check
import pkg from '../../../package.json'
import path from 'path'
import fs from 'fs'
import postcss from 'postcss'
import postcssrc from 'postcss-load-config'
import browserslist from 'browserslist'
import lightning, { Features } from 'lightningcss'
import { lilconfig } from 'lilconfig'
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
import tailwind from '../../processTailwindFeatures'
import { loadAutoprefixer, loadCssNano, loadPostcss, loadPostcssImport } from './deps'
import { formatNodes, drainStdin, outputFile } from './utils'
import { env } from '../../lib/sharedState'
import resolveConfig from '../../../resolveConfig.js'
@ -19,6 +22,50 @@ import { findAtConfigPath } from '../../lib/findAtConfigPath.js'
import log from '../../util/log'
import { loadConfig } from '../../lib/load-config'
import getModuleDependencies from '../../lib/getModuleDependencies'
import { validateConfig } from '../../util/validateConfig'
import { handleImportAtRules } from '../../lib/handleImportAtRules'
import { flagEnabled } from '../../featureFlags'
import { CssBasedConfig } from '../../lib/parseCssConfig.js'
async function lightningcss(result, { map = true, minify = true } = {}) {
try {
let resolvedBrowsersListConfig = browserslist.findConfig(
result.opts.from ?? process.cwd()
)?.defaults
let defaultBrowsersListConfig = pkg.browserslist
let browsersListConfig = resolvedBrowsersListConfig ?? defaultBrowsersListConfig
let transformed = lightning.transform({
filename: result.opts.from || 'input.css',
code: Buffer.from(result.css, 'utf-8'),
minify,
sourceMap: result.map === undefined ? map : !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets: lightning.browserslistToTargets(browserslist(browsersListConfig)),
drafts: {
nesting: true,
},
include: Features.Nesting,
exclude: Features.LogicalProperties,
})
return Object.assign(result, {
css: transformed.code.toString('utf8'),
map: result.map
? Object.assign(result.map, {
toString() {
return transformed.map?.toString()
},
})
: result.map,
})
} catch (err) {
console.error('Unable to use Lightning CSS. Using raw version instead.')
console.error(err)
return result
}
}
/**
*
@ -75,39 +122,6 @@ async function loadPostCssPlugins(customPostCssPath) {
return [beforePlugins, afterPlugins, config.options]
}
function loadBuiltinPostcssPlugins() {
let postcss = loadPostcss()
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
return [
[
(root) => {
root.walkAtRules('import', (rule) => {
if (rule.params.slice(1).startsWith('tailwindcss/')) {
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
rule.remove()
}
})
},
loadPostcssImport(),
(root) => {
root.walkComments((rule) => {
if (rule.text.startsWith(IMPORT_COMMENT)) {
rule.after(
postcss.atRule({
name: 'import',
params: rule.text.replace(IMPORT_COMMENT, ''),
})
)
rule.remove()
}
})
},
],
[],
{},
]
}
let state = {
/** @type {any} */
context: null,
@ -118,7 +132,7 @@ let state = {
/** @type {{content: string, extension: string}[]} */
changedContent: [],
/** @type {ReturnType<typeof load> | null} */
/** @type {{config: import('../../../types').Config, dependencies: Set<string>, dispose: Function } | null} */
configBag: null,
contextDependencies: new Set(),
@ -143,7 +157,7 @@ let state = {
}
},
loadConfig(configPath, content) {
loadConfig(configPath, content, cssThemeValues) {
if (this.watcher && configPath) {
this.refreshConfigDependencies()
}
@ -160,8 +174,7 @@ let state = {
},
}
// @ts-ignore
this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } })
this.configBag.config = validateConfig(resolveConfig(this.configBag.config, { theme: cssThemeValues }))
// Override content files if `--content` has been passed explicitly
if (content?.length > 0) {
@ -185,7 +198,7 @@ let state = {
let files = fastGlob.sync(this.contentPatterns.all)
for (let file of files) {
if (__OXIDE__) {
if (flagEnabled(this.config, 'oxideParser')) {
content.push({
file,
extension: path.extname(file).slice(1),
@ -210,7 +223,7 @@ let state = {
return content
},
getContext({ createContext, cliConfigPath, root, result, content }) {
getContext({ createContext, cliConfigPath, root, result, content, cssThemeValues }) {
if (this.context) {
this.context.changedContent = this.changedContent.splice(0)
@ -222,7 +235,7 @@ let state = {
env.DEBUG && console.timeEnd('Searching for config')
env.DEBUG && console.time('Loading config')
let config = this.loadConfig(configPath, content)
let config = this.loadConfig(configPath, content, cssThemeValues)
env.DEBUG && console.timeEnd('Loading config')
env.DEBUG && console.time('Creating context')
@ -251,8 +264,6 @@ let state = {
}
export async function createProcessor(args, cliConfigPath) {
let postcss = loadPostcss()
let input = args['--input']
let output = args['--output']
let includePostCss = args['--postcss']
@ -260,7 +271,9 @@ export async function createProcessor(args, cliConfigPath) {
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
? await loadPostCssPlugins(customPostCssPath)
: loadBuiltinPostcssPlugins()
: [[], [], {}]
beforePlugins.unshift(...handleImportAtRules())
if (args['--purge']) {
log.warn('purge-flag-deprecated', [
@ -279,6 +292,9 @@ export async function createProcessor(args, cliConfigPath) {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
// Parse any CSS-based config values from `:theme` rules
let cssThemeValues = new CssBasedConfig().parse(root)
env.DEBUG && console.time('Compiling CSS')
tailwind(({ createContext }) => {
console.error()
@ -291,6 +307,7 @@ export async function createProcessor(args, cliConfigPath) {
root,
result,
content,
cssThemeValues,
})
}
})(root, result)
@ -306,8 +323,6 @@ export async function createProcessor(args, cliConfigPath) {
tailwindPlugin,
!args['--minify'] && formatNodes,
...afterPlugins,
!args['--no-autoprefixer'] && loadAutoprefixer(),
args['--minify'] && loadCssNano(),
].filter(Boolean)
/** @type {import('postcss').Processor} */
@ -331,9 +346,20 @@ export async function createProcessor(args, cliConfigPath) {
async function build() {
let start = process.hrtime.bigint()
let options = {
...postcssOptions,
from: input,
to: output,
}
return readInput()
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
.then((css) => processor.process(css, options))
.then((result) =>
lightningcss(result, {
...options,
minify: !!args['--minify'],
})
)
.then((result) => {
if (!state.watcher) {
return result

View file

@ -70,6 +70,7 @@ let commands = {
description: 'Path to a custom config file',
},
'--no-autoprefixer': {
deprecated: true,
type: Boolean,
description: 'Disable autoprefixer',
},

View file

@ -386,6 +386,26 @@ export let variantPlugins = {
)
},
hasVariants: ({ matchVariant }) => {
matchVariant('has', (value) => `&:has(${normalize(value)})`, { values: {} })
matchVariant(
'group-has',
(value, { modifier }) =>
modifier
? `:merge(.group\\/${modifier}):has(${normalize(value)}) &`
: `:merge(.group):has(${normalize(value)}) &`,
{ values: {} }
)
matchVariant(
'peer-has',
(value, { modifier }) =>
modifier
? `:merge(.peer\\/${modifier}):has(${normalize(value)}) ~ &`
: `:merge(.peer):has(${normalize(value)}) ~ &`,
{ values: {} }
)
},
ariaVariants: ({ matchVariant, theme }) => {
matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} })
matchVariant(
@ -1255,13 +1275,13 @@ export let corePlugins = {
],
]),
space: ({ matchUtilities, addUtilities, theme }) => {
space: ({ matchUtilities, addUtilities, theme, config }) => {
matchUtilities(
{
'space-x': (value) => {
value = value === '0' ? '0px' : value
if (__OXIDE__) {
if (flagEnabled(config(), 'logicalSiblingUtilities')) {
return {
'& > :not([hidden]) ~ :not([hidden])': {
'--tw-space-x-reverse': '0',
@ -1300,13 +1320,13 @@ export let corePlugins = {
})
},
divideWidth: ({ matchUtilities, addUtilities, theme }) => {
divideWidth: ({ matchUtilities, addUtilities, theme, config }) => {
matchUtilities(
{
'divide-x': (value) => {
value = value === '0' ? '0px' : value
if (__OXIDE__) {
if (flagEnabled(config(), 'logicalSiblingUtilities')) {
return {
'& > :not([hidden]) ~ :not([hidden])': {
'@defaults border-width': {},
@ -1503,6 +1523,14 @@ export let corePlugins = {
})
},
textWrap: ({ addUtilities }) => {
addUtilities({
'.text-wrap': { 'text-wrap': 'wrap' },
'.text-nowrap': { 'text-wrap': 'nowrap' },
'.text-balance': { 'text-wrap': 'balance' },
})
},
wordBreak: ({ addUtilities }) => {
addUtilities({
'.break-normal': { 'overflow-wrap': 'normal', 'word-break': 'normal' },

View file

@ -163,6 +163,8 @@ optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-feature-settings: inherit; /* 1 */
font-variation-settings: inherit; /* 1 */
font-size: 100%; /* 1 */
font-weight: inherit; /* 1 */
line-height: inherit; /* 1 */
@ -264,7 +266,7 @@ summary {
}
/*
Removes the default spacing and border for appropriate elements.
Removes the default spacing for appropriate elements.
*/
blockquote,
@ -300,6 +302,10 @@ menu {
padding: 0;
}
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/

View file

@ -3,25 +3,23 @@ import log from './util/log'
let defaults = {
optimizeUniversalDefaults: false,
generalizedModifiers: true,
get disableColorOpacityUtilitiesByDefault() {
return __OXIDE__
},
get relativeContentPathsByDefault() {
return __OXIDE__
},
disableColorOpacityUtilitiesByDefault: true,
relativeContentPathsByDefault: false,
oxideParser: true,
logicalSiblingUtilities: false,
}
let featureFlags = {
export let featureFlags = {
future: [
'hoverOnlyWhenSupported',
'respectDefaultRingColorOpacity',
'disableColorOpacityUtilitiesByDefault',
'relativeContentPathsByDefault',
'logicalSiblingUtilities',
],
experimental: [
'optimizeUniversalDefaults',
'generalizedModifiers',
'oxideParser',
// 'variantGrouping',
],
}

View file

@ -7,10 +7,40 @@ import fastGlob from 'fast-glob'
import normalizePath from 'normalize-path'
import { parseGlob } from '../util/parseGlob'
import { env } from './sharedState'
import { resolveContentPaths } from '@tailwindcss/oxide'
/** @typedef {import('../../types/config.js').RawFile} RawFile */
/** @typedef {import('../../types/config.js').FilePath} FilePath */
/*
* @param {import('tailwindcss').Config} tailwindConfig
* @param {{skip:string[]}} options
* @returns {ContentPath[]}
*/
function resolveContentFiles(tailwindConfig, { skip = [] } = {}) {
if (
Array.isArray(tailwindConfig.content.files) &&
tailwindConfig.content.files.includes('auto')
) {
let idx = tailwindConfig.content.files.indexOf('auto')
if (idx !== -1) {
env.DEBUG && console.time('Calculating resolve content paths')
let resolved = resolveContentPaths({ base: process.cwd() })
env.DEBUG && console.timeEnd('Calculating resolve content paths')
tailwindConfig.content.files.splice(idx, 1, ...resolved)
}
}
if (skip.length > 0) {
tailwindConfig.content.files = tailwindConfig.content.files.filter(
(filePath) => !skip.includes(filePath)
)
}
return tailwindConfig.content.files
}
/**
* @typedef {object} ContentPath
* @property {string} original
@ -32,7 +62,9 @@ import { env } from './sharedState'
* @returns {ContentPath[]}
*/
export function parseCandidateFiles(context, tailwindConfig) {
let files = tailwindConfig.content.files
let files = resolveContentFiles(tailwindConfig, {
skip: [context.userConfigPath],
})
// Normalize the file globs
files = files.filter((filePath) => typeof filePath === 'string')

View file

@ -1,7 +1,7 @@
import dlv from 'dlv'
import didYouMean from 'didyoumean'
import transformThemeValue from '../util/transformThemeValue'
import parseValue from 'postcss-value-parser'
import parseValue from '../value-parser/index'
import { normalizeScreens } from '../util/normalizeScreens'
import buildMediaQuery from '../util/buildMediaQuery'
import { toPath } from '../util/toPath'
@ -146,6 +146,9 @@ function resolveVNode(node, vNode, functions) {
}
function resolveFunctions(node, input, functions) {
let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`))
if (!hasAnyFn) return input
return parseValue(input)
.walk((vNode) => {
resolveVNode(node, vNode, functions)

View file

@ -1,10 +1,12 @@
import fs from 'fs'
import LRU from '@alloc/quick-lru'
import { parseCandidateStrings, IO, Parsing } from '@tailwindcss/oxide'
import * as sharedState from './sharedState'
import { generateRules } from './generateRules'
import log from '../util/log'
import cloneNodes from '../util/cloneNodes'
import { defaultExtractor } from './defaultExtractor'
import { flagEnabled } from '../featureFlags'
let env = sharedState.env
@ -24,7 +26,10 @@ function getExtractor(context, fileExtension) {
extractors[fileExtension] ||
extractors.DEFAULT ||
builtInExtractors[fileExtension] ||
builtInExtractors.DEFAULT(context)
// Because we call `DEFAULT(context)`, the returning function is always a new function without a
// stable identity. Marking it with `DEFAULT_EXTRACTOR` allows us to check if it is the default
// extractor without relying on the function identity.
Object.assign(builtInExtractors.DEFAULT(context), { DEFAULT_EXTRACTOR: true })
)
}
@ -130,20 +135,36 @@ export default function expandTailwindAtRules(context) {
env.DEBUG && console.time('Reading changed files')
if (__OXIDE__) {
// TODO: Pass through or implement `extractor`
for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles(
context.changedContent
// Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
)) {
candidates.add(candidate)
if (flagEnabled(context.tailwindConfig, 'oxideParser')) {
let rustParserContent = []
let regexParserContent = []
for (let item of context.changedContent) {
let transformer = getTransformer(context.tailwindConfig, item.extension)
let extractor = getExtractor(context, item.extension)
if (transformer === builtInTransformers.DEFAULT && extractor?.DEFAULT_EXTRACTOR === true) {
rustParserContent.push(item)
} else {
regexParserContent.push([item, { transformer, extractor }])
}
}
// for (let { file, content, extension } of context.changedContent) {
// let transformer = getTransformer(context.tailwindConfig, extension)
// let extractor = getExtractor(context, extension)
// getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
// }
if (rustParserContent.length > 0) {
for (let candidate of parseCandidateStrings(
rustParserContent,
IO.Parallel | Parsing.Parallel
)) {
candidates.add(candidate)
}
}
if (regexParserContent.length > 0) {
for (let [{ file, content }, { transformer, extractor }] of regexParserContent) {
content = file ? fs.readFileSync(file, 'utf8') : content
getClassCandidates(transformer(content), extractor, candidates, seen)
}
}
} else {
for (let { file, content, extension } of context.changedContent) {
let transformer = getTransformer(context.tailwindConfig, extension)
@ -162,15 +183,16 @@ export default function expandTailwindAtRules(context) {
env.DEBUG && console.time('Generate rules')
env.DEBUG && console.time('Sorting candidates')
let sortedCandidates = __OXIDE__
? candidates
: new Set(
[...candidates].sort((a, z) => {
if (a === z) return 0
if (a < z) return -1
return 1
})
)
// TODO: only sort if we are not using the oxide parser (flagEnabled(context.tailwindConfig,
// 'oxideParser')) AND if we got all the candidates form the oxideParser alone. This will not
// be the case currently if you have custom transformers / extractors.
let sortedCandidates = new Set(
[...candidates].sort((a, z) => {
if (a === z) return 0
if (a < z) return -1
return 1
})
)
env.DEBUG && console.timeEnd('Sorting candidates')
generateRules(sortedCandidates, context)
env.DEBUG && console.timeEnd('Generate rules')
@ -244,11 +266,21 @@ export default function expandTailwindAtRules(context) {
)
layerNodes.variants.remove()
} else if (variantNodes.length > 0) {
root.append(
cloneNodes(variantNodes, root.source, {
layer: 'variants',
let cloned = cloneNodes(variantNodes, undefined, {
layer: 'variants',
})
cloned.forEach((node) => {
let parentLayer = node.raws.tailwind?.parentLayer ?? null
node.walk((n) => {
if (!n.source) {
n.source = layerNodes[parentLayer].source
}
})
)
})
root.append(cloned)
}
// If we've got a utility layer and no utilities are generated there's likely something wrong

View file

@ -45,7 +45,7 @@ function* candidatePermutations(candidate) {
let bracketIdx = candidate.indexOf('[')
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
// eg. string[]
// e.g. string[]
if (candidate[bracketIdx - 1] === '-') {
dashIdx = bracketIdx - 1
} else if (candidate[bracketIdx - 1] === '/') {
@ -175,10 +175,6 @@ function applyVariant(variant, matches, context) {
if (modifiers.length && !context.variantMap.has(variant)) {
variant = baseVariant
args.modifier = modifiers[0]
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
return []
}
}
}
@ -193,13 +189,13 @@ function applyVariant(variant, matches, context) {
// group[:hover] (`-` is missing)
let match = /(.)(-?)\[(.*)\]/g.exec(variant)
if (match) {
let [, char, seperator, value] = match
let [, char, separator, value] = match
// @-[200px] case
if (char === '@' && seperator === '-') return []
if (char === '@' && separator === '-') return []
// group[:hover] case
if (char !== '@' && seperator === '') return []
if (char !== '@' && separator === '') return []
variant = variant.replace(`${seperator}[${value}]`, '')
variant = variant.replace(`${separator}[${value}]`, '')
args.value = value
}
}
@ -459,9 +455,9 @@ function isParsableNode(node) {
}
function isParsableCssValue(property, value) {
// We don't want to to treat [https://example.com] as a custom property
// We don't want to treat [https://example.com] as a custom property
// Even though, according to the CSS grammar, it's a totally valid CSS declaration
// So we short-circuit here by checking if the custom property looks like a url
// So we short-circuit here by checking if the custom property looks like a URL
if (looksLikeUri(`${property}:${value}`)) {
return false
}

View file

@ -0,0 +1,34 @@
import postcss from 'postcss'
import postcssImport from 'postcss-import'
const TAILWIND = Symbol()
export function handleImportAtRules() {
let RESTORE_ATRULE_COMMENT = '__TAILWIND_RESTORE__'
let atRulesToRestore = ['tailwind', 'config']
return [
(root) => {
root.walkAtRules((rule) => {
if (!atRulesToRestore.includes(rule.name)) return rule
rule.after(
postcss.comment({
text: RESTORE_ATRULE_COMMENT,
raws: { [TAILWIND]: { rule } },
})
)
rule.remove()
})
},
postcssImport(),
(root) => {
root.walkComments((rule) => {
if (rule.text === RESTORE_ATRULE_COMMENT) {
rule.after(rule.raws[TAILWIND].rule)
rule.remove()
}
})
},
]
}

View file

@ -10,33 +10,6 @@ export default function normalizeTailwindDirectives(root) {
applyDirectives.add(atRule)
}
if (atRule.name === 'import') {
if (atRule.params === '"tailwindcss/base"' || atRule.params === "'tailwindcss/base'") {
atRule.name = 'tailwind'
atRule.params = 'base'
} else if (
atRule.params === '"tailwindcss/components"' ||
atRule.params === "'tailwindcss/components'"
) {
atRule.name = 'tailwind'
atRule.params = 'components'
} else if (
atRule.params === '"tailwindcss/utilities"' ||
atRule.params === "'tailwindcss/utilities'"
) {
atRule.name = 'tailwind'
atRule.params = 'utilities'
} else if (
atRule.params === '"tailwindcss/screens"' ||
atRule.params === "'tailwindcss/screens'" ||
atRule.params === '"tailwindcss/variants"' ||
atRule.params === "'tailwindcss/variants'"
) {
atRule.name = 'tailwind'
atRule.params = 'variants'
}
}
if (atRule.name === 'tailwind') {
if (atRule.params === 'screens') {
atRule.params = 'variants'

View file

@ -23,7 +23,7 @@ import { remapBitfield } from './remap-bitfield.js'
* @property {bigint} arbitrary 0n if false, 1n if true
* @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants
* @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable.
* @property {bigint} index Index of the rule / utility in it's given *parent* layer. Monotonically increasing.
* @property {bigint} index Index of the rule / utility in its given *parent* layer. Monotonically increasing.
* @property {VariantOption[]} options Some information on how we can sort arbitrary variants
*/
@ -302,7 +302,7 @@ export class Offsets {
let mapping = this.recalculateVariantOffsets()
// No arbitrary variants? Nothing to do.
// Everyhing already in order? Nothing to do.
// Everything already in order? Nothing to do.
if (mapping.length === 0) {
return list
}
@ -354,7 +354,7 @@ function max(nums) {
* Using `.sort()` without a custom compare function is faster
* But you can only use that if you're sorting an array of
* only strings. If you're sorting strings inside objects
* or arrays, you need must use a custom compare function.
* or arrays, you need to use a custom compare function.
*
* @param {string} a
* @param {string} b

205
src/lib/parseCssConfig.js Normal file
View file

@ -0,0 +1,205 @@
// @ts-check
/**
* @typedef {[plugin: string|null, key: string[]|null, option: string|null]} ConfigPath
*/
export class CssBasedConfig {
constructor() {
this.config = {}
}
/**
* Find and convert the `:theme` rule to a configuration object
* This is merged with the user's JS config
* It should take precedence over the JS config
*
* @param {import('postcss').Root} root
* @returns {*}
*/
parse(root) {
this.config = { extend: {} }
let rules = this.getThemeRules(root)
rules.forEach(rule => {
rule.walkDecls((decl) => {
if (!decl.prop.startsWith('--')) {
throw decl.error('Only CSS variables are allowed in `:theme`')
}
this.parseVariable(decl)
})
// Allow the variables to be used with var() anywhere in CSS
rule.selector = ':root'
})
return this.config
}
/**
* Walk the tree and find all `:theme` rules
*
* @param {import('postcss').Root} root
* @returns {import('postcss').Rule[]}
*/
getThemeRules(root) {
/** @type {any[]} */
let rules = root.nodes.filter(node => node.type === 'rule' && node.selector === ':theme')
return rules
}
/**
* Parse a variable declaration and modify the config
*
* @param {import('postcss').Declaration} decl
*/
parseVariable(decl) {
let config = this.config
// Parse the variable name into a config plugin and keypath
let [name, value] = [decl.prop.slice(2), decl.value]
let [plugin, keypath, option] = this.parseVariableName(name)
if (plugin === null) {
// We couldn't find a plugin for this variable, so we can't do anything with it
// This also means there is no reason to keep the variable in the CSS
decl.remove()
return
}
if (keypath === null && value === 'unset') {
// when using `--some-plugin: unset` we want to reset whatever the default value is
config[plugin] = {}
// This also means there is no reason to keep the variable in the CSS
decl.remove()
return
}
if (keypath !== null && value === 'unset') {
// when using `--plugin-key: unset` we want to remove that from the config
throw decl.error("Not implemented yet");
}
// TODO: Can this happen
if (keypath === null) {
return
}
// All other cases are config keys in a plugin
config.extend[plugin] ??= {}
let [path, key] = [
keypath.slice(0, -1),
keypath[keypath.length-1],
]
let obj = config.extend[plugin]
let segment
while (segment = path.shift()) {
obj[segment] ??= {}
obj = obj[segment]
}
if (option) {
// TODO: This is weird
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key], {}]
}
obj[key][1][option] = value
} else {
obj[key] = value
}
}
/**
* Convert a variable name into:
* - The associated plugin
* - A keypath to modify inside plugin's config (if available)
* - An "option" which is a key into the "options" object when a Tuple-like value is used
*
* @param {string} name
* @returns {ConfigPath}
*/
parseVariableName(name) {
// Unescape the name if needed
// This is needed for CSS variables that contain special characters
name = unescape(name)
for (const [prefix, plugin] of Object.entries(this.prefixPluginMap)) {
if (!name.startsWith(prefix)) continue
// The keypath is the part of the variable name after the prefix
let keypath = name.slice(prefix.length)
let option = null
let optionStart = keypath.indexOf('--')
if (optionStart !== -1) {
option = keypath.slice(optionStart+2)
keypath = keypath.slice(0, optionStart)
}
if (keypath == '' && option === 'default') {
keypath = 'DEFAULT'
option = null
} else if (keypath.startsWith('-')) {
keypath = keypath.slice(1)
}
if (keypath === '') {
return [plugin, null, null]
}
option = option === null ? null : camelize(option);
if (plugin === 'colors') {
return [plugin, keypath.split('-'), option]
}
return [plugin, [keypath], option]
}
return [null, null, null]
}
get prefixPluginMap() {
// TODO: Maybe build this based on a list of registered plugins
// Or maybe a plugin can register namespaces themselves?
return {
'animation': 'animation',
'colors': 'colors',
'spacing': 'spacing',
'font-family': 'fontFamily',
'font-size': 'fontSize',
// 'shadow': 'boxShadow',
'blur': 'blur',
}
}
}
/**
* @param {string} str
* @returns {string}
*/
function camelize(str) {
return str.replace(/-([a-zA-Z])/g, (v) => v.slice(1).toUpperCase())
}
/**
* @param {string} str
* @returns {string}
*/
function unescape(str) {
let pattern = /\\([0-9a-fA-F]{1,6}[\t\n\f\r ]?|[\S\s])/g
return str.replace(pattern, (match) =>
match.length > 2
? String.fromCodePoint(parseInt(match.slice(1).trimEnd(), 16))
: match[1]
)
}

View file

@ -21,7 +21,6 @@ import isSyntacticallyValidPropertyValue from '../util/isSyntacticallyValidPrope
import { generateRules, getClassNameFromSelector } from './generateRules'
import { hasContentChanged } from './cacheInvalidation.js'
import { Offsets } from './offsets.js'
import { flagEnabled } from '../featureFlags.js'
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'
const VARIANT_TYPES = {
@ -434,10 +433,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
},
}
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let ruleSets = []
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
.concat(rule(value, extras))
.filter(Boolean)
.map((declaration) => ({
[nameClass(identifier, modifier)]: declaration,
@ -514,10 +511,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
},
}
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let ruleSets = []
.concat(modifiersEnabled ? rule(value, extras) : rule(value))
.concat(rule(value, extras))
.filter(Boolean)
.map((declaration) => ({
[nameClass(identifier, modifier)]: declaration,
@ -585,18 +580,13 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
let id = options?.id ?? ++variantIdentifier
let isSpecial = variant === '@'
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
for (let [key, value] of Object.entries(options?.values ?? {})) {
if (key === 'DEFAULT') continue
api.addVariant(
isSpecial ? `${variant}${key}` : `${variant}-${key}`,
({ args, container }) => {
return variantFn(
value,
modifiersEnabled ? { modifier: args?.modifier, container } : { container }
)
return variantFn(value, { modifier: args?.modifier, container })
},
{
@ -624,7 +614,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
: // Falling back to args if it is a string, otherwise '' for older intellisense
// (JetBrains) plugins.
args?.value ?? (typeof args === 'string' ? args : ''),
modifiersEnabled ? { modifier: args?.modifier, container } : { container }
{ modifier: args?.modifier, container }
)
},
{
@ -729,7 +719,7 @@ function collectLayerPlugins(root) {
}
function resolvePlugins(context, root) {
let corePluginList = Object.entries({ ...variantPlugins, ...corePlugins })
let corePluginList = Object.entries(corePlugins)
.map(([name, plugin]) => {
if (!context.tailwindConfig.corePlugins.includes(name)) {
return null
@ -754,6 +744,7 @@ function resolvePlugins(context, root) {
let beforeVariants = [
variantPlugins['pseudoElementVariants'],
variantPlugins['pseudoClassVariants'],
variantPlugins['hasVariants'],
variantPlugins['ariaVariants'],
variantPlugins['dataVariants'],
]

View file

@ -12,6 +12,7 @@ import { validateConfig } from '../util/validateConfig.js'
import { parseCandidateFiles, resolvedChangedContent } from './content.js'
import { loadConfig } from '../lib/load-config'
import getModuleDependencies from './getModuleDependencies'
import { CssBasedConfig } from './parseCssConfig.js'
let configPathCache = new LRU({ maxSize: 100 })
@ -28,7 +29,7 @@ function getCandidateFiles(context, tailwindConfig) {
}
// Get the config object based on a path
function getTailwindConfig(configOrPath) {
function getTailwindConfig(configOrPath, cssThemeValues = {}) {
let userConfigPath = resolveConfigPath(configOrPath)
if (userConfigPath !== null) {
@ -56,7 +57,9 @@ function getTailwindConfig(configOrPath) {
for (let file of newDeps) {
delete require.cache[file]
}
let newConfig = validateConfig(resolveConfig(loadConfig(userConfigPath)))
let newConfig = validateConfig(
resolveConfig(loadConfig(userConfigPath), { theme: cssThemeValues })
)
let newHash = hash(newConfig)
configPathCache.set(userConfigPath, [newConfig, newHash, newDeps, newModified])
return [newConfig, userConfigPath, newHash, newDeps]
@ -64,7 +67,8 @@ function getTailwindConfig(configOrPath) {
// It's a plain object, not a path
let newConfig = resolveConfig(
configOrPath.config === undefined ? configOrPath : configOrPath.config
configOrPath?.config ?? configOrPath ?? {},
{ theme: cssThemeValues }
)
newConfig = validateConfig(newConfig)
@ -80,8 +84,11 @@ function getTailwindConfig(configOrPath) {
export default function setupTrackingContext(configOrPath) {
return ({ tailwindDirectives, registerDependency }) => {
return (root, result) => {
// Parse any CSS-based config values from `:theme` rules
let cssThemeValues = new CssBasedConfig().parse(root)
let [tailwindConfig, userConfigPath, tailwindConfigHash, configDependencies] =
getTailwindConfig(configOrPath)
getTailwindConfig(configOrPath, cssThemeValues)
let contextDependencies = new Set(configDependencies)

View file

@ -1,16 +1,12 @@
import pkg from '../../package.json'
export const env =
typeof process !== 'undefined'
? {
NODE_ENV: process.env.NODE_ENV,
DEBUG: resolveDebug(process.env.DEBUG),
ENGINE: pkg.tailwindcss.engine,
}
: {
NODE_ENV: 'production',
DEBUG: false,
ENGINE: pkg.tailwindcss.engine,
}
export const contextMap = new Map()

View file

@ -1 +0,0 @@
import './cli/index'

View file

@ -1,91 +0,0 @@
import packageJson from '../../../../package.json'
import browserslist from 'browserslist'
import { Result } from 'postcss'
import {
// @ts-ignore
lazyPostcss,
// @ts-ignore
lazyPostcssImport,
// @ts-ignore
lazyCssnano,
// @ts-ignore
} from '../../../../peers/index'
export function lazyLightningCss() {
// TODO: Make this lazy/bundled
return require('lightningcss')
}
let lightningCss
function loadLightningCss() {
if (lightningCss) {
return lightningCss
}
// Try to load a local version first
try {
return (lightningCss = require('lightningcss'))
} catch {}
return (lightningCss = lazyLightningCss())
}
export async function lightningcss(shouldMinify: boolean, result: Result) {
let css = loadLightningCss()
try {
let transformed = css.transform({
filename: result.opts.from || 'input.css',
code: Buffer.from(result.css, 'utf-8'),
minify: shouldMinify,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets: css.browserslistToTargets(browserslist(packageJson.browserslist)),
drafts: {
nesting: true,
},
})
return Object.assign(result, {
css: transformed.code.toString('utf8'),
map: result.map
? Object.assign(result.map, {
toString() {
return transformed.map.toString()
},
})
: result.map,
})
} catch (err) {
console.error('Unable to use Lightning CSS. Using raw version instead.')
console.error(err)
return result
}
}
/**
* @returns {import('postcss')}
*/
export function loadPostcss() {
// Try to load a local `postcss` version first
try {
return require('postcss')
} catch {}
return lazyPostcss()
}
export function loadPostcssImport() {
// Try to load a local `postcss-import` version first
try {
return require('postcss-import')
} catch {}
return lazyPostcssImport()
}

View file

@ -1,47 +0,0 @@
import fs from 'fs'
import path from 'path'
import { resolveDefaultConfigPath } from '../../../util/resolveConfigPath'
import { createProcessor } from './plugin'
export async function build(args) {
let input = args['--input']
let shouldWatch = args['--watch']
// TODO: Deprecate this in future versions
if (!input && args['_'][1]) {
console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
input = args['--input'] = args['_'][1]
}
if (input && input !== '-' && !fs.existsSync((input = path.resolve(input)))) {
console.error(`Specified input file ${args['--input']} does not exist.`)
process.exit(9)
}
if (args['--config'] && !fs.existsSync((args['--config'] = path.resolve(args['--config'])))) {
console.error(`Specified config file ${args['--config']} does not exist.`)
process.exit(9)
}
// TODO: Reference the @config path here if exists
let configPath = args['--config'] ? args['--config'] : resolveDefaultConfigPath()
let processor = await createProcessor(args, configPath)
if (shouldWatch) {
// Abort the watcher if stdin is closed to avoid zombie processes
// You can disable this behavior with --watch=always
if (args['--watch'] !== 'always') {
process.stdin.on('end', () => process.exit(0))
}
process.stdin.resume()
await processor.watch()
} else {
await processor.build().catch((e) => {
console.error(e)
process.exit(1)
})
}
}

View file

@ -1,442 +0,0 @@
import path from 'path'
import fs from 'fs'
import postcssrc from 'postcss-load-config'
import { lilconfig } from 'lilconfig'
import loadPlugins from 'postcss-load-config/src/plugins' // Little bit scary, looking at private/internal API
import loadOptions from 'postcss-load-config/src/options' // Little bit scary, looking at private/internal API
import tailwind from '../../../processTailwindFeatures'
import { loadPostcss, loadPostcssImport, lightningcss } from './deps'
import { formatNodes, drainStdin, outputFile } from './utils'
import { env } from '../../../lib/sharedState'
import resolveConfig from '../../../../resolveConfig'
import { parseCandidateFiles } from '../../../lib/content'
import { createWatcher } from './watching'
import fastGlob from 'fast-glob'
import { findAtConfigPath } from '../../../lib/findAtConfigPath'
import log from '../../../util/log'
import { loadConfig } from '../../../lib/load-config'
import getModuleDependencies from '../../../lib/getModuleDependencies'
import type { Config } from '../../../../types'
/**
*
* @param {string} [customPostCssPath ]
* @returns
*/
async function loadPostCssPlugins(customPostCssPath) {
let config = customPostCssPath
? await (async () => {
let file = path.resolve(customPostCssPath)
// Implementation, see: https://unpkg.com/browse/postcss-load-config@3.1.0/src/index.js
// @ts-ignore
let { config = {} } = await lilconfig('postcss').load(file)
if (typeof config === 'function') {
config = config()
} else {
config = Object.assign({}, config)
}
if (!config.plugins) {
config.plugins = []
}
return {
file,
plugins: loadPlugins(config, file),
options: loadOptions(config, file),
}
})()
: await postcssrc()
let configPlugins = config.plugins
let configPluginTailwindIdx = configPlugins.findIndex((plugin) => {
if (typeof plugin === 'function' && plugin.name === 'tailwindcss') {
return true
}
if (typeof plugin === 'object' && plugin !== null && plugin.postcssPlugin === 'tailwindcss') {
return true
}
return false
})
let beforePlugins =
configPluginTailwindIdx === -1 ? [] : configPlugins.slice(0, configPluginTailwindIdx)
let afterPlugins =
configPluginTailwindIdx === -1
? configPlugins
: configPlugins.slice(configPluginTailwindIdx + 1)
return [beforePlugins, afterPlugins, config.options]
}
function loadBuiltinPostcssPlugins() {
let postcss = loadPostcss()
let IMPORT_COMMENT = '__TAILWIND_RESTORE_IMPORT__: '
return [
[
(root) => {
root.walkAtRules('import', (rule) => {
if (rule.params.slice(1).startsWith('tailwindcss/')) {
rule.after(postcss.comment({ text: IMPORT_COMMENT + rule.params }))
rule.remove()
}
})
},
loadPostcssImport(),
(root) => {
root.walkComments((rule) => {
if (rule.text.startsWith(IMPORT_COMMENT)) {
rule.after(
postcss.atRule({
name: 'import',
params: rule.text.replace(IMPORT_COMMENT, ''),
})
)
rule.remove()
}
})
},
],
[],
{},
]
}
let state = {
/** @type {any} */
context: null,
/** @type {ReturnType<typeof createWatcher> | null} */
watcher: null,
/** @type {{content: string, extension: string}[]} */
changedContent: [],
/** @type {{config: Config, dependencies: Set<string>, dispose: Function } | null} */
configBag: null,
contextDependencies: new Set(),
/** @type {import('../../lib/content.js').ContentPath[]} */
contentPaths: [],
refreshContentPaths() {
this.contentPaths = parseCandidateFiles(this.context, this.context?.tailwindConfig)
},
get config() {
return this.context.tailwindConfig
},
get contentPatterns() {
return {
all: this.contentPaths.map((contentPath) => contentPath.pattern),
dynamic: this.contentPaths
.filter((contentPath) => contentPath.glob !== undefined)
.map((contentPath) => contentPath.pattern),
}
},
loadConfig(configPath, content) {
if (this.watcher && configPath) {
this.refreshConfigDependencies()
}
let config = loadConfig(configPath)
let dependencies = getModuleDependencies(configPath)
this.configBag = {
config,
dependencies,
dispose() {
for (let file of dependencies) {
delete require.cache[require.resolve(file)]
}
},
}
// @ts-ignore
this.configBag.config = resolveConfig(this.configBag.config, { content: { files: [] } })
// Override content files if `--content` has been passed explicitly
if (content?.length > 0) {
this.configBag.config.content.files = content
}
return this.configBag.config
},
refreshConfigDependencies(configPath) {
env.DEBUG && console.time('Module dependencies')
this.configBag?.dispose()
env.DEBUG && console.timeEnd('Module dependencies')
},
readContentPaths() {
let content = []
// Resolve globs from the content config
// TODO: When we make the postcss plugin async-capable this can become async
let files = fastGlob.sync(this.contentPatterns.all)
for (let file of files) {
if (__OXIDE__) {
content.push({
file,
extension: path.extname(file).slice(1),
})
} else {
content.push({
content: fs.readFileSync(path.resolve(file), 'utf8'),
extension: path.extname(file).slice(1),
})
}
}
// Resolve raw content in the tailwind config
let rawContent = this.config.content.files.filter((file) => {
return file !== null && typeof file === 'object'
})
for (let { raw: htmlContent, extension = 'html' } of rawContent) {
content.push({ content: htmlContent, extension })
}
return content
},
getContext({ createContext, cliConfigPath, root, result, content }) {
if (this.context) {
this.context.changedContent = this.changedContent.splice(0)
return this.context
}
env.DEBUG && console.time('Searching for config')
let configPath = findAtConfigPath(root, result) ?? cliConfigPath
env.DEBUG && console.timeEnd('Searching for config')
env.DEBUG && console.time('Loading config')
let config = this.loadConfig(configPath, content)
env.DEBUG && console.timeEnd('Loading config')
env.DEBUG && console.time('Creating context')
this.context = createContext(config, [])
Object.assign(this.context, {
userConfigPath: configPath,
})
env.DEBUG && console.timeEnd('Creating context')
env.DEBUG && console.time('Resolving content paths')
this.refreshContentPaths()
env.DEBUG && console.timeEnd('Resolving content paths')
if (this.watcher) {
env.DEBUG && console.time('Watch new files')
this.watcher.refreshWatchedFiles()
env.DEBUG && console.timeEnd('Watch new files')
}
for (let file of this.readContentPaths()) {
this.context.changedContent.push(file)
}
return this.context
},
}
export async function createProcessor(args, cliConfigPath) {
let postcss = loadPostcss()
let input = args['--input']
let output = args['--output']
let includePostCss = args['--postcss']
let customPostCssPath = typeof args['--postcss'] === 'string' ? args['--postcss'] : undefined
let [beforePlugins, afterPlugins, postcssOptions] = includePostCss
? await loadPostCssPlugins(customPostCssPath)
: loadBuiltinPostcssPlugins()
if (args['--purge']) {
log.warn('purge-flag-deprecated', [
'The `--purge` flag has been deprecated.',
'Please use `--content` instead.',
])
if (!args['--content']) {
args['--content'] = args['--purge']
}
}
let content = args['--content']?.split(/(?<!{[^}]+),/) ?? []
let tailwindPlugin = () => {
return {
postcssPlugin: 'tailwindcss',
Once(root, { result }) {
env.DEBUG && console.time('Compiling CSS')
tailwind(({ createContext }) => {
console.error()
console.error('Rebuilding...')
return () => {
return state.getContext({
createContext,
cliConfigPath,
root,
result,
content,
})
}
})(root, result)
env.DEBUG && console.timeEnd('Compiling CSS')
},
}
}
tailwindPlugin.postcss = true
let plugins = [
...beforePlugins,
tailwindPlugin,
!args['--minify'] && formatNodes,
...afterPlugins,
].filter(Boolean)
/** @type {import('postcss').Processor} */
// @ts-ignore
let processor = postcss(plugins)
async function readInput() {
// Piping in data, let's drain the stdin
if (input === '-') {
return drainStdin()
}
// Input file has been provided
if (input) {
return fs.promises.readFile(path.resolve(input), 'utf8')
}
// No input file provided, fallback to default atrules
return '@tailwind base; @tailwind components; @tailwind utilities'
}
async function build() {
let start = process.hrtime.bigint()
return readInput()
.then((css) => processor.process(css, { ...postcssOptions, from: input, to: output }))
.then((result) => lightningcss(!!args['--minify'], result))
.then((result) => {
if (!state.watcher) {
return result
}
env.DEBUG && console.time('Recording PostCSS dependencies')
for (let message of result.messages) {
if (message.type === 'dependency') {
state.contextDependencies.add(message.file)
}
}
env.DEBUG && console.timeEnd('Recording PostCSS dependencies')
// TODO: This needs to be in a different spot
env.DEBUG && console.time('Watch new files')
state.watcher.refreshWatchedFiles()
env.DEBUG && console.timeEnd('Watch new files')
return result
})
.then((result) => {
if (!output) {
process.stdout.write(result.css)
return
}
return Promise.all([
outputFile(result.opts.to, result.css),
result.map && outputFile(result.opts.to + '.map', result.map.toString()),
])
})
.then(() => {
let end = process.hrtime.bigint()
console.error()
console.error('Done in', (end - start) / BigInt(1e6) + 'ms.')
})
.then(
() => {},
(err) => {
// TODO: If an initial build fails we can't easily pick up any PostCSS dependencies
// that were collected before the error occurred
// The result is not stored on the error so we have to store it externally
// and pull the messages off of it here somehow
// This results in a less than ideal DX because the watcher will not pick up
// changes to imported CSS if one of them caused an error during the initial build
// If you fix it and then save the main CSS file so there's no error
// The watcher will start watching the imported CSS files and will be
// resilient to future errors.
if (state.watcher) {
console.error(err)
} else {
return Promise.reject(err)
}
}
)
}
/**
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
*/
async function parseChanges(changes) {
return Promise.all(
changes.map(async (change) => ({
content: await change.content(),
extension: change.extension,
}))
)
}
if (input !== undefined && input !== '-') {
state.contextDependencies.add(path.resolve(input))
}
return {
build,
watch: async () => {
state.watcher = createWatcher(args, {
state,
/**
* @param {{file: string, content(): Promise<string>, extension: string}[]} changes
*/
async rebuild(changes) {
let needsNewContext = changes.some((change) => {
return (
state.configBag?.dependencies.has(change.file) ||
state.contextDependencies.has(change.file)
)
})
if (needsNewContext) {
state.context = null
} else {
for (let change of await parseChanges(changes)) {
state.changedContent.push(change)
}
}
return build()
},
})
await build()
},
}
}

View file

@ -1,74 +0,0 @@
import fs from 'fs'
import path from 'path'
export function indentRecursive(node, indent = 0) {
node.each &&
node.each((child, i) => {
if (!child.raws.before || !child.raws.before.trim() || child.raws.before.includes('\n')) {
child.raws.before = `\n${node.type !== 'rule' && i > 0 ? '\n' : ''}${' '.repeat(indent)}`
}
child.raws.after = `\n${' '.repeat(indent)}`
indentRecursive(child, indent + 1)
})
}
export function formatNodes(root) {
indentRecursive(root)
if (root.first) {
root.first.raws.before = ''
}
}
/**
* When rapidly saving files atomically a couple of situations can happen:
* - The file is missing since the external program has deleted it by the time we've gotten around to reading it from the earlier save.
* - The file is being written to by the external program by the time we're going to read it and is thus treated as busy because a lock is held.
*
* To work around this we retry reading the file a handful of times with a delay between each attempt
*
* @param {string} path
* @param {number} tries
* @returns {Promise<string | undefined>}
* @throws {Error} If the file is still missing or busy after the specified number of tries
*/
export async function readFileWithRetries(path, tries = 5) {
for (let n = 0; n <= tries; n++) {
try {
return await fs.promises.readFile(path, 'utf8')
} catch (err) {
if (n !== tries) {
if (err.code === 'ENOENT' || err.code === 'EBUSY') {
await new Promise((resolve) => setTimeout(resolve, 10))
continue
}
}
throw err
}
}
}
export function drainStdin() {
return new Promise((resolve, reject) => {
let result = ''
process.stdin.on('data', (chunk) => {
result += chunk
})
process.stdin.on('end', () => resolve(result))
process.stdin.on('error', (err) => reject(err))
})
}
export async function outputFile(file, newContents) {
try {
let currentContents = await fs.promises.readFile(file, 'utf8')
if (currentContents === newContents) {
return // Skip writing the file
}
} catch {}
// Write the file
await fs.promises.mkdir(path.dirname(file), { recursive: true })
await fs.promises.writeFile(file, newContents, 'utf8')
}

View file

@ -1,225 +0,0 @@
import chokidar from 'chokidar'
import fs from 'fs'
import micromatch from 'micromatch'
import normalizePath from 'normalize-path'
import path from 'path'
import { readFileWithRetries } from './utils'
/**
* The core idea of this watcher is:
* 1. Whenever a file is added, changed, or renamed we queue a rebuild
* 2. Perform as few rebuilds as possible by batching them together
* 3. Coalesce events that happen in quick succession to avoid unnecessary rebuilds
* 4. Ensure another rebuild happens _if_ changed while a rebuild is in progress
*/
/**
*
* @param {*} args
* @param {{ state, rebuild(changedFiles: any[]): Promise<any> }} param1
* @returns {{
* fswatcher: import('chokidar').FSWatcher,
* refreshWatchedFiles(): void,
* }}
*/
export function createWatcher(args, { state, rebuild }) {
let shouldPoll = args['--poll']
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
// Polling interval in milliseconds
// Used only when polling or coalescing add/change events on Windows
let pollInterval = 10
let watcher = chokidar.watch([], {
// Force checking for atomic writes in all situations
// This causes chokidar to wait up to 100ms for a file to re-added after it's been unlinked
// This only works when watching directories though
atomic: true,
usePolling: shouldPoll,
interval: shouldPoll ? pollInterval : undefined,
ignoreInitial: true,
awaitWriteFinish: shouldCoalesceWriteEvents
? {
stabilityThreshold: 50,
pollInterval: pollInterval,
}
: false,
})
// A queue of rebuilds, file reads, etc… to run
let chain = Promise.resolve()
/**
* A list of files that have been changed since the last rebuild
*
* @type {{file: string, content: () => Promise<string>, extension: string}[]}
*/
let changedContent = []
/**
* A list of files for which a rebuild has already been queued.
* This is used to prevent duplicate rebuilds when multiple events are fired for the same file.
* The rebuilt file is cleared from this list when it's associated rebuild has _started_
* This is because if the file is changed during a rebuild it won't trigger a new rebuild which it should
**/
let pendingRebuilds = new Set()
let _timer
let _reject
/**
* Rebuilds the changed files and resolves when the rebuild is
* complete regardless of whether it was successful or not
*/
async function rebuildAndContinue() {
let changes = changedContent.splice(0)
// There are no changes to rebuild so we can just do nothing
if (changes.length === 0) {
return Promise.resolve()
}
// Clear all pending rebuilds for the about-to-be-built files
changes.forEach((change) => pendingRebuilds.delete(change.file))
// Resolve the promise even when the rebuild fails
return rebuild(changes).then(
() => {},
() => {}
)
}
/**
*
* @param {*} file
* @param {(() => Promise<string>) | null} content
* @param {boolean} skipPendingCheck
* @returns {Promise<void>}
*/
function recordChangedFile(file, content = null, skipPendingCheck = false) {
file = path.resolve(file)
// Applications like Vim/Neovim fire both rename and change events in succession for atomic writes
// In that case rebuild has already been queued by rename, so can be skipped in change
if (pendingRebuilds.has(file) && !skipPendingCheck) {
return Promise.resolve()
}
// Mark that a rebuild of this file is going to happen
// It MUST happen synchronously before the rebuild is queued for this to be effective
pendingRebuilds.add(file)
changedContent.push({
file,
content: content ?? (() => fs.promises.readFile(file, 'utf8')),
extension: path.extname(file).slice(1),
})
if (_timer) {
clearTimeout(_timer)
_reject()
}
// If a rebuild is already in progress we don't want to start another one until the 10ms timer has expired
chain = chain.then(
() =>
new Promise((resolve, reject) => {
_timer = setTimeout(resolve, 10)
_reject = reject
})
)
// Resolves once this file has been rebuilt (or the rebuild for this file has failed)
// This queues as many rebuilds as there are changed files
// But those rebuilds happen after some delay
// And will immediately resolve if there are no changes
chain = chain.then(rebuildAndContinue, rebuildAndContinue)
return chain
}
watcher.on('change', (file) => recordChangedFile(file))
watcher.on('add', (file) => recordChangedFile(file))
// Restore watching any files that are "removed"
// This can happen when a file is pseudo-atomically replaced (a copy is created, overwritten, the old one is unlinked, and the new one is renamed)
// TODO: An an optimization we should allow removal when the config changes
watcher.on('unlink', (file) => {
file = normalizePath(file)
// Only re-add the file if it's not covered by a dynamic pattern
if (!micromatch.some([file], state.contentPatterns.dynamic)) {
watcher.add(file)
}
})
// Some applications such as Visual Studio (but not VS Code)
// will only fire a rename event for atomic writes and not a change event
// This is very likely a chokidar bug but it's one we need to work around
// We treat this as a change event and rebuild the CSS
watcher.on('raw', (evt, filePath, meta) => {
if (evt !== 'rename') {
return
}
let watchedPath = meta.watchedPath
// Watched path might be the file itself
// Or the directory it is in
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
// Skip this event since the files it is for does not match any of the registered content globs
if (!micromatch.some([filePath], state.contentPatterns.all)) {
return
}
// Skip since we've already queued a rebuild for this file that hasn't happened yet
if (pendingRebuilds.has(filePath)) {
return
}
// We'll go ahead and add the file to the pending rebuilds list here
// It'll be removed when the rebuild starts unless the read fails
// which will be taken care of as well
pendingRebuilds.add(filePath)
async function enqueue() {
try {
// We need to read the file as early as possible outside of the chain
// because it may be gone by the time we get to it. doing the read
// immediately increases the chance that the file is still there
let content = await readFileWithRetries(path.resolve(filePath))
if (content === undefined) {
return
}
// This will push the rebuild onto the chain
// We MUST skip the rebuild check here otherwise the rebuild will never happen on Linux
// This is because the order of events and timing is different on Linux
// @ts-ignore: TypeScript isn't picking up that content is a string here
await recordChangedFile(filePath, () => content, true)
} catch {
// If reading the file fails, it's was probably a deleted temporary file
// So we can ignore it and no rebuild is needed
}
}
enqueue().then(() => {
// If the file read fails we still need to make sure the file isn't stuck in the pending rebuilds list
pendingRebuilds.delete(filePath)
})
})
return {
fswatcher: watcher,
refreshWatchedFiles() {
watcher.add(Array.from(state.contextDependencies))
watcher.add(Array.from(state.configBag.dependencies))
watcher.add(state.contentPatterns.all)
},
}
}

View file

@ -1,69 +0,0 @@
import packageJson from '../../../../package.json'
export function help({ message, usage, commands, options }) {
let indent = 2
// Render header
console.log()
console.log(`${packageJson.name} v${packageJson.version}`)
// Render message
if (message) {
console.log()
for (let msg of message.split('\n')) {
console.log(msg)
}
}
// Render usage
if (usage && usage.length > 0) {
console.log()
console.log('Usage:')
for (let example of usage) {
console.log(' '.repeat(indent), example)
}
}
// Render commands
if (commands && commands.length > 0) {
console.log()
console.log('Commands:')
for (let command of commands) {
console.log(' '.repeat(indent), command)
}
}
// Render options
if (options) {
let groupedOptions = {}
for (let [key, value] of Object.entries(options)) {
if (typeof value === 'object') {
groupedOptions[key] = { ...value, flags: [key] }
} else {
groupedOptions[value].flags.push(key)
}
}
console.log()
console.log('Options:')
for (let { flags, description, deprecated } of Object.values(groupedOptions)) {
if (deprecated) continue
if (flags.length === 1) {
console.log(
' '.repeat(indent + 4 /* 4 = "-i, ".length */),
flags.slice().reverse().join(', ').padEnd(20, ' '),
description
)
} else {
console.log(
' '.repeat(indent),
flags.slice().reverse().join(', ').padEnd(24, ' '),
description
)
}
}
}
console.log()
}

View file

@ -1,204 +0,0 @@
#!/usr/bin/env node
import path from 'path'
import arg from 'arg'
import fs from 'fs'
import { build } from './build'
import { help } from './help'
import { init } from './init'
// ---
function oneOf(...options) {
return Object.assign(
(value = true) => {
for (let option of options) {
let parsed = option(value)
if (parsed === value) {
return parsed
}
}
throw new Error('...')
},
{ manualParsing: true }
)
}
let commands = {
init: {
run: init,
args: {
'--esm': { type: Boolean, description: `Initialize configuration file as ESM` },
'--ts': { type: Boolean, description: `Initialize configuration file as TypeScript` },
'--full': {
type: Boolean,
description: `Include the default values for all options in the generated configuration file`,
},
'-f': '--full',
},
},
build: {
run: build,
args: {
'--input': { type: String, description: 'Input file' },
'--output': { type: String, description: 'Output file' },
'--watch': {
type: oneOf(String, Boolean),
description: 'Watch for changes and rebuild as needed',
},
'--poll': {
type: Boolean,
description: 'Use polling instead of filesystem events when watching',
},
'--content': {
type: String,
description: 'Content paths to use for removing unused classes',
},
'--minify': { type: Boolean, description: 'Minify the output' },
'--config': {
type: String,
description: 'Path to a custom config file',
},
'-c': '--config',
'-i': '--input',
'-o': '--output',
'-m': '--minify',
'-w': '--watch',
'-p': '--poll',
},
},
}
let sharedFlags = {
'--help': { type: Boolean, description: 'Display usage information' },
'-h': '--help',
}
if (
process.stdout.isTTY /* Detect redirecting output to a file */ &&
(process.argv[2] === undefined ||
process.argv.slice(2).every((flag) => sharedFlags[flag] !== undefined))
) {
help({
usage: [
'tailwindcss [--input input.css] [--output output.css] [--watch] [options...]',
'tailwindcss init [--full] [options...]',
],
commands: Object.keys(commands)
.filter((command) => command !== 'build')
.map((command) => `${command} [options]`),
options: { ...commands.build.args, ...sharedFlags },
})
process.exit(0)
}
let command = ((arg = '') => (arg.startsWith('-') ? undefined : arg))(process.argv[2]) || 'build'
if (commands[command] === undefined) {
if (fs.existsSync(path.resolve(command))) {
// TODO: Deprecate this in future versions
// Check if non-existing command, might be a file.
command = 'build'
} else {
help({
message: `Invalid command: ${command}`,
usage: ['tailwindcss <command> [options]'],
commands: Object.keys(commands)
.filter((command) => command !== 'build')
.map((command) => `${command} [options]`),
options: sharedFlags,
})
process.exit(1)
}
}
// Execute command
let { args: flags, run } = commands[command]
let args = (() => {
try {
let result = arg(
Object.fromEntries(
Object.entries({ ...flags, ...sharedFlags })
.filter(([_key, value]) => !value?.type?.manualParsing)
.map(([key, value]) => [key, typeof value === 'object' ? value.type : value])
),
{ permissive: true }
)
// Manual parsing of flags to allow for special flags like oneOf(Boolean, String)
for (let i = result['_'].length - 1; i >= 0; --i) {
let flag = result['_'][i]
if (!flag.startsWith('-')) continue
let [flagName, flagValue] = flag.split('=')
let handler = flags[flagName]
// Resolve flagName & handler
while (typeof handler === 'string') {
flagName = handler
handler = flags[handler]
}
if (!handler) continue
let args = []
let offset = i + 1
// --flag value syntax was used so we need to pull `value` from `args`
if (flagValue === undefined) {
// Parse args for current flag
while (result['_'][offset] && !result['_'][offset].startsWith('-')) {
args.push(result['_'][offset++])
}
// Cleanup manually parsed flags + args
result['_'].splice(i, 1 + args.length)
// No args were provided, use default value defined in handler
// One arg was provided, use that directly
// Multiple args were provided so pass them all in an array
flagValue = args.length === 0 ? undefined : args.length === 1 ? args[0] : args
} else {
// Remove the whole flag from the args array
result['_'].splice(i, 1)
}
// Set the resolved value in the `result` object
result[flagName] = handler.type(flagValue, flagName)
}
// Ensure that the `command` is always the first argument in the `args`.
// This is important so that we don't have to check if a default command
// (build) was used or not from within each plugin.
//
// E.g.: tailwindcss input.css -> _: ['build', 'input.css']
// E.g.: tailwindcss build input.css -> _: ['build', 'input.css']
if (result['_'][0] !== command) {
result['_'].unshift(command)
}
return result
} catch (err) {
if (err.code === 'ARG_UNKNOWN_OPTION') {
help({
message: err.message,
usage: ['tailwindcss <command> [options]'],
options: sharedFlags,
})
process.exit(1)
}
throw err
}
})()
if (args['--help']) {
help({
options: { ...flags, ...sharedFlags },
usage: [`tailwindcss ${command} [options]`],
})
process.exit(0)
}
run(args)

View file

@ -1,59 +0,0 @@
import fs from 'fs'
import path from 'path'
function isESM() {
const pkgPath = path.resolve('./package.json')
try {
let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
return pkg.type && pkg.type === 'module'
} catch (err) {
return false
}
}
export function init(args) {
let messages: string[] = []
let isProjectESM = args['--ts'] || args['--esm'] || isESM()
let syntax = args['--ts'] ? 'ts' : isProjectESM ? 'js' : 'cjs'
let extension = args['--ts'] ? 'ts' : 'js'
let tailwindConfigLocation = path.resolve(args['_'][1] ?? `./tailwind.config.${extension}`)
if (fs.existsSync(tailwindConfigLocation)) {
messages.push(`${path.basename(tailwindConfigLocation)} already exists.`)
} else {
let stubContentsFile = fs.readFileSync(
args['--full']
? path.resolve(__dirname, '../../../../stubs/config.full.js')
: path.resolve(__dirname, '../../../../stubs/config.simple.js'),
'utf8'
)
let stubFile = fs.readFileSync(
path.resolve(__dirname, `../../../../stubs/tailwind.config.${syntax}`),
'utf8'
)
// Change colors import
stubContentsFile = stubContentsFile.replace('../colors', 'tailwindcss/colors')
// Replace contents of {ts,js,cjs} file with the stub {simple,full}.
stubFile =
stubFile
.replace('__CONFIG__', stubContentsFile.replace('module.exports =', '').trim())
.trim() + '\n\n'
fs.writeFileSync(tailwindConfigLocation, stubFile, 'utf8')
messages.push(`Created Tailwind CSS config file: ${path.basename(tailwindConfigLocation)}`)
}
if (messages.length > 0) {
console.log()
for (let message of messages) {
console.log(message)
}
}
}

View file

@ -1 +0,0 @@
module.exports = require('../plugin.js')

View file

@ -1,7 +1,12 @@
import postcss from 'postcss'
import lightningcss, { Features } from 'lightningcss'
import browserslist from 'browserslist'
import setupTrackingContext from './lib/setupTrackingContext'
import processTailwindFeatures from './processTailwindFeatures'
import { env } from './lib/sharedState'
import { findAtConfigPath } from './lib/findAtConfigPath'
import { handleImportAtRules } from './lib/handleImportAtRules'
import { CssBasedConfig } from './lib/parseCssConfig'
module.exports = function tailwindcss(configOrPath) {
return {
@ -13,6 +18,7 @@ module.exports = function tailwindcss(configOrPath) {
console.time('JIT TOTAL')
return root
},
...handleImportAtRules(),
function (root, result) {
// Use the path for the `@config` directive if it exists, otherwise use the
// path for the file being processed
@ -34,66 +40,82 @@ module.exports = function tailwindcss(configOrPath) {
processTailwindFeatures(context)(root, result)
},
__OXIDE__ &&
function lightningCssPlugin(_root, result) {
let postcss = require('postcss')
let lightningcss = require('lightningcss')
let browserslist = require('browserslist')
function lightningCssPlugin(_root, result) {
let map = result.map ?? result.opts.map
try {
let transformed = lightningcss.transform({
filename: result.opts.from,
code: Buffer.from(result.root.toString()),
minify: false,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets:
typeof process !== 'undefined' && process.env.JEST_WORKER_ID
? { chrome: 106 << 16 }
: lightningcss.browserslistToTargets(
browserslist(require('../package.json').browserslist)
),
let intermediateResult = result.root.toResult({
map: map ? { inline: true } : false,
})
drafts: {
nesting: true,
customMedia: true,
},
})
let intermediateMap = intermediateResult.map?.toJSON?.() ?? map
result.map = Object.assign(result.map ?? {}, {
toJSON() {
return transformed.map.toJSON()
},
toString() {
return transformed.map.toString()
},
})
try {
let resolvedBrowsersListConfig = browserslist.findConfig(
result.opts.from ?? process.cwd()
)?.defaults
let defaultBrowsersListConfig = require('../package.json').browserslist
let browsersListConfig = resolvedBrowsersListConfig ?? defaultBrowsersListConfig
result.root = postcss.parse(transformed.code.toString('utf8'))
} catch (err) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
let lines = err.source.split('\n')
err = new Error(
[
'Error formatting using Lightning CSS:',
'',
...[
'```css',
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
...lines.slice(err.loc.line, err.loc.line + 2),
'```',
],
].join('\n')
)
let transformed = lightningcss.transform({
filename: result.opts.from,
code: Buffer.from(intermediateResult.css),
minify: false,
sourceMap: !!intermediateMap,
targets: lightningcss.browserslistToTargets(browserslist(browsersListConfig)),
drafts: {
nesting: true,
customMedia: true,
},
nonStandard: {
deepSelectorCombinator: true,
},
include: Features.Nesting,
exclude: Features.LogicalProperties,
})
let code = transformed.code.toString()
// https://postcss.org/api/#sourcemapoptions
if (intermediateMap && transformed.map != null) {
let prev = transformed.map.toString()
if (typeof intermediateMap === 'object') {
intermediateMap.prev = prev
} else {
code = `${code}\n/*# sourceMappingURL=data:application/json;base64,${Buffer.from(
prev
).toString('base64')} */`
}
if (Error.captureStackTrace) {
Error.captureStackTrace(err, lightningCssPlugin)
}
throw err
}
},
result.root = postcss.parse(code, {
...result.opts,
map: intermediateMap,
})
} catch (err) {
if (err.source && typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
let lines = err.source.split('\n')
err = new Error(
[
'Error formatting using Lightning CSS:',
'',
...[
'```css',
...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line),
' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(),
...lines.slice(err.loc.line, err.loc.line + 2),
'```',
],
].join('\n')
)
}
if (Error.captureStackTrace) {
Error.captureStackTrace(err, lightningCssPlugin)
}
throw err
}
},
env.DEBUG &&
function (root) {
console.timeEnd('JIT TOTAL')

View file

@ -7,7 +7,7 @@ import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
import collapseAdjacentRules from './lib/collapseAdjacentRules'
import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations'
import partitionApplyAtRules from './lib/partitionApplyAtRules'
import detectNesting from './lib/detectNesting'
// import detectNesting from './lib/detectNesting'
import { createContext } from './lib/setupContextUtils'
import { issueFlagNotices } from './featureFlags'
@ -15,7 +15,7 @@ export default function processTailwindFeatures(setupContext) {
return function (root, result) {
let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)
detectNesting()(root, result)
// detectNesting()(root, result)
// Partition apply rules that are found in the css
// itself.

View file

@ -49,10 +49,22 @@ export function normalize(value, isRoot = true) {
value = value.trim()
}
// Add spaces around operators inside math functions like calc() that do not follow an operator
// or '('.
value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
value = normalizeMathOperatorSpacing(value)
return value
}
/**
* Add spaces around operators inside math functions
* like calc() that do not follow an operator or '('.
*
* @param {string} value
* @returns {string}
*/
function normalizeMathOperatorSpacing(value) {
return value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
let vars = []
return match
.replace(/var\((--.+?)[,)]/g, (match, g1) => {
vars.push(g1)
@ -61,8 +73,6 @@ export function normalize(value, isRoot = true) {
.replace(/(-?\d*\.?\d(?!\b-\d.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g, '$1 $2 ')
.replace(placeholderRe, () => vars.shift())
})
return value
}
export function url(value) {
@ -178,11 +188,12 @@ export function image(value) {
}
let gradientTypes = new Set([
'conic-gradient',
'linear-gradient',
'radial-gradient',
'repeating-conic-gradient',
'repeating-linear-gradient',
'repeating-radial-gradient',
'conic-gradient',
])
export function gradient(value) {
value = normalize(value)

View file

@ -17,6 +17,30 @@ export function dim(input) {
}
export default {
group(key, cb) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return
if (key && alreadyShown.has(key)) return
if (key) alreadyShown.add(key)
console.warn('')
cb({
info(messages) {
messages.forEach((message) => console.warn(colors.bold(colors.cyan('info')), '-', message))
},
warn(messages) {
messages.forEach((message) =>
console.warn(colors.bold(colors.yellow('warn')), '-', message)
)
},
risk(messages) {
messages.forEach((message) =>
console.warn(colors.bold(colors.magenta('risk')), '-', message)
)
},
})
},
info(key, messages) {
log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key]))
},

View file

@ -1,4 +1,4 @@
import { flagEnabled } from '../featureFlags'
import { flagEnabled, featureFlags } from '../featureFlags'
import log, { dim } from './log'
export function normalizeConfig(config) {
@ -18,6 +18,10 @@ export function normalizeConfig(config) {
* }
*/
let valid = (() => {
if (config.content === 'auto') {
return true
}
// `config.purge` should not exist anymore
if (config.purge) {
return false
@ -196,11 +200,17 @@ export function normalizeConfig(config) {
files: (() => {
let { content, purge } = config
if (Array.isArray(purge)) return purge
if (Array.isArray(purge?.content)) return purge.content
if (content === undefined && purge === undefined) return []
if (purge) {
if (Array.isArray(purge)) return purge
if (Array.isArray(purge?.content)) return purge.content
return []
}
if (Array.isArray(content)) return content
if (Array.isArray(content?.content)) return content.content
if (Array.isArray(content?.files)) return content.files
if (content === 'auto') return ['auto']
return []
})(),
@ -284,16 +294,33 @@ export function normalizeConfig(config) {
})(),
}
// Force disable the `oxideParser` feature flag when using unsupported features.
// TODO: Remove once we have prefix or separator support in the oxide parser.
if (config.prefix !== '' || config.separator !== ':') {
if (config.experimental === 'all') {
config.experimental = {}
for (let key in featureFlags.experimental) {
config.experimental[key] = true
}
} else {
config.experimental = config.experimental ?? {}
}
config.experimental.oxideParser = false
}
// Validate globs to prevent bogus globs.
// E.g.: `./src/*.{html}` is invalid, the `{html}` should just be `html`
for (let file of config.content.files) {
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
log.warn('invalid-glob-braces', [
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
// TODO: Add https://tw.wtf/invalid-glob-braces
])
break
if (config.content.files !== 'auto') {
for (let file of config.content.files) {
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
log.warn('invalid-glob-braces', [
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
// TODO: Add https://tw.wtf/invalid-glob-braces
])
break
}
}
}

View file

@ -18,7 +18,6 @@ import {
} from './dataTypes'
import negateValue from './negateValue'
import { backgroundSize } from './validateFormalSyntax'
import { flagEnabled } from '../featureFlags.js'
/**
* @param {import('postcss-selector-parser').Container} selectors
@ -243,12 +242,9 @@ export function coerceValue(types, modifier, options, tailwindConfig) {
* @returns {Iterator<[value: string, type: string, modifier: string | null]>}
*/
export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
let [modifier, utilityModifier] = splitUtilityModifier(rawModifier)
let canUseUtilityModifier =
modifiersEnabled &&
options.modifiers != null &&
(options.modifiers === 'any' ||
(typeof options.modifiers === 'object' &&

View file

@ -19,40 +19,37 @@
// **Jumpable**
// Any terminal element may "jump" over combinators when moving to the end of the selector
//
// This is a backwards-compat quirk of :before and :after variants.
// This is a backwards-compat quirk of pseudo element variants from earlier versions of Tailwind CSS.
/** @typedef {'terminal' | 'actionable' | 'jumpable'} PseudoProperty */
/** @type {Record<string, PseudoProperty[]>} */
let elementProperties = {
// Pseudo elements from the spec
'::after': ['terminal', 'jumpable'],
'::backdrop': ['terminal'],
'::backdrop': ['terminal', 'jumpable'],
'::before': ['terminal', 'jumpable'],
'::cue': ['terminal'],
'::cue-region': ['terminal'],
'::first-letter': ['terminal', 'jumpable'],
'::first-line': ['terminal', 'jumpable'],
'::grammar-error': ['terminal'],
'::marker': ['terminal'],
'::marker': ['terminal', 'jumpable'],
'::part': ['terminal', 'actionable'],
'::placeholder': ['terminal'],
'::selection': ['terminal'],
'::placeholder': ['terminal', 'jumpable'],
'::selection': ['terminal', 'jumpable'],
'::slotted': ['terminal'],
'::spelling-error': ['terminal'],
'::target-text': ['terminal'],
// other
// Pseudo elements from the spec with special rules
'::file-selector-button': ['terminal', 'actionable'],
'::-webkit-progress-bar': ['terminal', 'actionable'],
// Webkit scroll bar pseudo elements can be combined with user-action pseudo classes
'::-webkit-scrollbar': ['terminal', 'actionable'],
'::-webkit-scrollbar-button': ['terminal', 'actionable'],
'::-webkit-scrollbar-thumb': ['terminal', 'actionable'],
'::-webkit-scrollbar-track': ['terminal', 'actionable'],
'::-webkit-scrollbar-track-piece': ['terminal', 'actionable'],
'::-webkit-scrollbar-corner': ['terminal', 'actionable'],
'::-webkit-resizer': ['terminal', 'actionable'],
// Library-specific pseudo elements used by component libraries
// These are Shadow DOM-like
'::deep': ['actionable'],
'::v-deep': ['actionable'],
'::ng-deep': ['actionable'],
// Note: As a rule, double colons (::) should be used instead of a single colon
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
@ -65,8 +62,8 @@ let elementProperties = {
// The default value is used when the pseudo-element is not recognized
// Because it's not recognized, we don't know if it's terminal or not
// So we assume it can't be moved AND can have user-action pseudo classes attached to it
__default__: ['actionable'],
// So we assume it can be moved AND can have user-action pseudo classes attached to it
__default__: ['terminal', 'actionable'],
}
/**

View file

@ -1,3 +1,4 @@
import pc from 'picocolors'
import log from './log'
export function validateConfig(config) {
@ -9,6 +10,15 @@ export function validateConfig(config) {
])
}
if (config.content.files.includes('auto')) {
log.group('auto-content-experimental', (log) => {
log.info([pc.bold('Automatically detecting Tailwind CSS content sources...')])
log.warn([
'Automatic content detection is experimental, and the behavior may change at any time.',
])
})
}
// Warn if the line-clamp plugin is installed
try {
let plugin = require('@tailwindcss/line-clamp')

22
src/value-parser/LICENSE Normal file
View file

@ -0,0 +1,22 @@
Copyright (c) Bogdan Chadkin <trysound@yandex.ru>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,3 @@
# postcss-value-parser (forked + inlined)
This is a customized version of of [PostCSS Value Parser](https://github.com/TrySound/postcss-value-parser) to fix some bugs around parsing CSS functions.

177
src/value-parser/index.d.ts vendored Normal file
View file

@ -0,0 +1,177 @@
declare namespace postcssValueParser {
interface BaseNode {
/**
* The offset, inclusive, inside the CSS value at which the node starts.
*/
sourceIndex: number
/**
* The offset, exclusive, inside the CSS value at which the node ends.
*/
sourceEndIndex: number
/**
* The node's characteristic value
*/
value: string
}
interface ClosableNode {
/**
* Whether the parsed CSS value ended before the node was properly closed
*/
unclosed?: true
}
interface AdjacentAwareNode {
/**
* The token at the start of the node
*/
before: string
/**
* The token at the end of the node
*/
after: string
}
interface CommentNode extends BaseNode, ClosableNode {
type: 'comment'
}
interface DivNode extends BaseNode, AdjacentAwareNode {
type: 'div'
}
interface FunctionNode extends BaseNode, ClosableNode, AdjacentAwareNode {
type: 'function'
/**
* Nodes inside the function
*/
nodes: Node[]
}
interface SpaceNode extends BaseNode {
type: 'space'
}
interface StringNode extends BaseNode, ClosableNode {
type: 'string'
/**
* The quote type delimiting the string
*/
quote: '"' | "'"
}
interface UnicodeRangeNode extends BaseNode {
type: 'unicode-range'
}
interface WordNode extends BaseNode {
type: 'word'
}
/**
* Any node parsed from a CSS value
*/
type Node =
| CommentNode
| DivNode
| FunctionNode
| SpaceNode
| StringNode
| UnicodeRangeNode
| WordNode
interface CustomStringifierCallback {
/**
* @param node The node to stringify
* @returns The serialized CSS representation of the node
*/
(nodes: Node): string | undefined
}
interface WalkCallback {
/**
* @param node The currently visited node
* @param index The index of the node in the series of parsed nodes
* @param nodes The series of parsed nodes
* @returns Returning `false` will prevent traversal of descendant nodes (only applies if `bubble` was set to `true` in the `walk()` call)
*/
(node: Node, index: number, nodes: Node[]): void | boolean
}
/**
* A CSS dimension, decomposed into its numeric and unit parts
*/
interface Dimension {
number: string
unit: string
}
/**
* A wrapper around a parsed CSS value that allows for inspecting and walking nodes
*/
interface ParsedValue {
/**
* The series of parsed nodes
*/
nodes: Node[]
/**
* Walk all parsed nodes, applying a callback
*
* @param callback A visitor callback that will be executed for each node
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
*/
walk(callback: WalkCallback, bubble?: boolean): this
}
interface ValueParser {
/**
* Decompose a CSS dimension into its numeric and unit part
*
* @param value The dimension to decompose
* @returns An object representing `number` and `unit` part of the dimension or `false` if the decomposing fails
*/
unit(value: string): Dimension | false
/**
* Serialize a series of nodes into a CSS value
*
* @param nodes The nodes to stringify
* @param custom A custom stringifier callback
* @returns The generated CSS value
*/
stringify(nodes: Node | Node[], custom?: CustomStringifierCallback): string
/**
* Walk a series of nodes, applying a callback
*
* @param nodes The nodes to walk
* @param callback A visitor callback that will be executed for each node
* @param bubble When set to `true`, walking will be done inside-out instead of outside-in
*/
walk(nodes: Node[], callback: WalkCallback, bubble?: boolean): void
/**
* Parse a CSS value into a series of nodes to operate on
*
* @param value The value to parse
*/
new (value: string): ParsedValue
/**
* Parse a CSS value into a series of nodes to operate on
*
* @param value The value to parse
*/
(value: string): ParsedValue
}
}
declare const postcssValueParser: postcssValueParser.ValueParser
export = postcssValueParser

28
src/value-parser/index.js Normal file
View file

@ -0,0 +1,28 @@
var parse = require('./parse')
var walk = require('./walk')
var stringify = require('./stringify')
function ValueParser(value) {
if (this instanceof ValueParser) {
this.nodes = parse(value)
return this
}
return new ValueParser(value)
}
ValueParser.prototype.toString = function () {
return Array.isArray(this.nodes) ? stringify(this.nodes) : ''
}
ValueParser.prototype.walk = function (cb, bubble) {
walk(this.nodes, cb, bubble)
return this
}
ValueParser.unit = require('./unit')
ValueParser.walk = walk
ValueParser.stringify = stringify
module.exports = ValueParser

Some files were not shown because too many files have changed in this diff Show more