Merge branch 'main' into feat/ohos-support
This commit is contained in:
commit
ac52bfe80e
92 changed files with 4787 additions and 1634 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -50,7 +50,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
|
|
|
|||
2
.github/workflows/integration-tests.yml
vendored
2
.github/workflows/integration-tests.yml
vendored
|
|
@ -58,7 +58,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
|
|
|||
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
|
|
@ -15,7 +15,7 @@ on:
|
|||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '9.6.0'
|
||||
PNPM_VERSION: '11.9.0'
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
permissions:
|
||||
|
|
@ -79,7 +79,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ jobs:
|
|||
run: |
|
||||
echo "TAG_NAME=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
|
|
|
|||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
|
@ -22,7 +22,7 @@ permissions:
|
|||
env:
|
||||
APP_NAME: tailwindcss-oxide
|
||||
NODE_VERSION: 24
|
||||
PNPM_VERSION: '9.6.0'
|
||||
PNPM_VERSION: '11.9.0'
|
||||
OXIDE_LOCATION: ./crates/node
|
||||
|
||||
concurrency:
|
||||
|
|
@ -83,7 +83,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ jobs:
|
|||
fetch-depth: 20
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
|
|
@ -365,7 +365,7 @@ jobs:
|
|||
run: |
|
||||
pnpm --recursive --filter="!@tailwindcss/oxide-wasm32-wasi" publish --tag ${RELEASE_CHANNEL} --no-git-checks
|
||||
# The wasm package needs a special npm config that isn't read when pnpm --recursive is used
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks; popd;
|
||||
pushd crates/node/npm/wasm32-wasi; pnpm publish --tag ${RELEASE_CHANNEL} --no-git-checks --config.node-linker=hoisted; popd;
|
||||
|
||||
- name: Trigger Tailwind Play update
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
|
|
@ -378,3 +378,38 @@ jobs:
|
|||
ref: 'main',
|
||||
workflow_id: 'upgrade-tailwindcss.yml'
|
||||
})
|
||||
|
||||
notify:
|
||||
if: ${{ always() && (needs.build.result == 'failure' || needs.build-freebsd.result == 'failure' || needs.release.result == 'failure') }}
|
||||
needs:
|
||||
- build
|
||||
- build-freebsd
|
||||
- release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve release label
|
||||
id: release
|
||||
env:
|
||||
INPUT_CHANNEL: ${{ github.event.inputs.channel || '' }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name || '' }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
tag_name="${RELEASE_TAG:-${GITHUB_REF_NAME}}"
|
||||
echo "label=release ${tag_name}" >> $GITHUB_OUTPUT
|
||||
elif [[ "$INPUT_CHANNEL" == "release" ]]; then
|
||||
version=$(node -p "require('./packages/tailwindcss/package.json').version")
|
||||
echo "label=release v${version}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
sha_short=$(git rev-parse --short HEAD)
|
||||
echo "label=insiders release 0.0.0-insiders.${sha_short}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify Discord
|
||||
uses: discord-actions/message@5c7149c81a83146e5d01f142be1bf87a61831c4d # v2
|
||||
with:
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
message: 'The [most recent ${{ github.workflow }} workflow](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) for `${{ steps.release.outputs.label }}` has failed.'
|
||||
|
|
|
|||
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -7,21 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Nothing yet!
|
||||
|
||||
- Support bare spacing values for `auto-rows-*` and `auto-cols-*` utilities (e.g. `auto-rows-12` and `auto-cols-16`) ([#20229](https://github.com/tailwindlabs/tailwindcss/pull/20229))
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode doesn't crash on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242))
|
||||
- Ensure `@tailwindcss/vite` doesn't crash in Deno v2.8.x when `context.parentURL` is not a valid URL ([#20245](https://github.com/tailwindlabs/tailwindcss/pull/20245))
|
||||
- Support `--watch --poll[=ms]` in `@tailwindcss/cli` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297))
|
||||
- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298))
|
||||
- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
|
||||
- Ensure `theme('colors.foo')` in JS plugins resolves correctly when both `--color-foo` and `--color-foo-bar` exist ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
|
||||
- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
|
||||
- Parse selectors like `[data-foo]div` as two selectors instead of one ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303))
|
||||
- Ensure `@tailwindcss/postcss` rebuilds when a preprocessor like Sass changes the input CSS without changing the input file on disk ([#20310](https://github.com/tailwindlabs/tailwindcss/pull/20310))
|
||||
- Ensure CSS nesting is handled even when Lightning CSS isn't run, such as in `@tailwindcss/browser` and Tailwind Play ([#20124](https://github.com/tailwindlabs/tailwindcss/pull/20124))
|
||||
- Prevent achromatic theme colors from shifting hue when mixed in polar color spaces like `oklch` ([#20314](https://github.com/tailwindlabs/tailwindcss/pull/20314))
|
||||
- Ensure `--spacing(0)` is optimized to `0px` instead of `0` so it remains a `<length>` when used in `calc(…)` ([#20319](https://github.com/tailwindlabs/tailwindcss/pull/20319))
|
||||
- Load `@parcel/watcher` only when needed in `@tailwindcss/cli --watch` mode, so one-off builds and `--watch --poll` work when `@parcel/watcher` can't be loaded ([#20325](https://github.com/tailwindlabs/tailwindcss/pull/20325))
|
||||
- Use explicit platform fonts instead of `system-ui` and `ui-sans-serif` so CJK text respects the page's `lang` attribute on Windows ([#20318](https://github.com/tailwindlabs/tailwindcss/pull/20318))
|
||||
- Prevent `@tailwindcss/upgrade` from rewriting ignored files when run from a subdirectory ([#20329](https://github.com/tailwindlabs/tailwindcss/pull/20329))
|
||||
- Ensure earlier `@source` rules pointing to nested files are scanned when later `@source` rules point to files in parent folders ([#20335](https://github.com/tailwindlabs/tailwindcss/pull/20335))
|
||||
- Prevent `@tailwindcss/vite` from triggering full page reloads when scanned files are processed by Vite but haven't been loaded as modules yet ([#20336](https://github.com/tailwindlabs/tailwindcss/pull/20336))
|
||||
|
||||
## [4.3.2] - 2026-06-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support bare spacing values for `auto-rows-*` and `auto-cols-*` utilities (e.g. `auto-rows-12` and `auto-cols-16`) ([#20229](https://github.com/tailwindlabs/tailwindcss/pull/20229))
|
||||
- Prevent `@tailwindcss/cli` in `--watch` mode from crashing on Windows when `@source` points to a directory that doesn't exist ([#20242](https://github.com/tailwindlabs/tailwindcss/pull/20242))
|
||||
- Prevent `@tailwindcss/vite` from crashing in Deno v2.8.x when `context.parentURL` is not a valid URL ([#20245](https://github.com/tailwindlabs/tailwindcss/pull/20245))
|
||||
- Ensure `@tailwindcss/cli` in `--watch` mode rebuilds when the input CSS file changes in an ignored directory ([#20246](https://github.com/tailwindlabs/tailwindcss/pull/20246))
|
||||
- Ensure `@variant` rules generated by `addBase` can use custom variants defined later ([#20247](https://github.com/tailwindlabs/tailwindcss/pull/20247))
|
||||
- Ensure `@tailwindcss/vite` doesn't crash during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259))
|
||||
- Ensure `text-[--spacing(…)]` generates `font-size` instead of `color` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260))
|
||||
- Allow `@variant` rules used in `addBase(…)` to use custom variants defined later ([#20247](https://github.com/tailwindlabs/tailwindcss/pull/20247))
|
||||
- Prevent `@tailwindcss/vite` from crashing during HMR when scanned files or directories are deleted ([#20259](https://github.com/tailwindlabs/tailwindcss/pull/20259))
|
||||
- Generate `font-size` instead of `color` declarations for `text-[--spacing(…)]` ([#20260](https://github.com/tailwindlabs/tailwindcss/pull/20260))
|
||||
- Prevent `@source` patterns from scanning unrelated sibling files and folders ([#20263](https://github.com/tailwindlabs/tailwindcss/pull/20263))
|
||||
- Extract class candidates adjacent to Template Toolkit delimiters like `%]…[%` in `.tt`, `.tt2`, and `.tx` files ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
|
||||
- Extract class candidates from conditional Maud syntax like `p.text-black[condition]` ([#20269](https://github.com/tailwindlabs/tailwindcss/pull/20269))
|
||||
- Prevent `@position-try` rules from triggering unknown at-rule warnings when optimizing CSS ([#20277](https://github.com/tailwindlabs/tailwindcss/pull/20277))
|
||||
- Support class suggestions for named opacity modifiers from `--opacity` theme values ([#20287](https://github.com/tailwindlabs/tailwindcss/pull/20287))
|
||||
- Prevent type errors in `@tailwindcss/postcss` when used with newer PostCSS patch releases ([#20289](https://github.com/tailwindlabs/tailwindcss/pull/20289))
|
||||
|
||||
## [4.3.1] - 2026-06-12
|
||||
|
||||
|
|
@ -4071,7 +4095,9 @@ No release notes
|
|||
|
||||
- Everything!
|
||||
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.1...HEAD
|
||||
[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.3...HEAD
|
||||
[4.3.3]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.2...v4.3.3
|
||||
[4.3.2]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.1...v4.3.2
|
||||
[4.3.1]: https://github.com/tailwindlabs/tailwindcss/compare/v4.3.0...v4.3.1
|
||||
[4.3.0]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.4...v4.3.0
|
||||
[4.2.4]: https://github.com/tailwindlabs/tailwindcss/compare/v4.2.3...v4.2.4
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm-eabi",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-android-arm64",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-arm64",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-darwin-x64",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-freebsd-x64",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-gnu",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-arm64-musl",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-gnu",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-linux-x64-musl",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
node-linker=hoisted
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-wasm32-wasi",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-arm64-msvc",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide-win32-x64-msvc",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/oxide",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
|
||||
|
|
|
|||
|
|
@ -165,6 +165,11 @@ impl Scanner {
|
|||
self.scanner.get_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn scanned_files(&self) -> Vec<String> {
|
||||
self.scanner.get_scanned_files()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn globs(&mut self) -> Vec<GlobEntry> {
|
||||
self
|
||||
|
|
|
|||
|
|
@ -78,11 +78,12 @@ pub struct Scanner {
|
|||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
|
||||
/// Track mtimes for files so re-scans can skip unchanged files.
|
||||
/// Only populated after the first scan completes (to avoid unnecessary
|
||||
/// metadata calls on initial build).
|
||||
/// Track mtimes for files so incremental scans can skip unchanged files.
|
||||
mtimes: FxHashMap<PathBuf, SystemTime>,
|
||||
|
||||
/// Files that were scanned during the last `scan()` call.
|
||||
scanned_files: Vec<String>,
|
||||
|
||||
/// Whether we've completed at least one full scan. When false, we skip
|
||||
/// mtime tracking entirely so the initial build stays fast.
|
||||
has_scanned_once: bool,
|
||||
|
|
@ -122,9 +123,10 @@ impl Scanner {
|
|||
pub fn scan(&mut self) -> Vec<String> {
|
||||
self.sources_scanned = false;
|
||||
|
||||
let (scanned_blobs, css_files) = self.discover_sources();
|
||||
let (scanned_blobs, css_files, files) = self.discover_sources();
|
||||
|
||||
self.extract_candidates(scanned_blobs, css_files);
|
||||
self.scanned_files = files;
|
||||
|
||||
// Return all candidates sorted
|
||||
let mut result = self.candidates.iter().cloned().collect::<Vec<_>>();
|
||||
|
|
@ -256,6 +258,11 @@ impl Scanner {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_scanned_files(&self) -> Vec<String> {
|
||||
self.scanned_files.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
|
||||
if let Some(globs) = &self.globs {
|
||||
|
|
@ -353,14 +360,14 @@ impl Scanner {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>) {
|
||||
fn discover_sources(&mut self) -> (Vec<Vec<u8>>, Vec<PathBuf>, Vec<String>) {
|
||||
if self.sources_scanned {
|
||||
return (vec![], vec![]);
|
||||
return (vec![], vec![], vec![]);
|
||||
}
|
||||
self.sources_scanned = true;
|
||||
|
||||
let Some(walker) = &mut self.walker else {
|
||||
return (vec![], vec![]);
|
||||
return (vec![], vec![], vec![]);
|
||||
};
|
||||
|
||||
// Use synchronous walk for the initial build (lower overhead) and parallel
|
||||
|
|
@ -372,7 +379,8 @@ impl Scanner {
|
|||
};
|
||||
|
||||
let mut css_files: Vec<PathBuf> = vec![];
|
||||
let mut content_paths: Vec<(PathBuf, String)> = Vec::new();
|
||||
let mut content_paths: Vec<(PathBuf, String)> = vec![];
|
||||
let mut changed_files = vec![];
|
||||
|
||||
// Fresh state
|
||||
self.files.clear();
|
||||
|
|
@ -380,7 +388,7 @@ impl Scanner {
|
|||
self.extensions.clear();
|
||||
self.globs = None;
|
||||
|
||||
for (path, is_dir, extension) in all_entries {
|
||||
for (path, is_dir, extension, mtime) in all_entries {
|
||||
if is_dir {
|
||||
self.dirs.insert(path);
|
||||
} else {
|
||||
|
|
@ -390,13 +398,10 @@ impl Scanner {
|
|||
}
|
||||
self.extensions.insert(extension.clone());
|
||||
|
||||
// On re-scans, check mtime to skip unchanged files.
|
||||
// On the first scan we skip this entirely to avoid extra
|
||||
// metadata syscalls.
|
||||
// On incremental scans, check mtime to skip unchanged files.
|
||||
// On the first scan, track mtimes while still scanning every file.
|
||||
let changed = if self.has_scanned_once {
|
||||
let current_mtime = path.metadata().ok().and_then(|m| m.modified().ok());
|
||||
|
||||
match current_mtime {
|
||||
match mtime {
|
||||
Some(mtime) => {
|
||||
let prev = self.mtimes.insert(path.clone(), mtime);
|
||||
prev.is_none_or(|prev| prev != mtime)
|
||||
|
|
@ -404,6 +409,10 @@ impl Scanner {
|
|||
None => true,
|
||||
}
|
||||
} else {
|
||||
if let Some(mtime) = mtime {
|
||||
self.mtimes.insert(path.clone(), mtime);
|
||||
}
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
|
|
@ -411,6 +420,10 @@ impl Scanner {
|
|||
continue;
|
||||
}
|
||||
|
||||
if let Ok(file) = path.clone().into_os_string().into_string() {
|
||||
changed_files.push(file);
|
||||
}
|
||||
|
||||
match extension.as_str() {
|
||||
// Special handing for CSS files, we don't want to extract candidates from
|
||||
// these files, but we do want to extract used CSS variables.
|
||||
|
|
@ -442,7 +455,9 @@ impl Scanner {
|
|||
self.has_scanned_once = true;
|
||||
}
|
||||
|
||||
(scanned_blobs, css_files)
|
||||
changed_files.par_sort_unstable();
|
||||
|
||||
(scanned_blobs, css_files, changed_files)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,27 +562,28 @@ where
|
|||
.collect()
|
||||
}
|
||||
|
||||
type WalkEntry = (PathBuf, bool, String);
|
||||
type WalkEntry = (PathBuf, bool, String, Option<SystemTime>);
|
||||
|
||||
/// Walk the file system synchronously. Used for the initial build where the overhead of spawning
|
||||
/// parallel walker threads is not worth it.
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn walk_synchronous(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
||||
let mut entries = Vec::new();
|
||||
let mut entries = vec![];
|
||||
|
||||
for entry in walker.build().filter_map(Result::ok) {
|
||||
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
|
||||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
entries.push((path, true, String::new()));
|
||||
entries.push((path, true, String::new(), None));
|
||||
} else {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
entries.push((path, false, ext));
|
||||
let mtime = path.metadata().ok().and_then(|m| m.modified().ok());
|
||||
entries.push((path, false, ext, mtime));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,7 +607,7 @@ fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
|||
}
|
||||
}
|
||||
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let collected: Arc<Mutex<Vec<WalkEntry>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
walker.build_parallel().run(|| {
|
||||
let mut buf = FlushOnDrop {
|
||||
|
|
@ -608,14 +624,15 @@ fn walk_parallel(walker: &mut WalkBuilder) -> Vec<WalkEntry> {
|
|||
let path = entry.into_path();
|
||||
|
||||
if is_dir {
|
||||
buf.local.push((path, true, String::new()));
|
||||
buf.local.push((path, true, String::new(), None));
|
||||
} else {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|x| x.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
buf.local.push((path, false, ext));
|
||||
let mtime = path.metadata().ok().and_then(|m| m.modified().ok());
|
||||
buf.local.push((path, false, ext, mtime));
|
||||
}
|
||||
|
||||
if buf.local.len() >= 256 {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,16 @@ fn expand_restricted_patterns(sources: Vec<SourceEntry>) -> Vec<SourceEntry> {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bases of restricted patterns. Each of these becomes its own walk root with its own
|
||||
// `*` + `!<pattern>` rules, so an ancestor base must not ignore them recursively.
|
||||
let pattern_roots = sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
SourceEntry::Pattern { base, .. } => Some(base.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut restricted_roots: FxHashSet<PathBuf> = FxHashSet::default();
|
||||
let mut expanded = vec![];
|
||||
|
||||
|
|
@ -167,14 +177,17 @@ fn expand_restricted_patterns(sources: Vec<SourceEntry>) -> Vec<SourceEntry> {
|
|||
}
|
||||
|
||||
// Ignore everything in the directory. We will later add the specific patterns we are
|
||||
// interested in. When another source root is nested in this base, only ignore direct
|
||||
// children so the nested source can still be walked from its own root.
|
||||
// interested in.
|
||||
if restricted_roots.insert(base.clone()) {
|
||||
let pattern = if unrestricted_roots.iter().any(|root| root.starts_with(base)) {
|
||||
"/*"
|
||||
} else {
|
||||
"*"
|
||||
};
|
||||
// When another source root is nested inside this base — an unrestricted root, or the
|
||||
// base of another restricted pattern (which is walked from its own root with its own
|
||||
// rules) — only ignore direct children so the nested root can still be walked.
|
||||
let has_nested_root = unrestricted_roots.iter().any(|root| root.starts_with(base))
|
||||
|| pattern_roots
|
||||
.iter()
|
||||
.any(|root| root != base && root.starts_with(base));
|
||||
|
||||
let pattern = if has_nested_root { "/*" } else { "*" };
|
||||
|
||||
expanded.push(SourceEntry::Ignored {
|
||||
base: base.clone(),
|
||||
|
|
|
|||
|
|
@ -106,6 +106,18 @@ mod scanner {
|
|||
globs
|
||||
}
|
||||
|
||||
fn normalize_files(files: Vec<String>, base: &Path) -> Vec<String> {
|
||||
let base_dir =
|
||||
format!("{}{}", dunce::canonicalize(base).unwrap().display(), "/").replace('\\', "/");
|
||||
|
||||
let mut files = files
|
||||
.iter()
|
||||
.map(|file| file.replace('\\', "/").replace(&base_dir, ""))
|
||||
.collect::<Vec<_>>();
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
|
||||
fn scan_with_globs(
|
||||
paths_with_content: &[(&str, &str)],
|
||||
source_directives: Vec<&str>,
|
||||
|
|
@ -1045,6 +1057,53 @@ mod scanner {
|
|||
assert!(!globs.iter().any(|glob| glob.starts_with("src/**/*")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_track_files_scanned_by_the_last_scan() {
|
||||
let dir = tempdir().unwrap().into_path();
|
||||
|
||||
let _ = Command::new("git").arg("init").current_dir(&dir).output();
|
||||
|
||||
create_files_in(
|
||||
&dir,
|
||||
&[
|
||||
("src/index.html", "content-['src/index.html']"),
|
||||
("src/keep.html", "content-['src/keep.html']"),
|
||||
],
|
||||
);
|
||||
|
||||
let mut scanner = Scanner::new(vec![public_source_entry_from_pattern(
|
||||
dir.clone(),
|
||||
"@source '**/*'",
|
||||
)]);
|
||||
|
||||
assert_eq!(
|
||||
scanner.scan(),
|
||||
vec!["content-['src/index.html']", "content-['src/keep.html']"]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scanner.scan(),
|
||||
vec!["content-['src/index.html']", "content-['src/keep.html']"]
|
||||
);
|
||||
assert_eq!(scanner.get_scanned_files(), Vec::<String>::new());
|
||||
|
||||
sleep(Duration::from_millis(10));
|
||||
fs::write(dir.join("src/index.html"), "content-['src/changed.html']").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
scanner.scan(),
|
||||
vec![
|
||||
"content-['src/changed.html']",
|
||||
"content-['src/index.html']",
|
||||
"content-['src/keep.html']",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_files(scanner.get_scanned_files(), &dir),
|
||||
vec!["src/index.html"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_ignore_negated_custom_sources() {
|
||||
let ScanResult {
|
||||
|
|
@ -1238,6 +1297,67 @@ mod scanner {
|
|||
assert_eq!(files, vec!["src/bar.html", "src/foo.html"]);
|
||||
}
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20333
|
||||
#[test]
|
||||
fn it_should_combine_nested_and_root_restricted_sources() {
|
||||
// The restriction (`*`) added for the root-level file must not prevent walking into the
|
||||
// `nested` folder that another explicit source points into. At the same time, relaxing
|
||||
// that restriction to `/*` must not accidentally open up sibling folders (`ignore-me`)
|
||||
// or files that no explicit source points at.
|
||||
let paths_with_content = &[
|
||||
("nested/component.html", "content-['nested/component.html']"),
|
||||
("nested/ignore-me.html", "content-['nested/ignore-me.html']"),
|
||||
("ignore-me/component.html", "content-['ignore-me']"),
|
||||
("component-sources.classes.txt", "content-['classes.txt']"),
|
||||
("ignore-me.txt", "content-['ignore-me.txt']"),
|
||||
];
|
||||
|
||||
let ScanResult {
|
||||
candidates, files, ..
|
||||
} = scan_with_globs(
|
||||
paths_with_content,
|
||||
vec![
|
||||
"@source './nested/component.html'",
|
||||
"@source './component-sources.classes.txt'",
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['classes.txt']",
|
||||
"content-['nested/component.html']"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
files,
|
||||
vec!["component-sources.classes.txt", "nested/component.html"]
|
||||
);
|
||||
|
||||
// Same setup, but with the root-level source declared first
|
||||
let ScanResult {
|
||||
candidates, files, ..
|
||||
} = scan_with_globs(
|
||||
paths_with_content,
|
||||
vec![
|
||||
"@source './component-sources.classes.txt'",
|
||||
"@source './nested/component.html'",
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
vec![
|
||||
"content-['classes.txt']",
|
||||
"content-['nested/component.html']"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
files,
|
||||
vec!["component-sources.classes.txt", "nested/component.html"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_allow_later_ignores_to_override_restricted_sources() {
|
||||
let ScanResult {
|
||||
|
|
|
|||
|
|
@ -375,6 +375,40 @@ describe.each([
|
|||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'watch mode with polling',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "workspace:^",
|
||||
"@tailwindcss/cli": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss/utilities';`,
|
||||
'src/index.html': html`
|
||||
<div class="underline"></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ fs, spawn }) => {
|
||||
let process = await spawn(
|
||||
`${command} --input src/index.css --output dist/out.css --watch --poll=50`,
|
||||
)
|
||||
await process.onStderr((m) => m.includes('Done in'))
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`underline`])
|
||||
|
||||
await fs.write('src/index.html', html`
|
||||
<div class="underline flex"></div>
|
||||
`)
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
"watch mode with unknown @source paths shouldn't crash on Windows",
|
||||
{
|
||||
|
|
@ -639,6 +673,15 @@ describe.each([
|
|||
'git ignore files outside of a repo are not considered',
|
||||
{
|
||||
fs: {
|
||||
// Make the test root a pnpm workspace that includes `home/project` so
|
||||
// the install below resolves the transitive dependency overrides that
|
||||
// the test harness injects into this `pnpm-workspace.yaml`.
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- home/project
|
||||
`,
|
||||
|
||||
// Ignore everything in the "home" directory
|
||||
'home/.gitignore': '*',
|
||||
|
||||
|
|
@ -676,7 +719,7 @@ describe.each([
|
|||
installDependencies: false,
|
||||
},
|
||||
async ({ fs, root, exec }) => {
|
||||
await exec(`pnpm install --ignore-workspace`, {
|
||||
await exec(`pnpm install`, {
|
||||
cwd: path.join(root, 'home/project'),
|
||||
})
|
||||
|
||||
|
|
@ -2788,8 +2831,8 @@ test(
|
|||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
|
|
@ -2808,7 +2851,7 @@ test(
|
|||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
|
@ -2860,7 +2903,7 @@ test(
|
|||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
:-moz-focusring {
|
||||
:-moz-focusring:where(:not(iframe)) {
|
||||
outline: auto;
|
||||
}
|
||||
progress {
|
||||
|
|
|
|||
|
|
@ -109,13 +109,11 @@ test(
|
|||
expect(await fs.dumpFiles('dist/out.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/out.css ---
|
||||
.prose-custom {
|
||||
:where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
--x: 1;
|
||||
@media (width >= 640px) {
|
||||
&.custom {
|
||||
--x: 2;
|
||||
}
|
||||
.prose-custom :where(hr):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
--x: 1;
|
||||
@media (width >= 640px) {
|
||||
&.custom {
|
||||
--x: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"devDependencies": {
|
||||
"dedent": "catalog:",
|
||||
"fast-glob": "^3.3.3",
|
||||
"source-map-js": "^1.2.1"
|
||||
"source-map-js": "^1.2.1",
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ fs, exec, expect }) => {
|
||||
let output = await exec('npx @tailwindcss/upgrade')
|
||||
let output = await exec('pnpm exec upgrade')
|
||||
expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.')
|
||||
|
||||
// Files should not be modified
|
||||
|
|
@ -100,7 +100,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -158,7 +158,7 @@ test(
|
|||
})
|
||||
|
||||
// Ensure the v4 project compiles correctly
|
||||
await exec('npx tailwindcss --input src/input.css --output dist/out.css')
|
||||
await exec('pnpm exec tailwindcss --input src/input.css --output dist/out.css')
|
||||
|
||||
await fs.expectFileToContain('dist/out.css', [
|
||||
candidate`flex!`,
|
||||
|
|
@ -202,9 +202,10 @@ test(
|
|||
`,
|
||||
'notes/unrelated.txt': `order-[0] bg-[--my-red]`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./**/*.{html,php,txt}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -262,7 +263,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -334,7 +335,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -406,7 +407,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -483,7 +484,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -586,7 +587,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -694,7 +695,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
await fs.expectFileToContain(
|
||||
'postcss.config.js',
|
||||
|
|
@ -765,7 +766,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
let packageJsonContent = await fs.read('package.json')
|
||||
let packageJson = JSON.parse(packageJsonContent)
|
||||
|
|
@ -812,7 +813,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
let packageJsonContent = await fs.read('package.json')
|
||||
let packageJson = JSON.parse(packageJsonContent)
|
||||
|
|
@ -864,7 +865,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
|
|
@ -940,7 +941,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
|
||||
await fs.expectFileToContain(
|
||||
|
|
@ -1007,7 +1008,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1054,7 +1055,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1098,7 +1099,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1168,9 +1169,10 @@ test(
|
|||
}
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1279,9 +1281,10 @@ test(
|
|||
}
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1414,7 +1417,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
let output = await exec('npx @tailwindcss/upgrade --force')
|
||||
let output = await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(output).toMatch(
|
||||
/You have one or more stylesheets that are imported into a utility layer and non-utility layer./,
|
||||
|
|
@ -1538,7 +1541,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1763,8 +1766,8 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch(
|
||||
(e) => e.toString(),
|
||||
let output = await exec('pnpm exec upgrade --force', {}, { ignoreStdErr: true }).catch((e) =>
|
||||
e.toString(),
|
||||
)
|
||||
|
||||
expect(output).toMatch('Could not determine configuration file for:')
|
||||
|
|
@ -1835,7 +1838,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1971,7 +1974,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2091,7 +2094,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2171,7 +2174,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2253,9 +2256,10 @@ test(
|
|||
}
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2309,7 +2313,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
let pkg = JSON.parse(await fs.read('package.json'))
|
||||
|
||||
|
|
@ -2388,7 +2392,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html}')).toMatchInlineSnapshot(`
|
||||
|
|
@ -2486,7 +2490,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade --force')
|
||||
await exec('pnpm exec upgrade --force')
|
||||
|
||||
// Files should not be modified
|
||||
expect(await fs.dumpFiles('./*.{js,css,html,tsx}')).toMatchInlineSnapshot(`
|
||||
|
|
@ -2593,7 +2597,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade ./src/index.css')
|
||||
await exec('pnpm exec upgrade ./src/index.css')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2719,7 +2723,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2732,8 +2736,8 @@ test(
|
|||
text-red-500/10
|
||||
text-red-500/semitransparent
|
||||
text-red-500/transparent
|
||||
text-red-500/50
|
||||
text-red-500/50.5
|
||||
text-red-500/semitransparent
|
||||
text-red-500/50.50
|
||||
text-red-500/50.50
|
||||
text-red-500/50%
|
||||
text-red-500/100%"
|
||||
|
|
@ -2805,7 +2809,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2873,7 +2877,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, root, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
let before = await fs.dumpFiles('./src/**/*.{css,html}')
|
||||
expect(before).toMatchInlineSnapshot(`
|
||||
|
|
@ -2915,7 +2919,7 @@ test(
|
|||
}
|
||||
|
||||
// Run the upgrade again
|
||||
let output = await exec('npx @tailwindcss/upgrade')
|
||||
let output = await exec('pnpm exec upgrade')
|
||||
expect(output).toContain('No changes were made to your repository')
|
||||
|
||||
let after = await fs.dumpFiles('./src/**/*.{css,html}')
|
||||
|
|
@ -2994,7 +2998,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -3043,7 +3047,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/{*,.env,.env.*}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -3114,7 +3118,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./{src,templates}/**/*')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -3190,6 +3194,10 @@ test(
|
|||
let originalKeepFile = await fs.read('src/keep.php')
|
||||
let originalTemplate = await fs.read('src/templates/template-0.php')
|
||||
|
||||
// NOTE: This test intentionally keeps `npx` instead of `pnpm exec`. It uses
|
||||
// a `NODE_OPTIONS` hook that overrides `fs.writeFile` to simulate a bad
|
||||
// write, and `pnpm exec` may write a bin shim on startup which would trip
|
||||
// that hook before template migration begins.
|
||||
let process = await spawn('npx @tailwindcss/upgrade --force', {
|
||||
env: {
|
||||
NODE_OPTIONS: '--require=./hook.cjs',
|
||||
|
|
@ -3257,7 +3265,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
let stdout = await exec('npx @tailwindcss/upgrade', {
|
||||
let stdout = await exec('pnpm exec upgrade', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
|
||||
|
|
@ -3331,7 +3339,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
let stdout = await exec('npx @tailwindcss/upgrade')
|
||||
let stdout = await exec('pnpm exec upgrade')
|
||||
|
||||
expect(stdout).not.toContain(
|
||||
'Running this command will add the dependency to the workspace root',
|
||||
|
|
@ -3363,6 +3371,61 @@ test(
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20328
|
||||
test(
|
||||
'ignored files should not be touched when upgrading from a nested directory',
|
||||
{
|
||||
fs: {
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- packages/*
|
||||
`,
|
||||
'package.json': json`
|
||||
{
|
||||
"name": "root",
|
||||
"private": true
|
||||
}
|
||||
`,
|
||||
'packages/css/package.json': json`
|
||||
{
|
||||
"name": "css-pkg",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4",
|
||||
"@tailwindcss/upgrade": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'packages/css/src/input.css': css`
|
||||
@import 'tailwindcss/utilities.css' layer(utilities) source(none);
|
||||
`,
|
||||
'packages/css/src/index.html': html`<div class="flex text-red-500">Hi</div>`,
|
||||
|
||||
// Ensure files/folders ignored by a `.gitignore` in the root take effect
|
||||
// when executing the upgrade tool from a sub-package.
|
||||
'.gitignore': txt`
|
||||
node_modules/
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
await exec('git init', { cwd: root })
|
||||
|
||||
await exec('pnpm exec upgrade --force', {
|
||||
cwd: path.join(root, 'packages/css'),
|
||||
})
|
||||
|
||||
expect(await fs.dumpFiles('packages/css/node_modules/tailwindcss/utilities.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- packages/css/node_modules/tailwindcss/utilities.css ---
|
||||
@tailwind utilities;
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'upgrade <style> blocks carefully',
|
||||
{
|
||||
|
|
@ -3407,7 +3470,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('./src/**/*.{css,vue}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -370,7 +370,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(
|
||||
await fs.dumpFiles(
|
||||
|
|
@ -484,7 +484,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -586,7 +586,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -594,17 +594,17 @@ test(
|
|||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-gray-50: oklch(98.5% 0 0);
|
||||
--color-gray-100: oklch(97% 0 0);
|
||||
--color-gray-200: oklch(92.2% 0 0);
|
||||
--color-gray-300: oklch(87% 0 0);
|
||||
--color-gray-400: oklch(70.8% 0 0);
|
||||
--color-gray-500: oklch(55.6% 0 0);
|
||||
--color-gray-600: oklch(43.9% 0 0);
|
||||
--color-gray-700: oklch(37.1% 0 0);
|
||||
--color-gray-800: oklch(26.9% 0 0);
|
||||
--color-gray-900: oklch(20.5% 0 0);
|
||||
--color-gray-950: oklch(14.5% 0 0);
|
||||
--color-gray-50: oklch(98.5% 0 none);
|
||||
--color-gray-100: oklch(97% 0 none);
|
||||
--color-gray-200: oklch(92.2% 0 none);
|
||||
--color-gray-300: oklch(87% 0 none);
|
||||
--color-gray-400: oklch(70.8% 0 none);
|
||||
--color-gray-500: oklch(55.6% 0 none);
|
||||
--color-gray-600: oklch(43.9% 0 none);
|
||||
--color-gray-700: oklch(37.1% 0 none);
|
||||
--color-gray-800: oklch(26.9% 0 none);
|
||||
--color-gray-900: oklch(20.5% 0 none);
|
||||
--color-gray-950: oklch(14.5% 0 none);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -672,7 +672,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -750,7 +750,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -824,7 +824,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -934,7 +934,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1032,7 +1032,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ root, exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade', {
|
||||
await exec('pnpm exec upgrade', {
|
||||
cwd: path.join(root, 'frontend'),
|
||||
})
|
||||
|
||||
|
|
@ -1119,7 +1119,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1194,7 +1194,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1270,7 +1270,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1346,7 +1346,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1408,7 +1408,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1470,7 +1470,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1509,7 +1509,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1580,7 +1580,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1683,7 +1683,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1793,7 +1793,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1895,7 +1895,7 @@ describe('border compatibility', () => {
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -1993,7 +1993,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2064,7 +2064,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
@ -2141,7 +2141,7 @@ test(
|
|||
},
|
||||
},
|
||||
async ({ exec, fs, expect }) => {
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ test(
|
|||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Ensure we are in a git repo
|
||||
|
|
@ -39,7 +40,7 @@ test(
|
|||
await exec('git commit -m "before migration"')
|
||||
|
||||
// Fully upgrade to v4
|
||||
await exec('npx @tailwindcss/upgrade')
|
||||
await exec('pnpm exec upgrade')
|
||||
|
||||
// Undo all changes to the current repo. This will bring the repo back to a
|
||||
// v3 state, but the `node_modules` will now have v4 installed.
|
||||
|
|
@ -47,7 +48,7 @@ test(
|
|||
|
||||
// Re-running the upgrade should result in an error
|
||||
return expect(() => {
|
||||
return exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
return exec('pnpm exec upgrade', {}, { ignoreStdErr: true }).catch((e) => {
|
||||
// Replacing the current version with a hardcoded `v4` to make it stable
|
||||
// when we release new minor/patch versions.
|
||||
return Promise.reject(
|
||||
|
|
@ -55,7 +56,7 @@ test(
|
|||
)
|
||||
})
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Command failed: npx @tailwindcss/upgrade
|
||||
"Command failed: pnpm exec upgrade
|
||||
≈ tailwindcss v4.0.0
|
||||
|
||||
│ ↳ Upgrading from Tailwind CSS \`v4.0.0\`
|
||||
|
|
@ -105,6 +106,7 @@ test(
|
|||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Use `bun` to install dependencies
|
||||
|
|
@ -192,6 +194,7 @@ test(
|
|||
@tailwind utilities;
|
||||
`,
|
||||
},
|
||||
retry: 0,
|
||||
},
|
||||
async ({ exec, expect }) => {
|
||||
// Use `npm` to install dependencies
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import path from 'node:path'
|
|||
import { promisify, stripVTControlCharacters } from 'node:util'
|
||||
import { RawSourceMap, SourceMapConsumer } from 'source-map-js'
|
||||
import { test as defaultTest, type ExpectStatic } from 'vitest'
|
||||
import * as Yaml from 'yaml'
|
||||
import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table'
|
||||
import { escape } from '../packages/tailwindcss/src/utils/escape'
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..')
|
||||
const ROOT_PNPM_WORKSPACE = Yaml.parse(
|
||||
await fs.readFile(path.join(REPO_ROOT, 'pnpm-workspace.yaml'), 'utf8'),
|
||||
)
|
||||
const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) =>
|
||||
name.replace('tailwindcss-', '@tailwindcss/').replace('.tgz', ''),
|
||||
)
|
||||
|
|
@ -40,6 +44,7 @@ interface TestConfig {
|
|||
|
||||
timeout?: number
|
||||
installDependencies?: boolean
|
||||
retry?: number
|
||||
}
|
||||
interface TestContext {
|
||||
root: string
|
||||
|
|
@ -94,7 +99,7 @@ export function test(
|
|||
name,
|
||||
{
|
||||
timeout: config.timeout ?? TEST_TIMEOUT,
|
||||
retry: process.env.CI ? 2 : 0,
|
||||
retry: config.retry ?? (process.env.CI ? 2 : 0),
|
||||
only: only || (!process.env.CI && debug),
|
||||
skip,
|
||||
concurrent,
|
||||
|
|
@ -121,6 +126,39 @@ export function test(
|
|||
execOptions: ExecOptions = {},
|
||||
) {
|
||||
let cwd = childProcessOptions.cwd ?? root
|
||||
let originalCommand = command
|
||||
|
||||
// Avoid `pnpm exec upgrade` on Windows because the upgrader runs
|
||||
// `pnpm add`, which rewrites Windows `.cmd` shims while the current
|
||||
// shim is still executing.
|
||||
//
|
||||
// Pretty sure this is a pnpm v11 bug on Windows.
|
||||
if (IS_WINDOWS && command.includes('pnpm exec upgrade')) {
|
||||
for (let base = cwd; ; base = path.dirname(base)) {
|
||||
let upgradeBin = path.join(
|
||||
base,
|
||||
'node_modules',
|
||||
'@tailwindcss',
|
||||
'upgrade',
|
||||
'dist',
|
||||
'index.mjs',
|
||||
)
|
||||
|
||||
try {
|
||||
await fs.access(upgradeBin)
|
||||
command = command.replace('pnpm exec upgrade', `node "${upgradeBin}"`)
|
||||
break
|
||||
} catch (error: any) {
|
||||
if (error?.code !== 'ENOENT') throw error
|
||||
}
|
||||
|
||||
let parent = path.dirname(base)
|
||||
if (parent === base) {
|
||||
throw new Error(`Unable to resolve @tailwindcss/upgrade from ${cwd}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug && cwd !== root) {
|
||||
let relative = path.relative(root, cwd)
|
||||
if (relative[0] !== '.') relative = `./${relative}`
|
||||
|
|
@ -140,6 +178,11 @@ export function test(
|
|||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
if (command !== originalCommand) {
|
||||
error.message = error.message.replace(command, originalCommand)
|
||||
let mappedError = error as any
|
||||
mappedError.cmd = originalCommand
|
||||
}
|
||||
if (execOptions.ignoreStdErr !== true) console.error(stderr)
|
||||
if (only || debug) {
|
||||
console.error(stdout)
|
||||
|
|
@ -306,6 +349,8 @@ export function test(
|
|||
|
||||
if (filename.endsWith('package.json')) {
|
||||
content = await overwriteVersionsInPackageJson(content)
|
||||
} else if (filename.endsWith('pnpm-workspace.yaml')) {
|
||||
content = overwriteVersionsInPnpmWorkspace(content)
|
||||
}
|
||||
|
||||
// Ensure that files written on Windows use \r\n line ending
|
||||
|
|
@ -424,6 +469,21 @@ export function test(
|
|||
node_modules/
|
||||
`
|
||||
|
||||
// Disable Git's automatic line ending conversion in these throwaway test
|
||||
// repositories. Tools like pnpm write files (e.g. `pnpm-lock.yaml`) with
|
||||
// LF endings, which on Windows (where `core.autocrlf=true` is common)
|
||||
// makes Git emit noisy "LF will be replaced by CRLF" warnings when it
|
||||
// touches those files.
|
||||
config.fs['.gitattributes'] ??= txt`
|
||||
* -text
|
||||
`
|
||||
|
||||
// Ensure there is always a root `pnpm-workspace.yaml` so that transitive
|
||||
// dependency overrides (and the build allow list) are injected into it.
|
||||
// As of pnpm v10, these can no longer live in the `pnpm.overrides` field
|
||||
// of `package.json`.
|
||||
config.fs['pnpm-workspace.yaml'] ??= ''
|
||||
|
||||
for (let [filename, content] of Object.entries(config.fs)) {
|
||||
if (content.toString().startsWith('symlink:')) {
|
||||
// The symlink path is relative to the target destination's path
|
||||
|
|
@ -441,14 +501,12 @@ export function test(
|
|||
let shouldInstallDependencies = config.installDependencies ?? true
|
||||
|
||||
try {
|
||||
// In debug mode, the directory is going to be inside the pnpm workspace
|
||||
// of the tailwindcss package. This means that `pnpm install` will run
|
||||
// pnpm install on the workspace instead (expect if the root dir defines
|
||||
// a separate workspace). We work around this by using the
|
||||
// `--ignore-workspace` flag.
|
||||
// Every test project gets its own root `pnpm-workspace.yaml` (see
|
||||
// above). This makes the test directory its own workspace root, so even
|
||||
// in debug mode — where the directory lives inside this repository's
|
||||
// pnpm workspace — `pnpm install` won't attach to the parent workspace.
|
||||
if (shouldInstallDependencies) {
|
||||
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
|
||||
await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
|
||||
await context.exec(`pnpm install`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
|
|
@ -526,28 +584,42 @@ async function overwriteVersionsInPackageJson(content: string): Promise<string>
|
|||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(json, null, 2)
|
||||
}
|
||||
|
||||
function overwriteVersionsInPnpmWorkspace(content: string): string {
|
||||
let workspace = content.trim() === '' ? {} : Yaml.parse(content)
|
||||
|
||||
// Inherit information from the root workspace
|
||||
workspace.allowBuilds = {
|
||||
...ROOT_PNPM_WORKSPACE.allowBuilds,
|
||||
...workspace.allowBuilds,
|
||||
}
|
||||
|
||||
// Inject transitive dependency overwrite. This is necessary because
|
||||
// @tailwindcss/vite internally depends on a specific version of
|
||||
// @tailwindcss/oxide and we instead want to resolve it to the locally built
|
||||
// version.
|
||||
json.pnpm ||= {}
|
||||
json.pnpm.overrides ||= {}
|
||||
//
|
||||
// As of pnpm v10, the `pnpm.overrides` field in `package.json` is no longer
|
||||
// read. Overrides have to be defined in `pnpm-workspace.yaml` instead.
|
||||
workspace.overrides ||= {}
|
||||
for (let pkg of PUBLIC_PACKAGES) {
|
||||
if (pkg === 'tailwindcss') {
|
||||
// We want to be explicit about the `tailwindcss` package so our tests can
|
||||
// also import v3 without conflicting v4 tarballs.
|
||||
json.pnpm.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
|
||||
json.pnpm.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/node>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
|
||||
} else {
|
||||
json.pnpm.overrides[pkg] = resolveVersion(pkg)
|
||||
workspace.overrides[pkg] = resolveVersion(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(json, null, 2)
|
||||
return Yaml.stringify(workspace)
|
||||
}
|
||||
|
||||
function resolveVersion(dependency: string) {
|
||||
|
|
|
|||
|
|
@ -584,6 +584,233 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
|
|||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20320
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19903
|
||||
test(
|
||||
'editing scanned files that Vite can process as modules does not trigger a full reload',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
{
|
||||
// A plugin that processes a custom file type into a JS
|
||||
// module, similar to e.g. \`.vue\` or \`.svelte\` files
|
||||
name: 'custom-file-type',
|
||||
transform(code, id) {
|
||||
if (id.endsWith('.custom')) {
|
||||
return { code: 'export default ' + JSON.stringify(code), map: null }
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// Log all HMR payloads to a file so the test can assert on
|
||||
// them
|
||||
name: 'hmr-wiretap',
|
||||
configureServer(server) {
|
||||
let logFile = path.resolve('hmr.log')
|
||||
fs.writeFileSync(logFile, '')
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
let send = environment.hot.send.bind(environment.hot)
|
||||
environment.hot.send = (payload) => {
|
||||
fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n')
|
||||
return send(payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'project-a/src/main.ts': ts`
|
||||
import compA from './comp-a.custom'
|
||||
import snippet from './snippet.php?raw'
|
||||
console.log(compA, snippet)
|
||||
`,
|
||||
'project-a/src/snippet.php': html`
|
||||
<div
|
||||
class="content-['src/snippet.php']"
|
||||
></div>
|
||||
`,
|
||||
'project-a/src/comp-a.custom': html`
|
||||
<div
|
||||
class="content-['src/comp-a.custom']"
|
||||
></div>
|
||||
`,
|
||||
'project-a/src/comp-b.custom': html`
|
||||
<div
|
||||
class="content-['src/comp-b.custom']"
|
||||
></div>
|
||||
`,
|
||||
'project-a/src/lazy.tsx': jsx`
|
||||
export default function Lazy() {
|
||||
return <div className="content-['src/lazy.tsx']" />
|
||||
}
|
||||
`,
|
||||
'project-a/src/unimported.css': css`
|
||||
.unimported {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss';
|
||||
@source '../../project-b/**/*.php';
|
||||
`,
|
||||
'project-b/src/index.php': html`
|
||||
<div
|
||||
class="content-['project-b/src/index.php']"
|
||||
></div>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm vite dev', {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['src/lazy.tsx']`)
|
||||
expect(styles).toContain(candidate`content-['src/comp-b.custom']`)
|
||||
expect(styles).toContain(candidate`content-['project-b/src/index.php']`)
|
||||
})
|
||||
|
||||
// Load `main.ts`, `comp-a.custom`, and `snippet.php?raw` as real
|
||||
// modules, like a browser visiting the page would
|
||||
await fetch(`${url}/src/main.ts`)
|
||||
await fetch(`${url}/src/comp-a.custom?import`)
|
||||
await fetch(`${url}/src/snippet.php?raw`)
|
||||
|
||||
// Changing a scanned `.tsx` file that is not part of the module graph
|
||||
// (e.g. a lazily-loaded route that hasn't been visited yet) should not
|
||||
// trigger a full reload, but new classes should still apply
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20320
|
||||
{
|
||||
await fs.write(
|
||||
'project-a/src/lazy.tsx',
|
||||
jsx`
|
||||
export default function Lazy() {
|
||||
return <div className="content-['updated:src/lazy.tsx']" />
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/lazy.tsx']`)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// The same holds for a custom file type as long as some file of the
|
||||
// same type was processed as a module before
|
||||
{
|
||||
await fs.write(
|
||||
'project-a/src/comp-b.custom',
|
||||
html`<div class="content-['updated:src/comp-b.custom']"></div>`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/comp-b.custom']`)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing a scanned stylesheet that is not part of the module graph
|
||||
// (e.g. a component stylesheet that a framework plugin compiles into
|
||||
// the component) should not trigger a full reload either
|
||||
//
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/19903
|
||||
{
|
||||
let updates = (await fs.read('project-a/hmr.log')).split('"type":"update"').length
|
||||
|
||||
await fs.write(
|
||||
'project-a/src/unimported.css',
|
||||
css`
|
||||
.unimported {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
// Wait until the change was handled and an update was pushed
|
||||
await retryAssertion(async () => {
|
||||
let log = await fs.read('project-a/hmr.log')
|
||||
expect(log.split('"type":"update"').length).toBeGreaterThan(updates)
|
||||
})
|
||||
expect(await fs.read('project-a/hmr.log')).not.toContain('full-reload')
|
||||
}
|
||||
|
||||
// Changing an external file (e.g. a PHP template) should still trigger
|
||||
// a full reload. This must work even though `snippet.php` is part of
|
||||
// the module graph via the `?raw` import: a query import only pulls
|
||||
// the file's contents into the graph (and creates an untransformed
|
||||
// module node for the underlying file), it is not evidence that Vite
|
||||
// processes `.php` files as modules.
|
||||
{
|
||||
await fs.write(
|
||||
'project-b/src/index.php',
|
||||
html`<div class="content-['updated:project-b/src/index.php']"></div>`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
expect(await fs.read('project-a/hmr.log')).toContain('full-reload')
|
||||
})
|
||||
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:project-b/src/index.php']`)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`source(none) disables looking at the module graph`,
|
||||
{
|
||||
|
|
@ -1090,6 +1317,14 @@ test(
|
|||
</head>
|
||||
`,
|
||||
'src/index.js': js`import Plotly from 'plotly.js/lib/core'`,
|
||||
|
||||
// `plotly.js` pulls in `es5-ext`, which has a build script. As of pnpm
|
||||
// v10, build scripts only run when explicitly allowed.
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
allowBuilds:
|
||||
es5-ext: true
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ exec, expect, fs }) => {
|
||||
|
|
@ -1137,7 +1372,7 @@ test(
|
|||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="hover:flex">Hello, world!</div>
|
||||
<div class="focus:text-[black]">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -1149,16 +1384,16 @@ test(
|
|||
async ({ exec, expect, fs }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
// Should not be minified when optimize is disabled
|
||||
let content = await fs.read(filename)
|
||||
expect(content).toContain('.hover\\:flex {')
|
||||
expect(content).toContain('&:hover {')
|
||||
expect(content).toContain('@media (hover: hover) {')
|
||||
expect(content).toContain('display: flex;')
|
||||
expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/assets/index-<hash>.css ---
|
||||
.focus\\:text-\\[black\\]:focus {
|
||||
color: black;
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1192,7 +1427,7 @@ test(
|
|||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="hover:flex">Hello, world!</div>
|
||||
<div class="hover:text-[black]">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'src/index.css': css`
|
||||
|
|
@ -1204,15 +1439,17 @@ test(
|
|||
async ({ exec, expect, fs }) => {
|
||||
await exec('pnpm vite build')
|
||||
|
||||
let files = await fs.glob('dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
// Should be optimized but not minified
|
||||
let content = await fs.read(filename)
|
||||
expect(content).toContain('@media (hover: hover) {')
|
||||
expect(content).toContain('.hover\\:flex:hover {')
|
||||
expect(content).toContain('display: flex;')
|
||||
expect((await fs.dumpFiles('dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
--- dist/assets/index-<hash>.css ---
|
||||
@media (hover: hover) {
|
||||
.hover\\:text-\\[black\\]:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
retryAssertion,
|
||||
test,
|
||||
ts,
|
||||
yaml,
|
||||
} from '../utils'
|
||||
|
||||
const SETUP = {
|
||||
|
|
@ -21,14 +22,14 @@ const SETUP = {
|
|||
"nitropack": "2.11.0",
|
||||
"tailwindcss": "workspace:^",
|
||||
"vue": "latest"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"nuxi": "3.28.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
overrides:
|
||||
nuxi: 3.28.0
|
||||
`,
|
||||
'nuxt.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ test(
|
|||
await exec('pnpm vite build')
|
||||
|
||||
expect(
|
||||
(await fs.dumpFiles('./dist/**/*.css')).replace(/-([_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'),
|
||||
(await fs.dumpFiles('./dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./dist/assets/index-<hash>.css ---
|
||||
|
|
@ -643,7 +643,7 @@ test(
|
|||
await exec('pnpm vite build')
|
||||
|
||||
expect(
|
||||
(await fs.dumpFiles('./dist/**/*.css')).replace(/-([_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'),
|
||||
(await fs.dumpFiles('./dist/**/*.css')).replace(/-([-_a-zA-Z0-9]*?)\.css/g, '-<hash>.css'),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
--- ./dist/assets/index-<hash>.css ---
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { stripVTControlCharacters } from 'node:util'
|
||||
import { candidate, css, html, json, test, ts } from '../utils'
|
||||
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
|
||||
|
||||
test(
|
||||
'production build',
|
||||
|
|
@ -248,3 +248,133 @@ test(
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20320
|
||||
test(
|
||||
'editing a scanned `.vue` file that is not loaded as a module does not trigger a full reload',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"vue": "^3.4.37",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6",
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"vite": "^8"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'vite.config.ts': ts`
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss(),
|
||||
{
|
||||
// Log update and full-reload HMR payloads to a file so the
|
||||
// test can assert on them. Custom events are not logged
|
||||
// because \`@vitejs/plugin-vue\` sends a \`file-changed\` event
|
||||
// for every file change, including changes to the log file
|
||||
// itself, which would cause an infinite feedback loop.
|
||||
name: 'hmr-wiretap',
|
||||
configureServer(server) {
|
||||
let logFile = path.resolve('hmr.log')
|
||||
fs.writeFileSync(logFile, '')
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
let send = environment.hot.send.bind(environment.hot)
|
||||
environment.hot.send = (payload) => {
|
||||
if (payload.type === 'update' || payload.type === 'full-reload') {
|
||||
fs.appendFileSync(logFile, JSON.stringify(payload) + '\\n')
|
||||
}
|
||||
return send(payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
`,
|
||||
'index.html': html`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
'src/index.css': css`@import 'tailwindcss';`,
|
||||
'src/main.ts': ts`
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
`,
|
||||
'src/App.vue': html`
|
||||
<template>
|
||||
<div class="content-['src/App.vue']">Hello Vue!</div>
|
||||
</template>
|
||||
`,
|
||||
|
||||
// This file is scanned by Tailwind but never imported, so it is not
|
||||
// part of the loaded module graph (e.g. a lazy route that hasn't been
|
||||
// visited yet)
|
||||
'src/LazyRoute.vue': html`
|
||||
<template>
|
||||
<div class="content-['src/LazyRoute.vue']">Lazy</div>
|
||||
</template>
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm vite dev')
|
||||
await process.onStdout((m) => m.includes('ready in'))
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)\//.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['src/App.vue']`)
|
||||
expect(styles).toContain(candidate`content-['src/LazyRoute.vue']`)
|
||||
})
|
||||
|
||||
// Load `main.ts` and `App.vue` as real modules, like a browser visiting
|
||||
// the page would
|
||||
await fetch(`${url}/src/main.ts`)
|
||||
await fetch(`${url}/src/App.vue`)
|
||||
|
||||
// Changing the scanned but unloaded `.vue` file should not trigger a
|
||||
// full reload, but new classes should still apply
|
||||
await fs.write(
|
||||
'src/LazyRoute.vue',
|
||||
html`
|
||||
<template>
|
||||
<div class="content-['updated:src/LazyRoute.vue']">Lazy</div>
|
||||
</template>
|
||||
`,
|
||||
)
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let styles = await fetchStyles(url, '/index.html')
|
||||
expect(styles).toContain(candidate`content-['updated:src/LazyRoute.vue']`)
|
||||
})
|
||||
expect(await fs.read('hmr.log')).not.toContain('full-reload')
|
||||
},
|
||||
)
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@types/node": "catalog:",
|
||||
"postcss": "catalog:",
|
||||
"postcss-import": "^16.1.1",
|
||||
|
|
@ -58,13 +58,7 @@
|
|||
"tsup": "^8.5.1",
|
||||
"turbo": "^2.9.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.7"
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@parcel/watcher@2.5.1": "patches/@parcel__watcher@2.5.1.patch",
|
||||
"lightningcss@1.32.0": "patches/lightningcss@1.32.0.patch"
|
||||
}
|
||||
}
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/browser",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.global.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/cli",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import watcher from '@parcel/watcher'
|
||||
import {
|
||||
compile,
|
||||
env,
|
||||
|
|
@ -21,11 +20,13 @@ import {
|
|||
highlight,
|
||||
println,
|
||||
relative,
|
||||
wordWrap,
|
||||
} from '../../utils/renderer'
|
||||
import { drainStdin, outputFile } from './utils'
|
||||
|
||||
const css = String.raw
|
||||
const DEBUG = env.DEBUG
|
||||
const DEFAULT_POLL_INTERVAL_MS = 250
|
||||
|
||||
export function options() {
|
||||
return {
|
||||
|
|
@ -47,6 +48,12 @@ export function options() {
|
|||
alias: '-w',
|
||||
values: ['always'],
|
||||
},
|
||||
'--poll': {
|
||||
type: 'boolean | number',
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
default: false,
|
||||
values: ['ms'],
|
||||
},
|
||||
'--minify': {
|
||||
type: 'boolean',
|
||||
description: 'Optimize and minify the output',
|
||||
|
|
@ -73,17 +80,44 @@ export function options() {
|
|||
} satisfies Arg
|
||||
}
|
||||
|
||||
function formatError(error: unknown): string {
|
||||
let seen = new Set() // Track seen errors to prevent circular errors
|
||||
function render(err: unknown, depth: number): string[] {
|
||||
let indent = ' '.repeat(depth)
|
||||
let width = (process.stderr.columns ?? Infinity) - 2 * (depth + 1)
|
||||
|
||||
let output = [
|
||||
`${indent}${red(depth === 0 ? 'Error:' : 'Caused by:')}`,
|
||||
`${indent}${dim('\u250C')}`,
|
||||
]
|
||||
|
||||
for (let line of `${err}`.split('\n')) {
|
||||
let wrapped = wordWrap(line, width)
|
||||
if (wrapped.length === 0) wrapped = ['']
|
||||
for (let chunk of wrapped) {
|
||||
output.push(`${indent}${dim('\u2502')} ${chunk}`)
|
||||
}
|
||||
}
|
||||
|
||||
output.push(`${indent}${dim('\u2514')}`)
|
||||
|
||||
if (typeof err === 'object' && err !== null && 'cause' in err && err.cause != null) {
|
||||
if (!seen.add(err).has(err.cause)) {
|
||||
output.push(...render(err.cause, depth + 1))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
return render(error, 0).join('\n')
|
||||
}
|
||||
|
||||
async function handleError<T>(fn: () => T): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
eprintln(
|
||||
[red('Error:'), dim('\u250C')]
|
||||
.concat(`${err}`.split('\n').map((line) => `${dim('\u2502')} ${line}`))
|
||||
.concat(dim('\u2514'))
|
||||
.join('\n'),
|
||||
)
|
||||
|
||||
eprintln(formatError(err))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +163,17 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// If the user passes `{bin} build --map -` then this likely means they want to output the map inline
|
||||
// this is the default behavior of `{bin build} --map` to inform the user of that
|
||||
if (args['--map'] === '-') {
|
||||
eprintln(`Use --map without a value to inline the source map`)
|
||||
eprintln(`Use --map without a value to inline the source map.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (args['--poll'] === undefined) {
|
||||
eprintln(`Use --poll with a non-zero value in milliseconds.`)
|
||||
process.exit(1)
|
||||
}
|
||||
let pollInterval = args['--poll'] === true ? DEFAULT_POLL_INTERVAL_MS : args['--poll']
|
||||
if (pollInterval !== false && pollInterval <= 0) {
|
||||
eprintln(`Specified polling interval must be a positive number.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +196,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
let previous = {
|
||||
css: '',
|
||||
optimizedCss: '',
|
||||
output: null as string | null,
|
||||
}
|
||||
|
||||
async function write(
|
||||
|
|
@ -208,13 +253,18 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
}
|
||||
}
|
||||
|
||||
let didChange = output !== previous.output
|
||||
|
||||
DEBUG && I.start('Write output')
|
||||
if (args['--output'] && args['--output'] !== '-') {
|
||||
await outputFile(args['--output'], output)
|
||||
} else {
|
||||
} else if (didChange) {
|
||||
println(output)
|
||||
}
|
||||
DEBUG && I.end('Write output')
|
||||
|
||||
previous.output = output
|
||||
return didChange
|
||||
}
|
||||
|
||||
let inputFilePath =
|
||||
|
|
@ -275,10 +325,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
}
|
||||
|
||||
let [compiler, scanner] = await handleError(() => createCompiler(input, I))
|
||||
let cleanupWatchers: (() => Promise<void>)[] = []
|
||||
|
||||
// Watch for changes
|
||||
if (args['--watch']) {
|
||||
let cleanupWatchers: (() => Promise<void>)[] = []
|
||||
if (args['--watch'] && pollInterval === false) {
|
||||
// Ensure the file watcher can be loaded before setting up any watchers,
|
||||
// such that we can present a helpful error message if needed.
|
||||
await handleError(() => loadWatcher())
|
||||
|
||||
cleanupWatchers.push(
|
||||
await createWatchers(await watchDirectories(scanner), async function handle(files) {
|
||||
try {
|
||||
|
|
@ -292,36 +346,15 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// Re-compile the input
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
let changedFiles: ChangedContent[] = []
|
||||
let rebuildStrategy: 'incremental' | 'full' = 'incremental'
|
||||
|
||||
let resolvedFullRebuildPaths = fullRebuildPaths
|
||||
|
||||
for (let file of files) {
|
||||
// If one of the changed files is related to the input CSS or JS
|
||||
// config/plugin files, then we need to do a full rebuild because
|
||||
// the theme might have changed.
|
||||
if (resolvedFullRebuildPaths.includes(file)) {
|
||||
rebuildStrategy = 'full'
|
||||
|
||||
// No need to check the rest of the events, because we already know we
|
||||
// need to do a full rebuild.
|
||||
break
|
||||
}
|
||||
|
||||
// Track new and updated files for incremental rebuilds.
|
||||
changedFiles.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
} satisfies ChangedContent)
|
||||
}
|
||||
let rebuildStrategy = getRebuildStrategy(files, resolvedFullRebuildPaths)
|
||||
|
||||
// Track the compiled CSS
|
||||
let compiledCss = ''
|
||||
let compiledMap: SourceMap | null = null
|
||||
|
||||
// Scan the entire `base` directory for full rebuilds.
|
||||
if (rebuildStrategy === 'full') {
|
||||
if (rebuildStrategy.kind === 'full') {
|
||||
// Read the new `input`.
|
||||
let input = args['--input']
|
||||
? args['--input'] === '-'
|
||||
|
|
@ -378,9 +411,9 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
}
|
||||
|
||||
// Scan changed files only for incremental rebuilds.
|
||||
else if (rebuildStrategy === 'incremental') {
|
||||
else if (rebuildStrategy.kind === 'incremental') {
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let newCandidates = scanner.scanFiles(changedFiles)
|
||||
let newCandidates = scanner.scanFiles(rebuildStrategy.changedFiles)
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
// No new candidates found which means we don't need to write to
|
||||
|
|
@ -443,12 +476,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
|
||||
// Catch any errors and print them to stderr, but don't exit the process
|
||||
// and keep watching.
|
||||
eprintln(
|
||||
[red('Error:'), dim('\u250C')]
|
||||
.concat(`${err}`.split('\n').map((line) => `${dim('\u2502')} ${line}`))
|
||||
.concat(dim('\u2514'))
|
||||
.join('\n'),
|
||||
)
|
||||
eprintln(formatError(err))
|
||||
|
||||
let end = process.hrtime.bigint()
|
||||
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
|
|
@ -490,9 +518,184 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
|
||||
let end = process.hrtime.bigint()
|
||||
if (!args['--silent']) eprintln(`Done in ${formatDuration(end - start)}`)
|
||||
|
||||
// Watch for changes, using polling
|
||||
if (args['--watch'] && pollInterval !== false) {
|
||||
let cleanupPollingIndicator = () => {}
|
||||
|
||||
function logPollingMessage(message: string) {
|
||||
if (!args['--silent']) {
|
||||
process.stderr.write('\r\x1B[2K')
|
||||
}
|
||||
|
||||
eprintln(message)
|
||||
|
||||
if (!args['--silent']) {
|
||||
process.stderr.write(`\r\x1B[2K${dim(`Polling for changes…`)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function fullRebuild(I: Instrumentation) {
|
||||
clearRequireCache(fullRebuildPaths)
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
fullRebuildPaths = inputFilePath ? [inputFilePath] : []
|
||||
|
||||
let input = args['--input']
|
||||
? args['--input'] === '-'
|
||||
? await drainStdin()
|
||||
: await fs.readFile(args['--input'], 'utf-8')
|
||||
: css`
|
||||
@import 'tailwindcss';
|
||||
`
|
||||
|
||||
;[compiler, scanner] = await createCompiler(input, I)
|
||||
backupRebuildPaths = fullRebuildPaths.slice()
|
||||
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let candidates = scanner.scan()
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
DEBUG && I.start('Build CSS')
|
||||
let output = compiler.build(candidates)
|
||||
DEBUG && I.end('Build CSS')
|
||||
|
||||
let map: SourceMap | null = null
|
||||
|
||||
if (args['--map']) {
|
||||
DEBUG && I.start('Build Source Map')
|
||||
map = toSourceMap(compiler.buildSourceMap())
|
||||
DEBUG && I.end('Build Source Map')
|
||||
}
|
||||
|
||||
await write(output, map, args, I)
|
||||
}
|
||||
|
||||
if (!args['--silent']) {
|
||||
let restored = false
|
||||
|
||||
function restoreCursor() {
|
||||
if (restored) return
|
||||
restored = true
|
||||
process.stderr.write('\r\x1B[2K\x1B[?25h')
|
||||
process.off('exit', restoreCursor)
|
||||
process.off('SIGINT', onSigint)
|
||||
process.off('SIGTERM', onSigterm)
|
||||
}
|
||||
|
||||
function onSigint() {
|
||||
restoreCursor()
|
||||
process.exit(130)
|
||||
}
|
||||
|
||||
function onSigterm() {
|
||||
restoreCursor()
|
||||
process.exit(143)
|
||||
}
|
||||
|
||||
process.stderr.write('\x1B[?25l')
|
||||
process.on('exit', restoreCursor)
|
||||
process.on('SIGINT', onSigint)
|
||||
process.on('SIGTERM', onSigterm)
|
||||
|
||||
cleanupPollingIndicator = restoreCursor
|
||||
cleanupWatchers.push(async () => cleanupPollingIndicator())
|
||||
}
|
||||
|
||||
cleanupWatchers.push(
|
||||
createPollingWatcher(async () => {
|
||||
using I = new Instrumentation()
|
||||
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
let candidates = scanner.scan()
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
let files = scanner.scannedFiles.filter(
|
||||
(file) => file !== args['--output'] && file !== args['--map'],
|
||||
)
|
||||
|
||||
if (files.length <= 0) return
|
||||
|
||||
let start = process.hrtime.bigint()
|
||||
|
||||
let strategy = getRebuildStrategy(files, fullRebuildPaths)
|
||||
|
||||
if (strategy.kind === 'full') {
|
||||
try {
|
||||
await fullRebuild(I)
|
||||
} catch (err) {
|
||||
fullRebuildPaths = backupRebuildPaths
|
||||
|
||||
logPollingMessage(formatError(err))
|
||||
}
|
||||
|
||||
if (!args['--silent']) {
|
||||
logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// No candidates found which means we don't need to write to disk, and
|
||||
// can return early.
|
||||
if (candidates.length <= 0) {
|
||||
if (!args['--silent']) {
|
||||
logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DEBUG && I.start('Build CSS')
|
||||
let output = compiler.build(candidates)
|
||||
DEBUG && I.end('Build CSS')
|
||||
|
||||
let map: SourceMap | null = null
|
||||
|
||||
if (args['--map']) {
|
||||
DEBUG && I.start('Build Source Map')
|
||||
map = toSourceMap(compiler.buildSourceMap())
|
||||
DEBUG && I.end('Build Source Map')
|
||||
}
|
||||
|
||||
await write(output, map, args, I)
|
||||
|
||||
if (!args['--silent']) {
|
||||
logPollingMessage(`Done in ${formatDuration(process.hrtime.bigint() - start)}`)
|
||||
}
|
||||
}, pollInterval),
|
||||
)
|
||||
|
||||
if (args['--watch'] !== 'always') {
|
||||
process.stdin.on('end', () => {
|
||||
Promise.all(cleanupWatchers.map((fn) => fn())).then(
|
||||
() => process.exit(0),
|
||||
() => process.exit(1),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
process.stdin.resume()
|
||||
if (!args['--silent']) {
|
||||
process.stderr.write(`\r\x1B[2K${dim(`Polling for changes…`)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load `@parcel/watcher` lazily so a missing or broken native binding only
|
||||
// affects `--watch` (without `--poll`), instead of crashing one-off builds and
|
||||
// polling mode as well.
|
||||
async function loadWatcher(): Promise<typeof import('@parcel/watcher')> {
|
||||
try {
|
||||
return (await import('@parcel/watcher')).default
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to load the file watcher. Your platform may not be supported by \`@parcel/watcher\`. As a workaround, you can use polling instead by passing the \`--watch --poll\` flags.`,
|
||||
{ cause: err },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
|
||||
let watcher = await loadWatcher()
|
||||
|
||||
// Remove any directories that are children of an already watched directory.
|
||||
// If we don't we may not get notified of certain filesystem events regardless
|
||||
// of whether or not they are for the directory that is duplicated.
|
||||
|
|
@ -594,6 +797,56 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) {
|
|||
}
|
||||
}
|
||||
|
||||
function getRebuildStrategy(
|
||||
files: string[],
|
||||
fullRebuildPaths: string[],
|
||||
): { kind: 'incremental'; changedFiles: ChangedContent[] } | { kind: 'full' } {
|
||||
let changedFiles: ChangedContent[] = []
|
||||
|
||||
for (let file of files) {
|
||||
// If one of the changed files is related to the input CSS or JS
|
||||
// config/plugin files, then we need to do a full rebuild because
|
||||
// the theme might have changed.
|
||||
if (fullRebuildPaths.includes(file)) {
|
||||
return { kind: 'full' }
|
||||
}
|
||||
|
||||
// Track new and updated files for incremental rebuilds.
|
||||
changedFiles.push({
|
||||
file,
|
||||
extension: path.extname(file).slice(1),
|
||||
} satisfies ChangedContent)
|
||||
}
|
||||
|
||||
return { kind: 'incremental', changedFiles }
|
||||
}
|
||||
|
||||
function createPollingWatcher(cb: () => Promise<void>, pollInterval: number) {
|
||||
let disposed = false
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function poll() {
|
||||
if (disposed) return
|
||||
|
||||
try {
|
||||
await cb()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
if (!disposed) {
|
||||
timer = setTimeout(poll, pollInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer = setTimeout(poll, pollInterval)
|
||||
|
||||
return async () => {
|
||||
disposed = true
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
async function watchDirectories(scanner: Scanner) {
|
||||
let directories = (
|
||||
await Promise.all(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const sharedOptions = {
|
|||
} satisfies Arg
|
||||
|
||||
function buildUsage(command = 'tailwindcss') {
|
||||
return `${command} [--input input.css] [--output output.css] [--watch] [options…]`
|
||||
return `${command} [--input input.css] [--output output.css] [--watch] [--poll=ms] [options…]`
|
||||
}
|
||||
|
||||
function rootHelp({ invalid }: { invalid?: string } = {}) {
|
||||
|
|
|
|||
|
|
@ -153,3 +153,34 @@ it('should be possible to provide multiple types, and convert the value to that
|
|||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should be possible to provide boolean or number arguments', () => {
|
||||
let options = {
|
||||
'--poll': {
|
||||
type: 'boolean | number',
|
||||
description: 'Use polling instead of filesystem events when watching',
|
||||
default: false,
|
||||
},
|
||||
} satisfies Arg
|
||||
|
||||
expect(args(options, ['--poll'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--poll": true,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
|
||||
expect(args(options, ['--poll=50'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--poll": 50,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
|
||||
expect(args(options, ['--poll=foo'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"--poll": undefined,
|
||||
"_": [],
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ export function wordWrap(text: string, width: number) {
|
|||
for (let word of words) {
|
||||
let wordLength = stripVTControlCharacters(word).length
|
||||
|
||||
if (lineLength + wordLength + 1 > width) {
|
||||
// A word longer than `width` is kept on its own line rather than
|
||||
// introducing an empty line before it.
|
||||
if (lineLength > 0 && lineLength + wordLength + 1 > width) {
|
||||
lines.push(line)
|
||||
line = ''
|
||||
lineLength = 0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/node",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ it('should measure async callbacks via the `span` api', async () => {
|
|||
`)
|
||||
|
||||
let [, duration] = stripVTControlCharacters(output).match(/\[(.*)\]/)!
|
||||
expect(parseFloat(duration)).toBeGreaterThanOrEqual(500)
|
||||
expect(parseFloat(duration)).toBeGreaterThanOrEqual(500 - 2 /* Margin of error */)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ export function optimize(
|
|||
return false
|
||||
}
|
||||
|
||||
// Ignore warnings about unknown at-rules.
|
||||
//
|
||||
// TODO: drop `@position-try` once https://github.com/parcel-bundler/lightningcss/pull/1238 lands
|
||||
if (/Unknown at rule: @position-try/.test(warning.message)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/postcss",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "PostCSS plugin for Tailwind CSS, a utility-first CSS framework for rapidly building custom user interfaces",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
|
|||
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
--color-black: #000;
|
||||
|
|
@ -44,7 +44,7 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
|
|||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
line-height: 1.5;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
|
@ -110,7 +110,7 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
|
|||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
:-moz-focusring:where(:not(iframe)) {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ function inputCssFilePath() {
|
|||
|
||||
const css = dedent
|
||||
|
||||
async function run(plugin: any, from: string, input: string): Promise<string> {
|
||||
let ast = postcss.parse(input)
|
||||
for (let runner of plugin.plugins) {
|
||||
if (runner.Once) {
|
||||
await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } })
|
||||
}
|
||||
}
|
||||
return ast.toString()
|
||||
}
|
||||
|
||||
test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => {
|
||||
let processor = postcss([
|
||||
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
|
||||
|
|
@ -422,17 +432,7 @@ describe('concurrent builds', () => {
|
|||
|
||||
let plugin = tailwindcss({ optimize: { minify: false } })
|
||||
|
||||
async function run(input: string): Promise<string> {
|
||||
let ast = postcss.parse(input)
|
||||
for (let runner of (plugin as any).plugins) {
|
||||
if (runner.Once) {
|
||||
await runner.Once(ast, { postcss, result: { opts: { from }, messages: [] } })
|
||||
}
|
||||
}
|
||||
return ast.toString()
|
||||
}
|
||||
|
||||
let result = await run(input)
|
||||
let result = await run(plugin, from, input)
|
||||
|
||||
expect(result).toContain('.underline')
|
||||
|
||||
|
|
@ -448,14 +448,51 @@ describe('concurrent builds', () => {
|
|||
`,
|
||||
)
|
||||
|
||||
let promise1 = run(input)
|
||||
let promise2 = run(input)
|
||||
let promise1 = run(plugin, from, input)
|
||||
let promise2 = run(plugin, from, input)
|
||||
|
||||
expect(await promise1).toContain('.red')
|
||||
expect(await promise2).toContain('.red')
|
||||
})
|
||||
})
|
||||
|
||||
test('rebuilds when the input CSS changes even if the `from` file on disk did not', async () => {
|
||||
let dir = await mkdtemp(path.join(tmpdir(), 'tw-postcss'))
|
||||
// The `from` file exists on disk with a stable mtime. The input CSS is handed
|
||||
// to the plugin directly (as a preprocessor like Sass would produce it), so it
|
||||
// can change without `from`'s mtime ever changing.
|
||||
await writeFile(path.join(dir, 'index.css'), '')
|
||||
|
||||
let from = path.join(dir, 'index.css')
|
||||
let plugin = tailwindcss({ base: dir, optimize: { minify: false } })
|
||||
|
||||
let first = await run(
|
||||
plugin,
|
||||
from,
|
||||
css`
|
||||
@tailwind utilities;
|
||||
.first {
|
||||
@apply underline;
|
||||
}
|
||||
`,
|
||||
)
|
||||
let second = await run(
|
||||
plugin,
|
||||
from,
|
||||
css`
|
||||
@tailwind utilities;
|
||||
.second {
|
||||
@apply line-through;
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
expect(first).toContain('.first')
|
||||
expect(second).toContain('.second')
|
||||
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('does not register the input file as a dependency, even if it is passed in as relative path', async () => {
|
||||
let processor = postcss([
|
||||
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const DEBUG = env.DEBUG
|
|||
|
||||
interface CacheEntry {
|
||||
mtimes: Map<string, number>
|
||||
inputCss: string
|
||||
compiler: null | ReturnType<typeof compileAst>
|
||||
scanner: null | Scanner
|
||||
tailwindCssAst: AstNode[]
|
||||
|
|
@ -34,6 +35,7 @@ function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOp
|
|||
if (cache.has(key)) return cache.get(key)!
|
||||
let entry = {
|
||||
mtimes: new Map<string, number>(),
|
||||
inputCss: '',
|
||||
compiler: null,
|
||||
scanner: null,
|
||||
|
||||
|
|
@ -194,6 +196,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
|
|||
rebuildStrategy = 'full'
|
||||
context.mtimes.set(file, changedTime)
|
||||
}
|
||||
|
||||
// The mtimes above only track files on disk. When the input CSS is
|
||||
// generated by another tool (e.g. Sass) and passed to `process()`,
|
||||
// it can change without any tracked file's mtime changing, so also
|
||||
// rebuild when the input CSS itself differs from the last build.
|
||||
let inputCss = root.source?.input.css ?? root.toString()
|
||||
if (context.inputCss !== inputCss) {
|
||||
rebuildStrategy = 'full'
|
||||
context.inputCss = inputCss
|
||||
}
|
||||
}
|
||||
DEBUG && I.end('Register full rebuild paths')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/standalone",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"private": true,
|
||||
"description": "Standalone CLI for Tailwind CSS",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,5 @@ declare module '*.css' {
|
|||
declare var __tw_version: string | undefined
|
||||
declare var __tw_resolve: undefined | ((id: string, base?: string) => string | false)
|
||||
declare var __tw_readFile:
|
||||
| undefined
|
||||
| ((path: string, encoding: BufferEncoding) => Promise<string | undefined>)
|
||||
undefined | ((path: string, encoding: BufferEncoding) => Promise<string | undefined>)
|
||||
declare var __tw_load: undefined | ((path: string) => Promise<object | undefined>)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/upgrade",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import postcss, { type Result } from 'postcss'
|
|||
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
|
||||
import { segment } from '../../../../tailwindcss/src/utils/segment'
|
||||
import { Stylesheet, type StylesheetConnection } from '../../stylesheet'
|
||||
import { gitRoot } from '../../utils/git'
|
||||
import { error, highlight, relative } from '../../utils/renderer'
|
||||
import { resolveCssId } from '../../utils/resolve'
|
||||
|
||||
export async function analyze(stylesheets: Stylesheet[]) {
|
||||
let isIgnored = await isGitIgnored()
|
||||
export async function analyze(stylesheets: Stylesheet[], { base }: { base: string }) {
|
||||
// Use the project's git root, instead of the `base` to ensure all
|
||||
// `.gitignore` rules are being used.
|
||||
let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base })
|
||||
let processingQueue: (() => Promise<Result>)[] = []
|
||||
let stylesheetsByFile = new DefaultMap<string, Stylesheet | null>((file) => {
|
||||
// We don't want to process ignored files (like node_modules)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { prepareConfig } from './codemods/template/prepare-config'
|
|||
import { help } from './commands/help'
|
||||
import { Stylesheet } from './stylesheet'
|
||||
import { args, type Arg } from './utils/args'
|
||||
import { isRepoDirty } from './utils/git'
|
||||
import { gitRoot, isRepoDirty } from './utils/git'
|
||||
import { pkg } from './utils/packages'
|
||||
import { eprintln, error, header, highlight, info, relative, success } from './utils/renderer'
|
||||
import * as version from './utils/version'
|
||||
|
|
@ -43,7 +43,9 @@ if (flags['--help']) {
|
|||
|
||||
async function run() {
|
||||
let base = process.cwd()
|
||||
let isIgnored = await isGitIgnored({ cwd: base })
|
||||
// Use the project's git root, instead of the `base` to ensure all
|
||||
// `.gitignore` rules are being used.
|
||||
let isIgnored = await isGitIgnored({ cwd: gitRoot(base) ?? base })
|
||||
|
||||
eprintln(header())
|
||||
eprintln()
|
||||
|
|
@ -125,7 +127,7 @@ async function run() {
|
|||
|
||||
// Analyze the stylesheets
|
||||
try {
|
||||
await analyzeStylesheets(stylesheets)
|
||||
await analyzeStylesheets(stylesheets, { base })
|
||||
} catch (e: any) {
|
||||
error(`${e?.message ?? e}`, { prefix: '↳ ' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,17 @@
|
|||
import { execSync } from 'node:child_process'
|
||||
|
||||
export function gitRoot(cwd?: string): string | null {
|
||||
try {
|
||||
return execSync('git rev-parse --show-toplevel', {
|
||||
encoding: 'utf-8',
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function isRepoDirty(cwd?: string) {
|
||||
try {
|
||||
let stdout = execSync('git status --porcelain', { encoding: 'utf-8', cwd })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/vite",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const DEBUG = env.DEBUG
|
|||
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/
|
||||
const COMMON_JS_PROXY_RE = /\?commonjs-proxy/
|
||||
const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/
|
||||
const JS_EXTENSIONS_RE = /^\.[cm]?[jt]sx?$/
|
||||
|
||||
export type PluginOptions = {
|
||||
/**
|
||||
|
|
@ -45,7 +46,7 @@ function createCustomResolver(
|
|||
// It does matter that this is a file, otherwise we would go up a directory,
|
||||
// which means that we would be resolving files from a parent folder first,
|
||||
// instead of the current folder we are in.
|
||||
let importer = path.resolve(base, '__placeholder__.css')
|
||||
let importer = path.resolve(base, '__placeholder__.ts')
|
||||
|
||||
for (let resolver of resolvers) {
|
||||
let resolved = await resolver(id, importer)
|
||||
|
|
@ -76,6 +77,13 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
let config: ResolvedConfig | null = null
|
||||
let rootsByEnv = new DefaultMap<string, Map<string, Root>>((env: string) => new Map())
|
||||
|
||||
// File extensions that Vite (or one of its plugins) has been seen to process
|
||||
// as a module. Plugins don't get added or removed while the dev server is
|
||||
// running (changing the Vite config restarts the server), so once we've seen
|
||||
// evidence for a file type we don't need to scan the module graphs for it
|
||||
// again.
|
||||
let viteProcessedExtensions = new Set<string>()
|
||||
|
||||
let isSSR = false
|
||||
let shouldOptimize = true
|
||||
let minify = true
|
||||
|
|
@ -271,29 +279,87 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
|
|||
// 'asset'. But it will also have a `HARD_INVALIDATED` state and will
|
||||
// do a full page reload already.
|
||||
//
|
||||
// Empty modules can be skipped since it means it's not `addWatchFile`d and thus irrelevant to Tailwind.
|
||||
// Empty modules can be skipped since it means it's not
|
||||
// `addWatchFile`d and thus irrelevant to Tailwind.
|
||||
let isExternalFile =
|
||||
modules.length > 0 &&
|
||||
modules.every((mod) => mod.type === 'asset' || mod.id === undefined)
|
||||
if (!isExternalFile) return
|
||||
|
||||
// Skip if the module exists in other environments. SSR framework has
|
||||
// its own server side hmr/reload mechanism when handling server
|
||||
// only modules. See https://v6.vite.dev/guide/migration.html
|
||||
// Skip files that Vite (or one of its plugins) processes as a
|
||||
// module — in this environment (e.g. a lazily-loaded route that
|
||||
// hasn't been visited yet) or in another one (e.g. an SSR-only
|
||||
// module). Such a file can only affect the page through Vite's own
|
||||
// pipeline, so a full reload would only destroy client state. Any
|
||||
// changes to the generated CSS still go through the regular
|
||||
// `css-update` flow because the file is registered via
|
||||
// `addWatchFile`.
|
||||
//
|
||||
// If the file exists as a real module in another environment, then
|
||||
// that environment is responsible for it. E.g. an SSR framework
|
||||
// has its own server side hmr/reload mechanism when handling
|
||||
// server only modules. See https://v6.vite.dev/guide/migration.html
|
||||
// > Updates to an SSR-only module no longer triggers a full page reload in the client. ...
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
if (environment.name === this.environment.name) continue
|
||||
|
||||
let modules = environment.moduleGraph.getModulesByFile(file)
|
||||
if (modules) {
|
||||
for (let module of modules) {
|
||||
if (module.type !== 'asset') {
|
||||
for (let mod of modules) {
|
||||
if (mod.type !== 'asset') {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise the file is not loaded as a module anywhere, so
|
||||
// determine whether its file _type_ would be processed by Vite
|
||||
// when requested by the browser (in which case the file just isn't
|
||||
// loaded yet, e.g. a lazily-loaded route that hasn't been visited).
|
||||
// Vite has no API to answer this without actually running the
|
||||
// plugin pipeline, so instead:
|
||||
//
|
||||
// Files Vite handles natively (the JS/TS and CSS families) are always
|
||||
// processed by Vite. This includes stylesheets that never show up as
|
||||
// their own module because a framework plugin compiles them into a
|
||||
// component (e.g. Angular), in which case that plugin owns their HMR.
|
||||
let extension = path.extname(file)
|
||||
if (JS_EXTENSIONS_RE.test(extension) || vite.isCSSRequest(file)) return
|
||||
|
||||
// For any other file type (e.g. `.vue`, `.svelte`, or `.md` with an
|
||||
// SSG plugin), if a file with the same extension exists as a real
|
||||
// module in any environment's module graph, then a plugin evidently
|
||||
// handles this file type and the changed file just isn't loaded
|
||||
// (yet).
|
||||
if (extension !== '') {
|
||||
if (viteProcessedExtensions.has(extension)) return
|
||||
|
||||
for (let environment of Object.values(server.environments)) {
|
||||
for (let mod of environment.moduleGraph.idToModuleMap.values()) {
|
||||
if (!mod.file?.endsWith(extension)) continue
|
||||
if (mod.type === 'asset') continue
|
||||
|
||||
// Only count modules that the plugin pipeline actually
|
||||
// transformed. Vite also creates untransformed placeholder
|
||||
// nodes (e.g. for the file underlying a `?raw` import) that
|
||||
// are not evidence that a plugin handles this file type.
|
||||
if (mod.transformResult == null) continue
|
||||
|
||||
// Similarly, ignore query imports (e.g. `./template.html?raw`,
|
||||
// or the `?html-proxy` modules Vite creates for inline
|
||||
// scripts): they pull a file's _contents_ into the graph
|
||||
// without a plugin processing the file type. A scanned
|
||||
// `.html` template must still trigger a full reload even if
|
||||
// some other `.html` file is imported with `?raw`.
|
||||
if (!mod.id || mod.id.includes('?')) continue
|
||||
|
||||
viteProcessedExtensions.add(extension)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of new Set([this.environment.name, 'client'])) {
|
||||
let roots = rootsByEnv.get(env)
|
||||
if (roots.size === 0) continue
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tailwindcss/webpack",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A webpack loader for Tailwind CSS v4.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tailwindcss",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.3",
|
||||
"description": "A utility-first CSS framework for rapidly building custom user interfaces.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,13 @@ html,
|
|||
tab-size: 4; /* 3 */
|
||||
font-family: --theme(
|
||||
--default-font-family,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
'Noto Sans',
|
||||
Arial,
|
||||
sans-serif,
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
|
|
@ -170,7 +175,7 @@ table {
|
|||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
:-moz-focusring:where(:not(iframe)) {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = `
|
|||
"
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--tw-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
--tw-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--tw-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
--tw-default-font-family: var(--tw-font-sans);
|
||||
|
|
@ -160,7 +160,7 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = `
|
|||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
line-height: 1.5;
|
||||
font-family: var(--tw-default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-family: var(--tw-default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-feature-settings: var(--tw-default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--tw-default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
|
@ -226,7 +226,7 @@ exports[`compiling CSS > prefix all CSS variables inside preflight 1`] = `
|
|||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
:-moz-focusring:where(:not(iframe)) {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,17 @@
|
|||
import { Polyfills } from '.'
|
||||
import * as SelectorParser from '../src/selector-parser'
|
||||
import { parseAtRule } from './css-parser'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import type { Source, SourceLocation } from './source-maps/source'
|
||||
import { Theme, ThemeOptions } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { segment } from './utils/segment'
|
||||
import { extractUsedVariables } from './utils/variables'
|
||||
import * as ValueParser from './value-parser'
|
||||
import { walk, WalkAction, type VisitContext } from './walk'
|
||||
|
||||
const AT_SIGN = 0x40
|
||||
const PIPE = 0x7c
|
||||
|
||||
export type StyleRule = {
|
||||
kind: 'rule'
|
||||
|
|
@ -286,9 +289,9 @@ export function optimizeAst(
|
|||
// found in the theme config.
|
||||
if (
|
||||
polyfills & Polyfills.ColorMix &&
|
||||
node.value.includes('color-mix(') &&
|
||||
!context.supportsColorMix &&
|
||||
!context.keyframes
|
||||
!context.keyframes &&
|
||||
node.value.includes('color-mix(')
|
||||
) {
|
||||
colorMixDeclarations.get(parent).add(node)
|
||||
}
|
||||
|
|
@ -304,37 +307,7 @@ export function optimizeAst(
|
|||
transform(child, nodes, context, depth + 1)
|
||||
}
|
||||
|
||||
// Keep the last decl when there are exact duplicates. Keeping the *first* one might
|
||||
// not be correct when given nested rules where a rule sits between declarations.
|
||||
let seen: Record<string, AstNode[]> = {}
|
||||
let toRemove = new Set<AstNode>()
|
||||
|
||||
// Keep track of all nodes that produce a given declaration
|
||||
for (let child of nodes) {
|
||||
if (child.kind !== 'declaration') continue
|
||||
|
||||
let key = `${child.property}:${child.value}:${child.important}`
|
||||
seen[key] ??= []
|
||||
seen[key].push(child)
|
||||
}
|
||||
|
||||
// And remove all but the last of each
|
||||
for (let key in seen) {
|
||||
for (let i = 0; i < seen[key].length - 1; ++i) {
|
||||
toRemove.add(seen[key][i])
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.size > 0) {
|
||||
nodes = nodes.filter((node) => !toRemove.has(node))
|
||||
}
|
||||
|
||||
if (nodes.length === 0) return
|
||||
|
||||
// Rules with `&` as the selector should be flattened
|
||||
if (node.selector === '&') {
|
||||
parent.push(...nodes)
|
||||
} else {
|
||||
if (nodes.length > 0) {
|
||||
parent.push({ ...node, nodes })
|
||||
}
|
||||
}
|
||||
|
|
@ -679,9 +652,682 @@ export function optimizeAst(
|
|||
}
|
||||
}
|
||||
|
||||
return newAst
|
||||
return handleNesting(newAst)
|
||||
}
|
||||
|
||||
export function handleNesting(ast: AstNode[]): AstNode[] {
|
||||
let parseSelectorCache = new DefaultMap(SelectorParser.parse)
|
||||
|
||||
// Track `rule` selectors as we go
|
||||
let selectorStack: [
|
||||
selector: string,
|
||||
src: SourceLocation | undefined,
|
||||
dst: SourceLocation | undefined,
|
||||
][] = []
|
||||
|
||||
// Track `at-rule` information as we go. Tracking this separately from the
|
||||
// selector stack for rules such that we can hoist this above all the rules.
|
||||
let atRuleStack: [
|
||||
name: string,
|
||||
params: string,
|
||||
src: SourceLocation | undefined,
|
||||
dst: SourceLocation | undefined,
|
||||
][] = []
|
||||
|
||||
// The current "nodes" we can push to
|
||||
let nodes = null as AstNode[] | null
|
||||
|
||||
// Optimization: Track the declaration properties we've seen in the current
|
||||
// nodes.
|
||||
let seenDeclarationProperties = new Set<string>()
|
||||
|
||||
// Track nodes lists where we want to dedupe declarations
|
||||
let dedupeDeclarationsInNodes = new Set<AstNode[]>()
|
||||
|
||||
// The final, new AST
|
||||
let result: AstNode[] = []
|
||||
|
||||
// Track whether we should skip a node in the `exit` phase
|
||||
let skipExit = new Set<AstNode>()
|
||||
|
||||
walk(ast, {
|
||||
enter(node) {
|
||||
switch (node.kind) {
|
||||
case 'rule': {
|
||||
nodes = null // Start a new level
|
||||
|
||||
// First time we see a rule
|
||||
if (selectorStack.length === 0) {
|
||||
// A rule with a selector containing `&` should replace the `&` with
|
||||
// `:scope` if there is no parent rule.
|
||||
//
|
||||
// Note: there could be false positives when the `&` is escaped or
|
||||
// part of a string inside an attribute selector. But the
|
||||
// SelectorParser will take care of that.
|
||||
if (node.selector.includes('&')) {
|
||||
let ast = SelectorParser.parse(node.selector)
|
||||
let changed = false
|
||||
|
||||
walk(ast, (node) => {
|
||||
if (node.kind === 'selector' && node.value === '&') {
|
||||
changed = true
|
||||
node.value = ':scope'
|
||||
}
|
||||
})
|
||||
|
||||
if (changed) {
|
||||
selectorStack.push([SelectorParser.toCss(ast), node.src, node.dst])
|
||||
} else {
|
||||
selectorStack.push([node.selector, node.src, node.dst])
|
||||
}
|
||||
}
|
||||
|
||||
// No nesting markers, track as-is
|
||||
else {
|
||||
selectorStack.push([node.selector, node.src, node.dst])
|
||||
}
|
||||
}
|
||||
|
||||
// Nested rule, ensure `&` is present in each selector. Then track the
|
||||
// selector.
|
||||
else {
|
||||
// A rule with just `&` can be replaced by its children. Let's
|
||||
// ignore this node and keep walking its children.
|
||||
if (node.selector === '&') {
|
||||
skipExit.add(node)
|
||||
return
|
||||
}
|
||||
|
||||
let lastSelector = selectorStack[selectorStack.length - 1][0]
|
||||
let selector = segment(node.selector, ',')
|
||||
.map((selector) => {
|
||||
// Fast path: we know there isn't an `&` so we can prepend the
|
||||
// parent selector immediately.
|
||||
if (!selector.includes('&')) {
|
||||
let lastAst = parseSelectorCache.get(lastSelector)
|
||||
return `${lastAst.length === 1 && lastAst[0].kind === 'list' ? `:is(${lastSelector})` : lastSelector} ${selector}`
|
||||
}
|
||||
|
||||
// Slow path: we need to replace the `&` with the parent
|
||||
// selector. A simple `replaceAll(…)` won't work because a `&`
|
||||
// could be escaped, or could be part of an attribute selector.
|
||||
//
|
||||
// Much safer to parse the selector and replace the `&` that way
|
||||
{
|
||||
let ast = SelectorParser.parse(selector)
|
||||
let changed = false
|
||||
walk(ast, {
|
||||
enter(node, ctx) {
|
||||
if (node.kind !== 'selector' || node.value !== '&') return
|
||||
|
||||
changed = true
|
||||
|
||||
// Safest option: use `is(…)` semantics when
|
||||
// substituting `&` for the parent selector.
|
||||
node.value = `:is(${lastSelector})`
|
||||
|
||||
// We should always have a parent, so this shouldn't happen
|
||||
if (ctx.parent === null) return
|
||||
|
||||
// Optimizations:
|
||||
let parentAst = parseSelectorCache.get(lastSelector)
|
||||
|
||||
// 1. If we're dealing with multiple selectors, then we
|
||||
// know that the `:is(…)` needs to stay. Nothing to optimize.
|
||||
if (parentAst.length === 1 && parentAst[0].kind === 'list') {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
|
||||
// 2. We know that `&` is standalone when it's inside of
|
||||
// a complex selector. E.g. `[before] & [after]`
|
||||
if (ctx.parent.kind === 'complex') {
|
||||
// `& [after]`
|
||||
//
|
||||
// `:is(…)` semantics are not required, because these
|
||||
// are equivalent:
|
||||
//
|
||||
// - `:is(div) [after]` → `div [after]`
|
||||
// - `:is(.x) [after]` → `.x [after]`
|
||||
// - `:is([before]) [after]` → `[before] [after]`
|
||||
// - `:is(.a > .b) [after]` → `.a > .b [after]`
|
||||
if (ctx.index === 0) {
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
|
||||
// `[before] &`
|
||||
//
|
||||
// `:is(…)` semantics are required if we're dealing
|
||||
// with a complex parent selector. Otherwise the
|
||||
// meaning of the selector could change:
|
||||
//
|
||||
// - `[before] :is(div)` → `[before] div`
|
||||
// - `[before] :is(.x)` → `[before] .x`
|
||||
// - `[before] :is([after])` → `[before] [after]`
|
||||
// - `[before] :is(.a > .b)` → `[before] :is(.a > .b)` (!)
|
||||
else if (ctx.index === ctx.siblings.length - 1) {
|
||||
if (parentAst[0].kind === 'complex') {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
|
||||
// `[before] & [after]`
|
||||
//
|
||||
// `:is(…)` semantics are required if we're dealing
|
||||
// with a complex parent selector. Otherwise the
|
||||
// meaning of the selector could change:
|
||||
//
|
||||
// - `[before] :is(div) [after]` → `[before] div [after]`
|
||||
// - `[before] :is(.x) [after]` → `[before] .x [after]`
|
||||
// - `[before] :is(.a > .b) [after]` → `[before] :is(.a > .b) [after]` (!)
|
||||
else {
|
||||
if (parentAst[0].kind === 'complex') {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. We know that `&` is attached to some other
|
||||
// selector when it's inside of a compound selector.
|
||||
//
|
||||
// E.g. `[before]&[after]`
|
||||
//
|
||||
// We have to be careful that our parent, when it's
|
||||
// part of a complex selector, that the same rules apply
|
||||
//
|
||||
// E.g.: `[before] &[after]`
|
||||
// ^ current node
|
||||
// ^^^^^^^^ compound selector
|
||||
// ^^^^^^^^^^^^^^^^^ complex selector
|
||||
//
|
||||
else if (ctx.parent.kind === 'compound') {
|
||||
if (parentAst[0].kind === 'complex') {
|
||||
let path = ctx.path()
|
||||
let grandParent = path[path.length - 2]
|
||||
|
||||
// When our compound parent is part of a complex
|
||||
// selector, and it's not the very first node, then we
|
||||
// can't safely get rid of the `:is(…)` if the last
|
||||
// selector is a complex selector as well, unless the
|
||||
// `&` maps to a single selector or compound selector.
|
||||
//
|
||||
// ```css
|
||||
// .foo .bar { /* Complex selector */
|
||||
// .system &:focus { /* Complex selector + compound selecto*/
|
||||
// --x: 1;
|
||||
// }
|
||||
// }
|
||||
// .foo:hover { /* Compound selector */
|
||||
// .system &:focus { /* Complex selector + compound selector */
|
||||
// --x: 2;
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
//
|
||||
// ```css
|
||||
// .system :is(.foo .bar):focus { /* Cannot drop the `:is(…)`, otherwise `.system` and `.foo` can be swapped in the DOM */
|
||||
// --x: 1;
|
||||
// }
|
||||
// .system .foo:hover:focus {
|
||||
// --x: 2;
|
||||
// }
|
||||
// ```
|
||||
if (
|
||||
grandParent &&
|
||||
grandParent.kind === 'complex' &&
|
||||
grandParent.nodes[0] !== ctx.parent
|
||||
) {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
}
|
||||
|
||||
// `&*` and `&div` are invalid CSS so these should stay
|
||||
// invalid. They should be written as `*&` and `div&` instead.
|
||||
if (
|
||||
ctx.siblings
|
||||
.slice(ctx.index + 1)
|
||||
.some(
|
||||
(sibling) =>
|
||||
SelectorParser.isUniversalSelector(sibling) ||
|
||||
SelectorParser.isTypeSelector(sibling),
|
||||
)
|
||||
) {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
|
||||
// `&[after]`
|
||||
//
|
||||
// `:is(…)` semantics are not required, because these
|
||||
// are equivalent:
|
||||
//
|
||||
// - `:is(div)[after]` → `div[after]`
|
||||
// - `:is(.x)[after]` → `.x[after]`
|
||||
// - `:is([before])[after]` → `[before][after]`
|
||||
// - `:is(.a > .b)[after]` → `.a > .b[after]`
|
||||
if (ctx.index === 0) {
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
|
||||
// `[before]&`
|
||||
//
|
||||
// `:is(…)` semantics are required if we're dealing with a
|
||||
//
|
||||
// - complex parent selector, to prevent changing the meaning of the selector
|
||||
// - a universal selector, because `*` needs to be first
|
||||
// - a type selector, because `div` needs to be first
|
||||
//
|
||||
// - `[before]:is(div)` → `[before]:is(div)` (!)
|
||||
// - `[before]:is(*)` → `[before]:is(*)` (!)
|
||||
// - `[before]:is(.a > .b)` → `[before]:is(.a > .b)` (!)
|
||||
// - `[before]:is(.x)` → `[before].x`
|
||||
else if (ctx.index === ctx.siblings.length - 1) {
|
||||
if (
|
||||
parentAst[0].kind === 'complex' ||
|
||||
SelectorParser.isUniversalSelector(parentAst[0]) ||
|
||||
SelectorParser.isTypeSelector(parentAst[0])
|
||||
) {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
|
||||
// `[before]&[after]`
|
||||
//
|
||||
// `:is(…)` semantics are required if we're dealing with a
|
||||
//
|
||||
// - complex parent selector, to prevent changing the meaning of the selector
|
||||
// - a universal selector, because `*` needs to be first
|
||||
// - a type selector, because `div` needs to be first
|
||||
//
|
||||
// - `[before]:is(div)[after]` → `[before]:is(div)[after]` (!)
|
||||
// - `[before]:is(*)[after]` → `[before]:is(*)[after]` (!)
|
||||
// - `[before]:is(.a > .b)[after]` → `[before]:is(.a > .b)[after]` (!)
|
||||
// - `[before]:is(.x)[after]` → `[before].x[after]`
|
||||
else {
|
||||
if (
|
||||
parentAst[0].kind === 'complex' ||
|
||||
SelectorParser.isUniversalSelector(parentAst[0]) ||
|
||||
SelectorParser.isTypeSelector(parentAst[0])
|
||||
) {
|
||||
return // Keep `:is(…)` semantics
|
||||
}
|
||||
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 4. When the current node is a function argument (e.g.
|
||||
// `:not(&))`, then we can drop the `:is(…)` entirely.
|
||||
//
|
||||
// The only exception is when the parent has multiple
|
||||
// selectors because then we would introduce multiple
|
||||
// arguments. Multiple selectors are already handled.
|
||||
else if (ctx.parent.kind === 'function') {
|
||||
node.value = lastSelector
|
||||
return
|
||||
}
|
||||
},
|
||||
exit(node, ctx) {
|
||||
// Optimization: We can remove the universal selector `*` if
|
||||
// they are part of a compound selector. E.g.:
|
||||
//
|
||||
// - `*:hover` → `:hover`
|
||||
// - `*[attribute]` → `[attribute]`
|
||||
//
|
||||
// Except when the `*` is a namespace, e.g.: `*|div`,
|
||||
// because `*|div` (any namespace) and `|div` (no
|
||||
// namespace) have different meanings.
|
||||
if (
|
||||
ctx.index === 0 &&
|
||||
ctx.siblings.length > 1 &&
|
||||
ctx.parent?.kind === 'compound' &&
|
||||
SelectorParser.isUniversalSelector(node)
|
||||
) {
|
||||
let next = ctx.siblings[1]
|
||||
if (next.kind === 'selector' && next.value.charCodeAt(0) === PIPE) {
|
||||
return
|
||||
}
|
||||
return WalkAction.ReplaceSkip([])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (changed) {
|
||||
return SelectorParser.toCss(ast)
|
||||
}
|
||||
|
||||
// It could be that `&` was not found as an actual selector,
|
||||
// in that case we still have to prepend the parent selector.
|
||||
let lastAst = parseSelectorCache.get(lastSelector)
|
||||
return `${lastAst.length === 1 && lastAst[0].kind === 'list' ? `:is(${lastSelector})` : lastSelector} ${selector}`
|
||||
}
|
||||
})
|
||||
.join(', ')
|
||||
selectorStack.push([selector, node.src, node.dst])
|
||||
}
|
||||
|
||||
// Once we hit a rule that has at least one declaration, then we can
|
||||
// stop handling the nested selectors.
|
||||
//
|
||||
// This ensures that browser devtools can at least show _something_
|
||||
// for a given rule. This also means that we can leverage CSS nesting
|
||||
// which is better for gzip results due to increased repetition.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .a {
|
||||
// &[b] {
|
||||
// color: red;
|
||||
// &:hover {
|
||||
// color: blue;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
//
|
||||
// ```css
|
||||
// .a[b] { /* ← flattened */
|
||||
// color: red; /* ← saw a declaration, therefore */
|
||||
// &:hover { /* ← we keep this nested syntax */
|
||||
// color: blue;
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
if (node.nodes.some((child) => child.kind === 'declaration')) {
|
||||
// Emitting each child instead of the node itself because we do want
|
||||
// to flatten the current node and its selector that is already
|
||||
// pushed to the stack.
|
||||
for (let child of node.nodes) emit(child)
|
||||
return WalkAction.Skip
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'at-rule': {
|
||||
nodes = null // Start a new level
|
||||
|
||||
// `@layer` is hoistable, but when it's empty then we have to make
|
||||
// sure that we still emit it because this might influence the layer
|
||||
// order. We can't just get rid of it.
|
||||
if (node.nodes.length === 0 && !DROPPABLE_IF_EMPTY_AT_RULES.has(node.name)) {
|
||||
emit(node)
|
||||
skipExit.add(node)
|
||||
return WalkAction.Skip
|
||||
}
|
||||
|
||||
// Hoist at-rules
|
||||
else if (HOISTABLE_AT_RULES.has(node.name)) {
|
||||
atRuleStack.push([node.name, node.params, node.src, node.dst])
|
||||
}
|
||||
|
||||
// If we can't hoist them, emit them immediately as-is
|
||||
else {
|
||||
emit(node)
|
||||
skipExit.add(node)
|
||||
return WalkAction.Skip
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'declaration':
|
||||
case 'comment': {
|
||||
emit(node)
|
||||
break
|
||||
}
|
||||
|
||||
case 'context':
|
||||
case 'at-root':
|
||||
break
|
||||
|
||||
default:
|
||||
node satisfies never
|
||||
break
|
||||
}
|
||||
},
|
||||
exit(node) {
|
||||
if (skipExit.delete(node)) return
|
||||
|
||||
switch (node.kind) {
|
||||
case 'rule': {
|
||||
nodes = null
|
||||
selectorStack.pop()
|
||||
break
|
||||
}
|
||||
|
||||
case 'at-rule': {
|
||||
nodes = null
|
||||
atRuleStack.pop()
|
||||
break
|
||||
}
|
||||
|
||||
case 'declaration':
|
||||
case 'comment':
|
||||
case 'context':
|
||||
case 'at-root':
|
||||
break
|
||||
|
||||
default:
|
||||
node satisfies never
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Dedupe declarations that we've already seen before if they match the
|
||||
// `property`, `value` and `important` information.
|
||||
{
|
||||
for (let nodes of dedupeDeclarationsInNodes) {
|
||||
let seen = new Set()
|
||||
for (let i = nodes.length - 1; i >= 0; --i) {
|
||||
let node = nodes[i]
|
||||
if (node.kind !== 'declaration') continue
|
||||
|
||||
let id = `${node.property}\0${node.value}\0${node.important}`
|
||||
|
||||
if (seen.has(id)) nodes.splice(i, 1)
|
||||
else seen.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
function emit(node: AstNode) {
|
||||
// Existing nodes are available, emit into those nodes
|
||||
if (nodes) {
|
||||
// Optimization: track used declarations in the current node.
|
||||
if (node.kind === 'declaration') {
|
||||
if (seenDeclarationProperties.has(node.property)) {
|
||||
dedupeDeclarationsInNodes.add(nodes)
|
||||
} else {
|
||||
seenDeclarationProperties.add(node.property)
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push(node)
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing available, setup a fresh node
|
||||
{
|
||||
// There are no parent rules or at-rules available, which means that we
|
||||
// can emit the node as-is.
|
||||
if (selectorStack.length === 0 && atRuleStack.length === 0) {
|
||||
let target = result
|
||||
let lastNode = target[target.length - 1]
|
||||
|
||||
// Optimization: when the current and last node are the same, ignore the
|
||||
// new node entirely otherwise we will get unnecessary duplicate
|
||||
// results.
|
||||
//
|
||||
// We only care about at-rules with no body because some of them (such
|
||||
// as `@charset` or `@layer`) need to be emitted. A normal rule that's
|
||||
// empty doesn't need to be emitted. At-rules _with_ a body (such as
|
||||
// `@font-face` or `@keyframes`) can share the same prelude while
|
||||
// containing different bodies, so they must all be emitted.
|
||||
if (
|
||||
lastNode &&
|
||||
lastNode.kind === 'at-rule' &&
|
||||
node.kind === 'at-rule' &&
|
||||
lastNode.nodes.length === 0 &&
|
||||
node.nodes.length === 0 &&
|
||||
lastNode.name === node.name &&
|
||||
lastNode.params === node.params
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
result.push(node)
|
||||
return
|
||||
}
|
||||
|
||||
// Track the new "parent" nodes
|
||||
{
|
||||
nodes = [node]
|
||||
|
||||
// Clear out seen declarations from the previous work in progress nodes
|
||||
seenDeclarationProperties.clear()
|
||||
|
||||
// Track new declaration
|
||||
if (node.kind === 'declaration') {
|
||||
seenDeclarationProperties.add(node.property)
|
||||
}
|
||||
}
|
||||
|
||||
// Track the new root node that we build up to store in the final AST
|
||||
let root = null as AstNode | null
|
||||
|
||||
let target = result
|
||||
let atRuleOffset = 0
|
||||
|
||||
// Optimization: merge adjacent at-rules
|
||||
//
|
||||
// Figure out whether we can push our new node into a previous node that
|
||||
// was already emitted.
|
||||
//
|
||||
// We have to make sure that the order stays the same, so therefore we
|
||||
// only ever have to look at the last node that was emitted.
|
||||
{
|
||||
let lastNode = target[target.length - 1]
|
||||
if (lastNode && lastNode.kind === 'at-rule') {
|
||||
for (let i = 0; i < atRuleStack.length; i++) {
|
||||
let atRule = atRuleStack[i]
|
||||
if (!lastNode) break
|
||||
if (lastNode.kind !== 'at-rule') break
|
||||
if (lastNode.name !== atRule[0]) break
|
||||
if (lastNode.params !== atRule[1]) break
|
||||
|
||||
atRuleOffset++
|
||||
target = lastNode.nodes
|
||||
lastNode = lastNode.nodes[lastNode.nodes.length - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build up the rule
|
||||
if (selectorStack.length > 0) {
|
||||
let [selector, src, dst] = selectorStack[selectorStack.length - 1]
|
||||
|
||||
// Optimization: merge adjacent rules with the same selector
|
||||
//
|
||||
// Figure out whether we can push into an existing rule.
|
||||
//
|
||||
// If we have some at-rules that we have to keep into account, then we
|
||||
// definitely can't.
|
||||
if (atRuleStack.length - atRuleOffset <= 0) {
|
||||
let lastNode = target[target.length - 1]
|
||||
if (lastNode && lastNode.kind === 'rule' && lastNode.selector === selector) {
|
||||
lastNode.nodes.push(...nodes)
|
||||
|
||||
// Ensure that our current nodes points to the nodes of the
|
||||
// `lastNode`, otherwise we will lose information.
|
||||
nodes = lastNode.nodes
|
||||
|
||||
// We appended a group that could contain declarations already in the
|
||||
// existing rule, so let the final dedupe pass handle it once.
|
||||
//
|
||||
// Note: we could loop over _all_ previous nodes to figure out if we
|
||||
// really want to dedupe this. But this could result in a bunch of
|
||||
// duplicate work if we have `n` nodes that we want to merge
|
||||
// together.
|
||||
dedupeDeclarationsInNodes.add(nodes)
|
||||
|
||||
// We know that we don't have to handle any more at-rules, so we can
|
||||
// bail early since we just merged the nodes with the same selector.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Can't push into existing node, create a new node
|
||||
root = rule(selector, nodes)
|
||||
if (src || dst) Object.assign(root, { src, dst })
|
||||
}
|
||||
|
||||
// Wrap in at-rules, if we can push into an existing node then we can
|
||||
// ignore `offset` amount of nodes since the `root`/`nodes` will already
|
||||
// point to a nested node.
|
||||
for (let i = atRuleStack.length - 1; i >= atRuleOffset; --i) {
|
||||
let [name, params, src, dst] = atRuleStack[i]
|
||||
|
||||
root = atRule(name, params, root ? [root] : nodes)
|
||||
if (src || dst) Object.assign(root, { src, dst })
|
||||
}
|
||||
|
||||
// Track the root node in the AST
|
||||
if (root) {
|
||||
target.push(root)
|
||||
}
|
||||
|
||||
// We didn't build up any new root, so we can move our node directly into
|
||||
// the target. This can happen when we emit a node that is not a
|
||||
// declaration or a comment.
|
||||
else {
|
||||
target.push(...nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A set of at-rules that can be hoisted to the top without any repercussions.
|
||||
// Typically at-rules that rely on the environment, not parent information and
|
||||
// contain other rules/declarations.
|
||||
const HOISTABLE_AT_RULES = new Set([
|
||||
'@container',
|
||||
'@layer',
|
||||
'@media',
|
||||
'@page',
|
||||
'@starting-style',
|
||||
'@supports',
|
||||
'@view-transition',
|
||||
])
|
||||
|
||||
// A set of at-rules that can be dropped if they don't contain any nodes. We
|
||||
// don't have the distinction between an at-rule with no body, or an at-rule
|
||||
// with a body that is empty right now.
|
||||
const DROPPABLE_IF_EMPTY_AT_RULES = new Set([
|
||||
'@container',
|
||||
'@media',
|
||||
'@page',
|
||||
'@starting-style',
|
||||
'@supports',
|
||||
'@view-transition',
|
||||
])
|
||||
|
||||
export function toCss(ast: AstNode[], track?: boolean) {
|
||||
let pos = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ describe('parse', () => {
|
|||
'[ data-foo ]',
|
||||
{ attribute: 'data-foo', operator: null, quote: null, value: null, sensitivity: null },
|
||||
],
|
||||
[
|
||||
// Non-ASCII code points are valid in a CSS ident
|
||||
'[data-état]',
|
||||
{ attribute: 'data-état', operator: null, quote: null, value: null, sensitivity: null },
|
||||
],
|
||||
[
|
||||
'[data-state=expanded]',
|
||||
{ attribute: 'data-state', operator: '=', quote: null, value: 'expanded', sensitivity: null },
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const ZERO = 48
|
|||
const NINE = 57
|
||||
const DASH = 45
|
||||
const UNDERSCORE = 95
|
||||
const NON_ASCII = 128
|
||||
|
||||
interface AttributeSelector {
|
||||
attribute: string
|
||||
|
|
@ -61,6 +62,8 @@ export function parse(input: string): AttributeSelector | null {
|
|||
if (currentChar >= LOWER_A && currentChar <= LOWER_Z) continue
|
||||
if (currentChar >= ZERO && currentChar <= NINE) continue
|
||||
if (currentChar === DASH || currentChar === UNDERSCORE) continue
|
||||
// Non-ASCII code points are valid in a CSS ident, e.g.: `[data-état]`
|
||||
if (currentChar >= NON_ASCII) continue
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -316,6 +316,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
['[color:var(--color-red-500)]', 'text-red-500'],
|
||||
['[background-color:var(--color-red-500)]', 'bg-red-500'],
|
||||
|
||||
// Arbitrary property to named utility (case insensitive)
|
||||
['[color:#fff]', 'text-white'],
|
||||
['[color:#FFF]', 'text-white'],
|
||||
|
||||
// Arbitrary property with modifier to named functional utility with modifier
|
||||
['[color:var(--color-red-500)]/25', 'text-red-500/25'],
|
||||
|
||||
|
|
@ -354,6 +358,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
--*: initial;
|
||||
--spacing: 0.25rem;
|
||||
--color-red-500: red;
|
||||
--color-white: #fff;
|
||||
|
||||
/* Equivalent of blue-500/50 */
|
||||
--color-primary: color-mix(in oklab, oklch(62.3% 0.214 259.815) 50%, transparent);
|
||||
|
|
@ -394,6 +399,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'],
|
||||
['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'],
|
||||
|
||||
// Arbitrary value with different casing
|
||||
['bg-[#fff]', 'bg-white'],
|
||||
['bg-[#FFF]', 'bg-white'],
|
||||
|
||||
// Values that don't contain only `theme(…)` calls should not be converted to
|
||||
// use a modifier since the color is not the whole value.
|
||||
[
|
||||
|
|
@ -478,6 +487,9 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
|
|||
])(testName, { timeout }, async (candidate, expected) => {
|
||||
let input = css`
|
||||
@import 'tailwindcss';
|
||||
@theme {
|
||||
--color-white: #fff;
|
||||
}
|
||||
`
|
||||
|
||||
await expectCanonicalization(input, candidate, expected)
|
||||
|
|
@ -1508,4 +1520,30 @@ describe('regressions', () => {
|
|||
|
||||
expect(designSystem.canonicalizeCandidates(['px-[calc(1rem+0px)]'], options)).toEqual(['px-4'])
|
||||
})
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/20295
|
||||
test('canonicalizations work when casing of arbitrary values is different', async () => {
|
||||
let designSystem = await designSystems.get(__dirname).get(css`
|
||||
@import 'tailwindcss';
|
||||
@theme {
|
||||
--color-brand-purple: #3f3cbb;
|
||||
}
|
||||
`)
|
||||
|
||||
let options: CanonicalizeOptions = {
|
||||
collapse: true,
|
||||
logicalToPhysical: true,
|
||||
rem: 16,
|
||||
}
|
||||
|
||||
expect(designSystem.canonicalizeCandidates(['bg-[#3F3cbb]'], options)).toEqual([
|
||||
'bg-brand-purple',
|
||||
])
|
||||
expect(designSystem.canonicalizeCandidates(['bg-[#3F3Cbb]'], options)).toEqual([
|
||||
'bg-brand-purple',
|
||||
])
|
||||
expect(designSystem.canonicalizeCandidates(['bg-[#3F3CBB]'], options)).toEqual([
|
||||
'bg-brand-purple',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2425,6 +2425,12 @@ function createUtilitySignatureCache(
|
|||
})
|
||||
}
|
||||
|
||||
// #RGB
|
||||
// #RGBA
|
||||
// #RRGGBB
|
||||
// #RRGGBBAA
|
||||
const HEX_REGEX = /#(?:[a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{4}|[a-f0-9]{3})/gi
|
||||
|
||||
// Optimize the CSS AST to make it suitable for signature comparison. We want to
|
||||
// expand declarations, ignore comments, sort declarations etc...
|
||||
function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: SignatureOptions) {
|
||||
|
|
@ -2536,6 +2542,18 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
return a.property.localeCompare(b.property)
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
else if (node.kind === 'declaration' && node.value) {
|
||||
// Leave CSS variables alone
|
||||
if (node.property[0] === '-' && node.property[1] === '-') return
|
||||
|
||||
// Ensure hex colors are always lowercased
|
||||
{
|
||||
HEX_REGEX.lastIndex = 0
|
||||
node.value = node.value.replace(HEX_REGEX, (color) => color.toLowerCase())
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default {
|
|||
'950': 'oklch(13% 0.028 261.692)',
|
||||
},
|
||||
zinc: {
|
||||
'50': 'oklch(98.5% 0 0)',
|
||||
'50': 'oklch(98.5% 0 none)',
|
||||
'100': 'oklch(96.7% 0.001 286.375)',
|
||||
'200': 'oklch(92% 0.004 286.32)',
|
||||
'300': 'oklch(87.1% 0.006 286.286)',
|
||||
|
|
@ -44,17 +44,17 @@ export default {
|
|||
'950': 'oklch(14.1% 0.005 285.823)',
|
||||
},
|
||||
neutral: {
|
||||
'50': 'oklch(98.5% 0 0)',
|
||||
'100': 'oklch(97% 0 0)',
|
||||
'200': 'oklch(92.2% 0 0)',
|
||||
'300': 'oklch(87% 0 0)',
|
||||
'400': 'oklch(70.8% 0 0)',
|
||||
'500': 'oklch(55.6% 0 0)',
|
||||
'600': 'oklch(43.9% 0 0)',
|
||||
'700': 'oklch(37.1% 0 0)',
|
||||
'800': 'oklch(26.9% 0 0)',
|
||||
'900': 'oklch(20.5% 0 0)',
|
||||
'950': 'oklch(14.5% 0 0)',
|
||||
'50': 'oklch(98.5% 0 none)',
|
||||
'100': 'oklch(97% 0 none)',
|
||||
'200': 'oklch(92.2% 0 none)',
|
||||
'300': 'oklch(87% 0 none)',
|
||||
'400': 'oklch(70.8% 0 none)',
|
||||
'500': 'oklch(55.6% 0 none)',
|
||||
'600': 'oklch(43.9% 0 none)',
|
||||
'700': 'oklch(37.1% 0 none)',
|
||||
'800': 'oklch(26.9% 0 none)',
|
||||
'900': 'oklch(20.5% 0 none)',
|
||||
'950': 'oklch(14.5% 0 none)',
|
||||
},
|
||||
stone: {
|
||||
'50': 'oklch(98.5% 0.001 106.423)',
|
||||
|
|
@ -70,7 +70,7 @@ export default {
|
|||
'950': 'oklch(14.7% 0.004 49.25)',
|
||||
},
|
||||
mauve: {
|
||||
'50': 'oklch(98.5% 0 0)',
|
||||
'50': 'oklch(98.5% 0 none)',
|
||||
'100': 'oklch(96% 0.003 325.6)',
|
||||
'200': 'oklch(92.2% 0.005 325.62)',
|
||||
'300': 'oklch(86.5% 0.012 325.68)',
|
||||
|
|
|
|||
|
|
@ -1297,22 +1297,22 @@ test('utilities must be prefixed', async () => {
|
|||
// Prefixed utilities are generated
|
||||
expect(await run(['tw:underline', 'tw:hover:line-through', 'tw:custom'], input, options))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.tw\\:custom {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.tw\\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.tw\\:hover\\:line-through:hover {
|
||||
text-decoration-line: line-through;
|
||||
"
|
||||
.tw\\:custom {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
.tw\\:underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.tw\\:hover\\:line-through:hover {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
// Non-prefixed utilities are ignored
|
||||
expect(await run(['underline', 'hover:line-through', 'custom'], input, options)).toEqual('')
|
||||
|
|
@ -1534,18 +1534,18 @@ test('blocklisted candidates are not generated', async () => {
|
|||
// underline will as will md:bg-white
|
||||
expect(await run(['underline', 'bg-white', 'md:bg-white'], input, options))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.md\\:bg-white {
|
||||
background-color: var(--color-white, #fff);
|
||||
"
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.md\\:bg-white {
|
||||
background-color: var(--color-white, #fff);
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('blocklisted candidates cannot be used with `@apply`', async () => {
|
||||
|
|
|
|||
|
|
@ -353,8 +353,13 @@ export default {
|
|||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'ui-sans-serif',
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'"Noto Sans"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
|
|
|
|||
|
|
@ -635,6 +635,46 @@ describe('theme', async () => {
|
|||
})
|
||||
})
|
||||
|
||||
test('theme() resolves the DEFAULT value when a bare CSS theme key shares a prefix with a sibling key', async () => {
|
||||
expect(
|
||||
await run(
|
||||
['example-foo', 'example-foo-bar'],
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@theme {
|
||||
--color-foo: red;
|
||||
--color-foo-bar: blue;
|
||||
}
|
||||
@plugin "my-plugin";
|
||||
`,
|
||||
{
|
||||
loadModule: async (_id, base) => {
|
||||
return {
|
||||
path: '',
|
||||
base,
|
||||
module: plugin(function ({ addUtilities, theme }) {
|
||||
addUtilities({
|
||||
'.example-foo': { color: theme('colors.foo') },
|
||||
'.example-foo-bar': { color: theme('colors.foo-bar') },
|
||||
})
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.example-foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.example-foo-bar {
|
||||
color: #00f;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('all necessary theme keys support bare values', async () => {
|
||||
expect(
|
||||
await run(
|
||||
|
|
@ -3288,7 +3328,42 @@ describe('addUtilities()', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.j.j, .j.j, .a .b:hover .c, .a .b:hover .c, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .e .bar:not(.f):has(.g), .h ~ .i, .h ~ .i {
|
||||
.j.j, .a .b:hover .c, .d > *, .e .bar:not(.f):has(.g), .h ~ .i {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('does not register class names inside `:nth-child(… of …)` as utilities', async () => {
|
||||
expect(
|
||||
await run(
|
||||
['foo', 'mark'],
|
||||
css`
|
||||
@plugin "my-plugin";
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
`,
|
||||
{
|
||||
async loadModule(_id, base) {
|
||||
return {
|
||||
path: '',
|
||||
base,
|
||||
module: ({ addUtilities }: PluginAPI) => {
|
||||
addUtilities({
|
||||
'.foo:nth-child(2 of .mark)': { color: 'red' },
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@layer utilities {
|
||||
.foo:nth-child(2 of .mark) {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,7 +310,14 @@ export function buildPluginApi({
|
|||
return
|
||||
}
|
||||
|
||||
if (node.kind === 'function' && node.value === ':not') {
|
||||
if (
|
||||
node.kind === 'function' &&
|
||||
(node.value === ':not' ||
|
||||
// A class inside `:nth-child(… of <selector>)` is part of the
|
||||
// condition, not a utility being defined.
|
||||
node.value === ':nth-child' ||
|
||||
node.value === ':nth-last-child')
|
||||
) {
|
||||
return WalkAction.Skip
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -100,6 +100,23 @@ export function createThemeFn(
|
|||
return [base, extra]
|
||||
}
|
||||
|
||||
// If `--color-foo` and `--color-foo-bar` are both defined, `colors.foo`
|
||||
// produces a synthetic object:
|
||||
//
|
||||
// ```ts
|
||||
// { DEFAULT: 'red', bar: 'blue', __CSS_VALUES__: { DEFAULT: 0, bar: 0 } }
|
||||
// ```
|
||||
//
|
||||
// Prefer `DEFAULT` instead of exposing the object to plugin code.
|
||||
if (
|
||||
cssValue !== null &&
|
||||
typeof cssValue === 'object' &&
|
||||
!Array.isArray(cssValue) &&
|
||||
'DEFAULT' in cssValue
|
||||
) {
|
||||
return cssValue.DEFAULT
|
||||
}
|
||||
|
||||
// Values from CSS take precedence over values from the config
|
||||
return cssValue ?? configValue
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export function constantFoldDeclarationAst(
|
|||
if (
|
||||
operator === '+' &&
|
||||
!(
|
||||
(constant[1] === known[1]) // Only same unit is allowed
|
||||
constant[1] === known[1] // Only same unit is allowed
|
||||
)
|
||||
) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { compile } from '.'
|
||||
import plugin from './plugin'
|
||||
import { compileCss, run } from './test-utils/run'
|
||||
import { compileCss, pretty, run } from './test-utils/run'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
|
|
@ -106,6 +107,14 @@ describe('--spacing(…)', () => {
|
|||
})
|
||||
|
||||
describe('optimizations', () => {
|
||||
// Use custom `compileCss` such that we can use a version without Lightning
|
||||
// CSS. This way we test _our_ optimizations instead of Lightning's
|
||||
// optimizations.
|
||||
async function compileCss(css: string, options: Parameters<typeof compile>[1] = {}) {
|
||||
let { build } = await compile(css, options)
|
||||
return pretty(build([]))
|
||||
}
|
||||
|
||||
test('--spacing(…) optimizes the output when the input is `0`', async () => {
|
||||
expect(
|
||||
await compileCss(css`
|
||||
|
|
@ -121,8 +130,8 @@ describe('--spacing(…)', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.foo {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
|
@ -143,8 +152,8 @@ describe('--spacing(…)', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.foo {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
|
@ -165,9 +174,8 @@ describe('--spacing(…)', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
:root, :host {
|
||||
--spacing: .25rem;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.foo {
|
||||
margin: var(--spacing);
|
||||
padding: var(--spacing);
|
||||
|
|
@ -191,8 +199,8 @@ describe('--spacing(…)', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.foo {
|
||||
margin: .25rem;
|
||||
padding: .25rem;
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
|
@ -904,8 +912,9 @@ describe('theme(…)', () => {
|
|||
await compileCss(css`
|
||||
@theme default reference {
|
||||
--font-family-sans:
|
||||
ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
||||
Segoe UI Symbol, Noto Color Emoji;
|
||||
-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Noto Sans,
|
||||
Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
|
||||
Noto Color Emoji;
|
||||
}
|
||||
.fam {
|
||||
font-family: theme(fontFamily.sans);
|
||||
|
|
@ -914,7 +923,7 @@ describe('theme(…)', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.fam {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Noto Sans, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
|
@ -934,7 +943,7 @@ describe('theme(…)', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
.fam {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Noto Sans, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
|
@ -1154,7 +1163,7 @@ describe('theme(…)', () => {
|
|||
test.each([
|
||||
[
|
||||
'fontFamily.sans',
|
||||
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
||||
],
|
||||
['maxWidth.xs', '20rem'],
|
||||
['transitionTimingFunction.in-out', 'cubic-bezier(.4, 0, .2, 1)'],
|
||||
|
|
|
|||
|
|
@ -70,11 +70,13 @@ function spacing(
|
|||
// - We can assume that the `--spacing` value is not set to a `0`-like value.
|
||||
// Otherwise `p-<anything>` would calculate as `0` which wouldn't make sense.
|
||||
//
|
||||
// - That means that a value of `0` can be replaced by `0`
|
||||
// - That means that a value of `0` can be replaced by `0px`. It's important
|
||||
// to keep the unit so the value stays a `<length>`, e.g. when assigned to
|
||||
// a custom property and later used inside `calc(…)`.
|
||||
// - That means that a value of `1` can be replaced by `multiplier`
|
||||
let valueDimension = dimensions.get(value)
|
||||
if (valueDimension) {
|
||||
if (valueDimension[0] === 0) return '0'
|
||||
if (valueDimension[0] === 0) return '0px'
|
||||
if (valueDimension[0] === 1) return multiplier
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,11 +213,9 @@ test('Utilities do not show wrapping selector in intellisense', async () => {
|
|||
text-decoration-line: underline;
|
||||
}
|
||||
",
|
||||
".hover\\:line-through {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
"@media (hover: hover) {
|
||||
.hover\\:line-through:hover {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
}
|
||||
",
|
||||
|
|
@ -244,11 +242,9 @@ test('Utilities, when marked as important, show as important in intellisense', a
|
|||
text-decoration-line: underline !important;
|
||||
}
|
||||
",
|
||||
".hover\\:line-through {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
text-decoration-line: line-through !important;
|
||||
}
|
||||
"@media (hover: hover) {
|
||||
.hover\\:line-through:hover {
|
||||
text-decoration-line: line-through !important;
|
||||
}
|
||||
}
|
||||
",
|
||||
|
|
@ -361,48 +357,48 @@ test('Functional utilities from plugins are listed in hovers and completions', a
|
|||
|
||||
expect(design.candidatesToCss(['custom-1-red', 'custom-1-green', 'custom-1-unknown']))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
".custom-1-red {
|
||||
color: #ff0000;
|
||||
}
|
||||
",
|
||||
".custom-1-green {
|
||||
color: #ff0000;
|
||||
}
|
||||
",
|
||||
null,
|
||||
]
|
||||
`)
|
||||
[
|
||||
".custom-1-red {
|
||||
color: #ff0000;
|
||||
}
|
||||
",
|
||||
".custom-1-green {
|
||||
color: #ff0000;
|
||||
}
|
||||
",
|
||||
null,
|
||||
]
|
||||
`)
|
||||
|
||||
expect(design.candidatesToCss(['custom-2-red', 'custom-2-green', 'custom-2-unknown']))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
".custom-2-red {
|
||||
color: #ff0000 / 0%;
|
||||
}
|
||||
",
|
||||
".custom-2-green {
|
||||
color: #ff0000 / 0%;
|
||||
}
|
||||
",
|
||||
null,
|
||||
]
|
||||
`)
|
||||
[
|
||||
".custom-2-red {
|
||||
color: #ff0000 / 0%;
|
||||
}
|
||||
",
|
||||
".custom-2-green {
|
||||
color: #ff0000 / 0%;
|
||||
}
|
||||
",
|
||||
null,
|
||||
]
|
||||
`)
|
||||
|
||||
expect(design.candidatesToCss(['custom-2-red/50', 'custom-2-red/75', 'custom-2-red/unknown']))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
".custom-2-red\\/50 {
|
||||
color: #ff0000 / 50%;
|
||||
}
|
||||
",
|
||||
".custom-2-red\\/75 {
|
||||
color: #ff0000 / 75%;
|
||||
}
|
||||
",
|
||||
null,
|
||||
]
|
||||
`)
|
||||
[
|
||||
".custom-2-red\\/50 {
|
||||
color: #ff0000 / 50%;
|
||||
}
|
||||
",
|
||||
".custom-2-red\\/75 {
|
||||
color: #ff0000 / 75%;
|
||||
}
|
||||
",
|
||||
null,
|
||||
]
|
||||
`)
|
||||
|
||||
let classMap = new Map(design.getClassList())
|
||||
let classNames = Array.from(classMap.keys())
|
||||
|
|
|
|||
|
|
@ -326,14 +326,39 @@ describe('parse', () => {
|
|||
})
|
||||
|
||||
it('parses :nth-child()', () => {
|
||||
// The `An+B` part is not a selector, it stays an opaque value
|
||||
expect(parse(':nth-child(n+1)')).toEqual([
|
||||
{
|
||||
kind: 'function',
|
||||
value: ':nth-child',
|
||||
nodes: [{ kind: 'value', value: 'n+1' }],
|
||||
},
|
||||
])
|
||||
|
||||
// The selector list after `of` is parsed as a selector
|
||||
expect(parse(':nth-child(2 of &)')).toEqual([
|
||||
{
|
||||
kind: 'function',
|
||||
value: ':nth-child',
|
||||
nodes: [
|
||||
{ kind: 'value', value: '2 of ' },
|
||||
{ kind: 'selector', value: '&' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
expect(parse(':nth-child(2n + 1 of .foo, .bar)')).toEqual([
|
||||
{
|
||||
kind: 'function',
|
||||
value: ':nth-child',
|
||||
nodes: [
|
||||
{ kind: 'value', value: '2n + 1 of ' },
|
||||
{
|
||||
kind: 'value',
|
||||
value: 'n+1',
|
||||
kind: 'list',
|
||||
nodes: [
|
||||
{ kind: 'selector', value: '.foo' },
|
||||
{ kind: 'selector', value: '.bar' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -445,7 +470,7 @@ describe('parse', () => {
|
|||
])
|
||||
})
|
||||
|
||||
it('parses universal selector after an attribute selector', () => {
|
||||
it('parses the universal selector after an attribute selector', () => {
|
||||
expect(parse('[data-foo]*')).toEqual([
|
||||
{
|
||||
kind: 'compound',
|
||||
|
|
@ -457,6 +482,42 @@ describe('parse', () => {
|
|||
])
|
||||
})
|
||||
|
||||
it('parses another attribute selector after an attribute selector', () => {
|
||||
expect(parse('[data-foo][data-bar]')).toEqual([
|
||||
{
|
||||
kind: 'compound',
|
||||
nodes: [
|
||||
{ kind: 'selector', value: '[data-foo]' },
|
||||
{ kind: 'selector', value: '[data-bar]' },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('parses a type selector before an attribute selector', () => {
|
||||
expect(parse('div[data-foo]')).toEqual([
|
||||
{
|
||||
kind: 'compound',
|
||||
nodes: [
|
||||
{ kind: 'selector', value: 'div' },
|
||||
{ kind: 'selector', value: '[data-foo]' },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('parses a type selector after an attribute selector', () => {
|
||||
expect(parse('[data-foo]div')).toEqual([
|
||||
{
|
||||
kind: 'compound',
|
||||
nodes: [
|
||||
{ kind: 'selector', value: '[data-foo]' },
|
||||
{ kind: 'selector', value: 'div' },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should parse selector lists as real selectors', () => {
|
||||
expect(parse('.foo[attr], .bar#id, .baz + .qux')).toEqual([
|
||||
{
|
||||
|
|
@ -529,7 +590,11 @@ describe('toCss', () => {
|
|||
})
|
||||
|
||||
it('should print :nth-child()', () => {
|
||||
// The `An+B` part is printed verbatim, whitespace in the selector list
|
||||
// after `of` is normalized
|
||||
expect(toCss(parse(':nth-child(n+1)'))).toBe(':nth-child(n+1)')
|
||||
expect(toCss(parse(':nth-child(+2)'))).toBe(':nth-child(+2)')
|
||||
expect(toCss(parse(':nth-child(2n + 1 of .foo,.bar)'))).toBe(':nth-child(2n + 1 of .foo, .bar)')
|
||||
})
|
||||
|
||||
it('should pretty print a complex selector', () => {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function compound(nodes: SelectorAstNode[]): SelectorCompoundNode {
|
|||
}
|
||||
}
|
||||
|
||||
function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode {
|
||||
export function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode {
|
||||
return {
|
||||
kind: 'function',
|
||||
value,
|
||||
|
|
@ -85,7 +85,7 @@ function list(nodes: SelectorAstNode[]): SelectorListNode {
|
|||
}
|
||||
}
|
||||
|
||||
function selector(value: string): SelectorNode {
|
||||
export function selector(value: string): SelectorNode {
|
||||
return {
|
||||
kind: 'selector',
|
||||
value,
|
||||
|
|
@ -99,6 +99,80 @@ function value(value: string): SelectorValueNode {
|
|||
}
|
||||
}
|
||||
|
||||
export function isUniversalSelector(node: SelectorAstNode): boolean {
|
||||
return node.kind === 'selector' && node.value.charCodeAt(0) === ASTERISK
|
||||
}
|
||||
|
||||
export function isNestingSelector(node: SelectorAstNode): boolean {
|
||||
return node.kind === 'selector' && node.value.charCodeAt(0) === AMPERSAND
|
||||
}
|
||||
|
||||
export function isClassSelector(node: SelectorAstNode): boolean {
|
||||
return node.kind === 'selector' && node.value.charCodeAt(0) === DOT
|
||||
}
|
||||
|
||||
export function isIdSelector(node: SelectorAstNode): boolean {
|
||||
return node.kind === 'selector' && node.value.charCodeAt(0) === HASH
|
||||
}
|
||||
|
||||
export function isPseudoSelector(node: SelectorAstNode): boolean {
|
||||
return node.kind === 'selector' && node.value.charCodeAt(0) === COLON
|
||||
}
|
||||
|
||||
export function isAttributeSelector(node: SelectorAstNode): boolean {
|
||||
return node.kind === 'selector' && node.value.charCodeAt(0) === OPEN_BRACKET
|
||||
}
|
||||
|
||||
export function isTypeSelector(node: SelectorAstNode): boolean {
|
||||
if (node.kind !== 'selector') return false
|
||||
|
||||
switch (node.value.charCodeAt(0)) {
|
||||
case ASTERISK: // Universal selector
|
||||
case AMPERSAND: // Nesting selector
|
||||
case DOT: // Class selector
|
||||
case HASH: // ID selector
|
||||
case COLON: // Pseudo selector
|
||||
case OPEN_BRACKET: // Attribute selector
|
||||
return false
|
||||
|
||||
// We don't fully verify whether this is actually a proper type selector,
|
||||
// but we assume it is one if it's not any of the other ones.
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneAstNode<T extends SelectorAstNode>(node: T): T {
|
||||
switch (node.kind) {
|
||||
case 'combinator':
|
||||
case 'selector':
|
||||
case 'value':
|
||||
return {
|
||||
kind: node.kind,
|
||||
value: node.value,
|
||||
} as T
|
||||
|
||||
case 'complex':
|
||||
case 'compound':
|
||||
case 'list':
|
||||
return {
|
||||
kind: node.kind,
|
||||
nodes: node.nodes.map(cloneAstNode),
|
||||
} as T
|
||||
|
||||
case 'function':
|
||||
return {
|
||||
kind: node.kind,
|
||||
value: node.value,
|
||||
nodes: node.nodes.map(cloneAstNode),
|
||||
} satisfies SelectorFunctionNode as T
|
||||
|
||||
default:
|
||||
node satisfies never
|
||||
throw new Error(`Unknown node kind: ${(node as any).kind}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function toCss(ast: SelectorAstNode[], minify = false) {
|
||||
let css = ''
|
||||
for (let node of ast) {
|
||||
|
|
@ -141,10 +215,10 @@ const CLOSE_PAREN = 0x29
|
|||
const COLON = 0x3a
|
||||
const COMMA = 0x2c
|
||||
const DOUBLE_QUOTE = 0x22
|
||||
const FULL_STOP = 0x2e
|
||||
const DOT = 0x2e
|
||||
const GREATER_THAN = 0x3e
|
||||
const NEWLINE = 0x0a
|
||||
const NUMBER_SIGN = 0x23
|
||||
const HASH = 0x23
|
||||
const OPEN_BRACKET = 0x5b
|
||||
const OPEN_PAREN = 0x28
|
||||
const PLUS = 0x2b
|
||||
|
|
@ -196,20 +270,14 @@ export function parse(input: string) {
|
|||
let currentChar = input.charCodeAt(i)
|
||||
|
||||
switch (currentChar) {
|
||||
// E.g.:
|
||||
// Handle selector lists
|
||||
//
|
||||
// ```css
|
||||
// .foo .bar
|
||||
// .foo, .bar {}
|
||||
// ^
|
||||
//
|
||||
// .foo > .bar
|
||||
// ^^^
|
||||
// ```
|
||||
case COMMA: {
|
||||
// Flush remaining buffer, mark it as a selector
|
||||
//
|
||||
// Combinators are handled separately, and functions end with `)` which
|
||||
// means that the `buffer` will be empty at that point.
|
||||
// Flush remaining buffer as a selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
buffer = ''
|
||||
|
|
@ -244,19 +312,30 @@ export function parse(input: string) {
|
|||
break
|
||||
}
|
||||
|
||||
// Handle combinators
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// .foo .bar
|
||||
// ^
|
||||
//
|
||||
// .foo > .bar
|
||||
// ^^^
|
||||
// ```
|
||||
case GREATER_THAN:
|
||||
case NEWLINE:
|
||||
case SPACE:
|
||||
case PLUS:
|
||||
case TAB:
|
||||
case TILDE: {
|
||||
// 1. Handle everything before the combinator as a selector
|
||||
// Flush remaining buffer as a selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
// 2. Look ahead and find the end of the combinator
|
||||
// Look ahead and find the end of the combinator
|
||||
let start = i
|
||||
let end = i + 1
|
||||
for (; end < input.length; end++) {
|
||||
|
|
@ -288,7 +367,7 @@ export function parse(input: string) {
|
|||
break
|
||||
}
|
||||
|
||||
// Start of a function call.
|
||||
// Start of a function call
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
|
|
@ -300,7 +379,7 @@ export function parse(input: string) {
|
|||
let node = fun(buffer, [])
|
||||
buffer = ''
|
||||
|
||||
// If the function is not one of the following, we combine all it's
|
||||
// If the function is not one of the following, we combine all its
|
||||
// contents into a single value node
|
||||
if (
|
||||
node.value !== ':not' &&
|
||||
|
|
@ -312,7 +391,7 @@ export function parse(input: string) {
|
|||
let start = i + 1
|
||||
let nesting = 0
|
||||
|
||||
// Find the closing bracket.
|
||||
// Find the closing bracket
|
||||
for (let j = i + 1; j < input.length; j++) {
|
||||
peekChar = input.charCodeAt(j)
|
||||
if (peekChar === OPEN_PAREN) {
|
||||
|
|
@ -329,7 +408,39 @@ export function parse(input: string) {
|
|||
}
|
||||
let end = i
|
||||
|
||||
node.nodes.push(value(input.slice(start, end)))
|
||||
let contents = input.slice(start, end)
|
||||
|
||||
// `:nth-child(…)` and `:nth-last-child(…)` can contain an
|
||||
// `of <complex-selector-list>` clause. The selector list must be
|
||||
// parsed (e.g. to be able to substitute `&`), but the `An+B` part
|
||||
// is not a selector so it stays an opaque value node. E.g.:
|
||||
//
|
||||
// ```css
|
||||
// :nth-child(2n + 1 of .foo, .bar)
|
||||
// ^^^^^^^^^^ value
|
||||
// ^^^^^^^^^^ selector list
|
||||
// ```
|
||||
if (node.value === ':nth-child' || node.value === ':nth-last-child') {
|
||||
let idx = contents.indexOf('of ')
|
||||
if (idx !== -1) {
|
||||
node.nodes.push(
|
||||
value(
|
||||
contents.slice(0, idx + 3), // value `2n + 1 of `
|
||||
),
|
||||
...parse(
|
||||
contents.slice(idx + 3), // `.foo, .bar`
|
||||
),
|
||||
)
|
||||
buffer = ''
|
||||
i = end
|
||||
|
||||
append(node)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
node.nodes.push(value(contents))
|
||||
buffer = ''
|
||||
i = end
|
||||
|
||||
|
|
@ -347,7 +458,7 @@ export function parse(input: string) {
|
|||
break
|
||||
}
|
||||
|
||||
// End of a function call.
|
||||
// End of a function call
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
|
|
@ -356,7 +467,7 @@ export function parse(input: string) {
|
|||
// ^
|
||||
// ```
|
||||
case CLOSE_PAREN: {
|
||||
// Handle everything before the closing paren a selector
|
||||
// Flush remaining buffer as a selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
buffer = ''
|
||||
|
|
@ -376,7 +487,7 @@ export function parse(input: string) {
|
|||
break
|
||||
}
|
||||
|
||||
// Split compound selectors.
|
||||
// Split compound selectors
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
|
|
@ -384,16 +495,16 @@ export function parse(input: string) {
|
|||
// .foo.bar
|
||||
// ^
|
||||
// ```
|
||||
case FULL_STOP:
|
||||
case DOT:
|
||||
case COLON:
|
||||
case NUMBER_SIGN: {
|
||||
case HASH: {
|
||||
if (currentChar === COLON && buffer === ':') {
|
||||
buffer += input[i]
|
||||
break
|
||||
}
|
||||
|
||||
// Handle everything before the combinator as a selector and
|
||||
// start a new selector
|
||||
// Handle everything before the combinator as a selector and start a new
|
||||
// selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
}
|
||||
|
|
@ -401,7 +512,7 @@ export function parse(input: string) {
|
|||
break
|
||||
}
|
||||
|
||||
// Start of an attribute selector.
|
||||
// Start of an attribute selector
|
||||
//
|
||||
// NOTE: Right now we don't care about the individual parts of the
|
||||
// attribute selector, we just want to find the matching closing bracket.
|
||||
|
|
@ -410,16 +521,16 @@ export function parse(input: string) {
|
|||
// future, then we can use the `AttributeSelectorParser` here (and even
|
||||
// inline it if needed)
|
||||
case OPEN_BRACKET: {
|
||||
// Handle everything before the combinator as a selector
|
||||
// Flush remaining buffer as a selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
buffer = ''
|
||||
}
|
||||
buffer = ''
|
||||
|
||||
let start = i
|
||||
let nesting = 0
|
||||
|
||||
// Find the closing bracket.
|
||||
// Find the closing bracket
|
||||
for (let j = i + 1; j < input.length; j++) {
|
||||
peekChar = input.charCodeAt(j)
|
||||
if (peekChar === OPEN_BRACKET) {
|
||||
|
|
@ -435,12 +546,11 @@ export function parse(input: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Adjust `buffer` to include the string.
|
||||
buffer += input.slice(start, i + 1)
|
||||
append(selector(input.slice(start, i + 1)))
|
||||
break
|
||||
}
|
||||
|
||||
// Start of a string.
|
||||
// Start of a string
|
||||
case SINGLE_QUOTE:
|
||||
case DOUBLE_QUOTE: {
|
||||
let start = i
|
||||
|
|
@ -452,43 +562,43 @@ export function parse(input: string) {
|
|||
//
|
||||
// ```css
|
||||
// "This is a string with a 'quote' in it"
|
||||
// ^ ^ -> These are not the end of the string.
|
||||
// ^ ^ -> These are not the end of the string
|
||||
// ```
|
||||
for (let j = i + 1; j < input.length; j++) {
|
||||
peekChar = input.charCodeAt(j)
|
||||
// Current character is a `\` therefore the next character is escaped.
|
||||
// Current character is a `\` therefore the next character is escaped
|
||||
if (peekChar === BACKSLASH) {
|
||||
j += 1
|
||||
}
|
||||
|
||||
// End of the string.
|
||||
// End of the string
|
||||
else if (peekChar === currentChar) {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust `buffer` to include the string.
|
||||
// Adjust `buffer` to include the string
|
||||
buffer += input.slice(start, i + 1)
|
||||
break
|
||||
}
|
||||
|
||||
// Nesting `&` is always a new selector.
|
||||
// Universal `*` is always a new selector.
|
||||
// Nesting `&` is always a new selector
|
||||
// Universal `*` is always a new selector
|
||||
case AMPERSAND:
|
||||
case ASTERISK: {
|
||||
// 1. Handle everything before the combinator as a selector
|
||||
// Flush remaining buffer as a selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
buffer = ''
|
||||
}
|
||||
|
||||
// 2. Handle the `&` or `*` as a selector on its own
|
||||
// Handle the `&` or `*` as a selector on its own
|
||||
append(selector(input[i]))
|
||||
break
|
||||
}
|
||||
|
||||
// Escaped characters.
|
||||
// Escaped characters
|
||||
case BACKSLASH: {
|
||||
buffer += input[i] + input[i + 1]
|
||||
i += 1
|
||||
|
|
@ -502,7 +612,7 @@ export function parse(input: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Collect the remainder as a word
|
||||
// Collect the remainder as a selector
|
||||
if (buffer.length > 0) {
|
||||
append(selector(buffer))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,14 +117,14 @@ test('source maps trace back to @import location', async ({ expect }) => {
|
|||
| --- theme.css ---
|
||||
3 :root, :host { | 1 @theme default {
|
||||
^^^^^^^^^^^^^ C @ 3:2-15 | ^^^^^^^^^^^^^^^ C @ 1:0-15
|
||||
4 --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'... | 2 --font-sans:
|
||||
4 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'N... | 2 --font-sans:
|
||||
^ D @ 4:4 | ^ D @ 2:2
|
||||
4 --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'... | 3 ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI S...
|
||||
4 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'N... | 3 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Ar...
|
||||
^ E @ 4:4 | ^ E @ 3:0
|
||||
4 --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'... | 4 'Noto Color Emoji';
|
||||
4 --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'N... | 4 sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... F @ 4:4-5:0 | ^ F @ 4:0
|
||||
5 'Noto Color Emoji'; | 4 'Noto Color Emoji';
|
||||
^ G @ 5:22-6:0 | ^ G @ 4:22
|
||||
5 sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; | 4 sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
... G @ 5:92-6:0 | ... G @ 4:92
|
||||
| 5 --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
6 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', '... | 6 --font-mono:
|
||||
^ H @ 6:4 | ^ H @ 6:2
|
||||
|
|
@ -176,398 +176,408 @@ test('source maps trace back to @import location', async ({ expect }) => {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AA @ 21:4-34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AA @ 31:2-32
|
||||
22 tab-size: 4; | 32 tab-size: 4; /* 3 */
|
||||
^^^^^^^^^^^ AB @ 22:4-15 | ^^^^^^^^^^^ AB @ 32:2-13
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 33 font-family: --theme(
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 33 font-family: --theme(
|
||||
^ AC @ 23:4 | ^ AC @ 33:2
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 34 --default-font-family,
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 34 --default-font-family,
|
||||
^ AD @ 23:4 | ^ AD @ 34:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 35 ui-sans-serif,
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 35 -apple-system,
|
||||
^ AE @ 23:4 | ^ AE @ 35:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 36 system-ui,
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 36 BlinkMacSystemFont,
|
||||
^ AF @ 23:4 | ^ AF @ 36:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 37 sans-serif,
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 37 'Segoe UI',
|
||||
^ AG @ 23:4 | ^ AG @ 37:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 38 'Apple Color Emoji',
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 38 Roboto,
|
||||
^ AH @ 23:4 | ^ AH @ 38:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 39 'Segoe UI Emoji',
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 39 'Helvetica Neue',
|
||||
^ AI @ 23:4 | ^ AI @ 39:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 40 'Segoe UI Symbol',
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 40 'Noto Sans',
|
||||
^ AJ @ 23:4 | ^ AJ @ 40:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 41 'Noto Color Emoji'
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 41 Arial,
|
||||
^ AK @ 23:4 | ^ AK @ 41:0
|
||||
23 font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Col... | 42 ); /* 4 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... AL @ 23:4-159 | ^^^ AL @ 42:0-3
|
||||
24 font-feature-settings: var(--default-font-feature-settings, normal); | 43 font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AM @ 24:4-71 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AM @ 43:2-73
|
||||
25 font-variation-settings: var(--default-font-variation-settings, normal); | 44 font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AN @ 25:4-75 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AN @ 44:2-77
|
||||
26 -webkit-tap-highlight-color: transparent; | 45 -webkit-tap-highlight-color: transparent; /* 7 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AO @ 26:4-44 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AO @ 45:2-42
|
||||
| 46 }
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 42 sans-serif,
|
||||
^ AL @ 23:4 | ^ AL @ 42:0
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 43 'Apple Color Emoji',
|
||||
^ AM @ 23:4 | ^ AM @ 43:0
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 44 'Segoe UI Emoji',
|
||||
^ AN @ 23:4 | ^ AN @ 44:0
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 45 'Segoe UI Symbol',
|
||||
^ AO @ 23:4 | ^ AO @ 45:0
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 46 'Noto Color Emoji'
|
||||
^ AP @ 23:4 | ^ AP @ 46:0
|
||||
23 font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', R... | 47 ); /* 4 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... AQ @ 23:4-226 | ^^^ AQ @ 47:0-3
|
||||
24 font-feature-settings: var(--default-font-feature-settings, normal); | 48 font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AR @ 24:4-71 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AR @ 48:2-73
|
||||
25 font-variation-settings: var(--default-font-variation-settings, normal); | 49 font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AS @ 25:4-75 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AS @ 49:2-77
|
||||
26 -webkit-tap-highlight-color: transparent; | 50 -webkit-tap-highlight-color: transparent; /* 7 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AT @ 26:4-44 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AT @ 50:2-42
|
||||
| 51 }
|
||||
27 } |
|
||||
28 hr { | 54 hr {
|
||||
^^^ AP @ 28:2-5 | ^^^ AP @ 54:0-3
|
||||
29 height: 0; | 55 height: 0; /* 1 */
|
||||
^^^^^^^^^ AQ @ 29:4-13 | ^^^^^^^^^ AQ @ 55:2-11
|
||||
30 color: inherit; | 56 color: inherit; /* 2 */
|
||||
^^^^^^^^^^^^^^ AR @ 30:4-18 | ^^^^^^^^^^^^^^ AR @ 56:2-16
|
||||
31 border-top-width: 1px; | 57 border-top-width: 1px; /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^ AS @ 31:4-25 | ^^^^^^^^^^^^^^^^^^^^^ AS @ 57:2-23
|
||||
| 58 }
|
||||
28 hr { | 59 hr {
|
||||
^^^ AU @ 28:2-5 | ^^^ AU @ 59:0-3
|
||||
29 height: 0; | 60 height: 0; /* 1 */
|
||||
^^^^^^^^^ AV @ 29:4-13 | ^^^^^^^^^ AV @ 60:2-11
|
||||
30 color: inherit; | 61 color: inherit; /* 2 */
|
||||
^^^^^^^^^^^^^^ AW @ 30:4-18 | ^^^^^^^^^^^^^^ AW @ 61:2-16
|
||||
31 border-top-width: 1px; | 62 border-top-width: 1px; /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^ AX @ 31:4-25 | ^^^^^^^^^^^^^^^^^^^^^ AX @ 62:2-23
|
||||
| 63 }
|
||||
32 } |
|
||||
33 abbr:where([title]) { | 64 abbr:where([title]) {
|
||||
^^^^^^^^^^^^^^^^^^^^ AT @ 33:2-22 | ^^^^^^^^^^^^^^^^^^^^ AT @ 64:0-20
|
||||
34 -webkit-text-decoration: underline dotted; | 65 -webkit-text-decoration: underline dotted;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AU @ 34:4-45 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AU @ 65:2-43
|
||||
35 text-decoration: underline dotted; | 66 text-decoration: underline dotted;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AV @ 35:4-37 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AV @ 66:2-35
|
||||
| 67 }
|
||||
33 abbr:where([title]) { | 69 abbr:where([title]) {
|
||||
^^^^^^^^^^^^^^^^^^^^ AY @ 33:2-22 | ^^^^^^^^^^^^^^^^^^^^ AY @ 69:0-20
|
||||
34 -webkit-text-decoration: underline dotted; | 70 -webkit-text-decoration: underline dotted;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AZ @ 34:4-45 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AZ @ 70:2-43
|
||||
35 text-decoration: underline dotted; | 71 text-decoration: underline dotted;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BA @ 35:4-37 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BA @ 71:2-35
|
||||
| 72 }
|
||||
36 } |
|
||||
37 h1, h2, h3, h4, h5, h6 { | 73 h1,
|
||||
^ AW @ 37:2 | ^ AW @ 73:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 74 h2,
|
||||
^ AX @ 37:2 | ^ AX @ 74:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 75 h3,
|
||||
^ AY @ 37:2 | ^ AY @ 75:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 76 h4,
|
||||
^ AZ @ 37:2 | ^ AZ @ 76:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 77 h5,
|
||||
^ BA @ 37:2 | ^ BA @ 77:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 78 h6 {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ BB @ 37:2-25 | ^^^ BB @ 78:0-3
|
||||
38 font-size: inherit; | 79 font-size: inherit;
|
||||
^^^^^^^^^^^^^^^^^^ BC @ 38:4-22 | ^^^^^^^^^^^^^^^^^^ BC @ 79:2-20
|
||||
39 font-weight: inherit; | 80 font-weight: inherit;
|
||||
^^^^^^^^^^^^^^^^^^^^ BD @ 39:4-24 | ^^^^^^^^^^^^^^^^^^^^ BD @ 80:2-22
|
||||
| 81 }
|
||||
37 h1, h2, h3, h4, h5, h6 { | 78 h1,
|
||||
^ BB @ 37:2 | ^ BB @ 78:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 79 h2,
|
||||
^ BC @ 37:2 | ^ BC @ 79:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 80 h3,
|
||||
^ BD @ 37:2 | ^ BD @ 80:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 81 h4,
|
||||
^ BE @ 37:2 | ^ BE @ 81:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 82 h5,
|
||||
^ BF @ 37:2 | ^ BF @ 82:0
|
||||
37 h1, h2, h3, h4, h5, h6 { | 83 h6 {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ BG @ 37:2-25 | ^^^ BG @ 83:0-3
|
||||
38 font-size: inherit; | 84 font-size: inherit;
|
||||
^^^^^^^^^^^^^^^^^^ BH @ 38:4-22 | ^^^^^^^^^^^^^^^^^^ BH @ 84:2-20
|
||||
39 font-weight: inherit; | 85 font-weight: inherit;
|
||||
^^^^^^^^^^^^^^^^^^^^ BI @ 39:4-24 | ^^^^^^^^^^^^^^^^^^^^ BI @ 85:2-22
|
||||
| 86 }
|
||||
40 } |
|
||||
41 a { | 87 a {
|
||||
^^ BE @ 41:2-4 | ^^ BE @ 87:0-2
|
||||
42 color: inherit; | 88 color: inherit;
|
||||
^^^^^^^^^^^^^^ BF @ 42:4-18 | ^^^^^^^^^^^^^^ BF @ 88:2-16
|
||||
43 -webkit-text-decoration: inherit; | 89 -webkit-text-decoration: inherit;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BG @ 43:4-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BG @ 89:2-34
|
||||
44 text-decoration: inherit; | 90 text-decoration: inherit;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ BH @ 44:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ BH @ 90:2-26
|
||||
| 91 }
|
||||
41 a { | 92 a {
|
||||
^^ BJ @ 41:2-4 | ^^ BJ @ 92:0-2
|
||||
42 color: inherit; | 93 color: inherit;
|
||||
^^^^^^^^^^^^^^ BK @ 42:4-18 | ^^^^^^^^^^^^^^ BK @ 93:2-16
|
||||
43 -webkit-text-decoration: inherit; | 94 -webkit-text-decoration: inherit;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BL @ 43:4-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ BL @ 94:2-34
|
||||
44 text-decoration: inherit; | 95 text-decoration: inherit;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ BM @ 44:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ BM @ 95:2-26
|
||||
| 96 }
|
||||
45 } |
|
||||
46 b, strong { | 97 b,
|
||||
^ BI @ 46:2 | ^ BI @ 97:0
|
||||
46 b, strong { | 98 strong {
|
||||
^^^^^^^^^^ BJ @ 46:2-12 | ^^^^^^^ BJ @ 98:0-7
|
||||
47 font-weight: bolder; | 99 font-weight: bolder;
|
||||
^^^^^^^^^^^^^^^^^^^ BK @ 47:4-23 | ^^^^^^^^^^^^^^^^^^^ BK @ 99:2-21
|
||||
| 100 }
|
||||
46 b, strong { | 102 b,
|
||||
^ BN @ 46:2 | ^ BN @ 102:0
|
||||
46 b, strong { | 103 strong {
|
||||
^^^^^^^^^^ BO @ 46:2-12 | ^^^^^^^ BO @ 103:0-7
|
||||
47 font-weight: bolder; | 104 font-weight: bolder;
|
||||
^^^^^^^^^^^^^^^^^^^ BP @ 47:4-23 | ^^^^^^^^^^^^^^^^^^^ BP @ 104:2-21
|
||||
| 105 }
|
||||
48 } |
|
||||
49 code, kbd, samp, pre { | 109 code,
|
||||
^ BL @ 49:2 | ^ BL @ 109:0
|
||||
49 code, kbd, samp, pre { | 110 kbd,
|
||||
^ BM @ 49:2 | ^ BM @ 110:0
|
||||
49 code, kbd, samp, pre { | 111 samp,
|
||||
^ BN @ 49:2 | ^ BN @ 111:0
|
||||
49 code, kbd, samp, pre { | 112 pre {
|
||||
^^^^^^^^^^^^^^^^^^^^^ BO @ 49:2-23 | ^^^^ BO @ 112:0-4
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 113 font-family: --theme(
|
||||
^ BP @ 50:4 | ^ BP @ 113:2
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 114 --default-mono-font-family,
|
||||
^ BQ @ 50:4 | ^ BQ @ 114:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 115 ui-monospace,
|
||||
^ BR @ 50:4 | ^ BR @ 115:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 116 SFMono-Regular,
|
||||
^ BS @ 50:4 | ^ BS @ 116:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 117 Menlo,
|
||||
^ BT @ 50:4 | ^ BT @ 117:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 118 Monaco,
|
||||
^ BU @ 50:4 | ^ BU @ 118:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 119 Consolas,
|
||||
49 code, kbd, samp, pre { | 114 code,
|
||||
^ BQ @ 49:2 | ^ BQ @ 114:0
|
||||
49 code, kbd, samp, pre { | 115 kbd,
|
||||
^ BR @ 49:2 | ^ BR @ 115:0
|
||||
49 code, kbd, samp, pre { | 116 samp,
|
||||
^ BS @ 49:2 | ^ BS @ 116:0
|
||||
49 code, kbd, samp, pre { | 117 pre {
|
||||
^^^^^^^^^^^^^^^^^^^^^ BT @ 49:2-23 | ^^^^ BT @ 117:0-4
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 118 font-family: --theme(
|
||||
^ BU @ 50:4 | ^ BU @ 118:2
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 119 --default-mono-font-family,
|
||||
^ BV @ 50:4 | ^ BV @ 119:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 120 'Liberation Mono',
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 120 ui-monospace,
|
||||
^ BW @ 50:4 | ^ BW @ 120:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 121 'Courier New',
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 121 SFMono-Regular,
|
||||
^ BX @ 50:4 | ^ BX @ 121:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 122 monospace
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 122 Menlo,
|
||||
^ BY @ 50:4 | ^ BY @ 122:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 123 ); /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... BZ @ 50:4-148 | ^^^ BZ @ 123:0-3
|
||||
51 font-feature-settings: var(--default-mono-font-feature-settings, normal); | 124 font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CA @ 51:4-76 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CA @ 124:2-78
|
||||
52 font-variation-settings: var(--default-mono-font-variation-settings, normal); | 125 font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CB @ 52:4-80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... CB @ 125:2-82
|
||||
53 font-size: 1em; | 126 font-size: 1em; /* 4 */
|
||||
^^^^^^^^^^^^^^ CC @ 53:4-18 | ^^^^^^^^^^^^^^ CC @ 126:2-16
|
||||
| 127 }
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 123 Monaco,
|
||||
^ BZ @ 50:4 | ^ BZ @ 123:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 124 Consolas,
|
||||
^ CA @ 50:4 | ^ CA @ 124:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 125 'Liberation Mono',
|
||||
^ CB @ 50:4 | ^ CB @ 125:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 126 'Courier New',
|
||||
^ CC @ 50:4 | ^ CC @ 126:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 127 monospace
|
||||
^ CD @ 50:4 | ^ CD @ 127:0
|
||||
50 font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco... | 128 ); /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... CE @ 50:4-148 | ^^^ CE @ 128:0-3
|
||||
51 font-feature-settings: var(--default-mono-font-feature-settings, normal); | 129 font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CF @ 51:4-76 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CF @ 129:2-78
|
||||
52 font-variation-settings: var(--default-mono-font-variation-settings, normal); | 130 font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CG @ 52:4-80 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... CG @ 130:2-82
|
||||
53 font-size: 1em; | 131 font-size: 1em; /* 4 */
|
||||
^^^^^^^^^^^^^^ CH @ 53:4-18 | ^^^^^^^^^^^^^^ CH @ 131:2-16
|
||||
| 132 }
|
||||
54 } |
|
||||
55 small { | 133 small {
|
||||
^^^^^^ CD @ 55:2-8 | ^^^^^^ CD @ 133:0-6
|
||||
56 font-size: 80%; | 134 font-size: 80%;
|
||||
^^^^^^^^^^^^^^ CE @ 56:4-18 | ^^^^^^^^^^^^^^ CE @ 134:2-16
|
||||
| 135 }
|
||||
55 small { | 138 small {
|
||||
^^^^^^ CI @ 55:2-8 | ^^^^^^ CI @ 138:0-6
|
||||
56 font-size: 80%; | 139 font-size: 80%;
|
||||
^^^^^^^^^^^^^^ CJ @ 56:4-18 | ^^^^^^^^^^^^^^ CJ @ 139:2-16
|
||||
| 140 }
|
||||
57 } |
|
||||
58 sub, sup { | 141 sub,
|
||||
^ CF @ 58:2 | ^ CF @ 141:0
|
||||
58 sub, sup { | 142 sup {
|
||||
^^^^^^^^^ CG @ 58:2-11 | ^^^^ CG @ 142:0-4
|
||||
59 font-size: 75%; | 143 font-size: 75%;
|
||||
^^^^^^^^^^^^^^ CH @ 59:4-18 | ^^^^^^^^^^^^^^ CH @ 143:2-16
|
||||
60 line-height: 0; | 144 line-height: 0;
|
||||
^^^^^^^^^^^^^^ CI @ 60:4-18 | ^^^^^^^^^^^^^^ CI @ 144:2-16
|
||||
61 position: relative; | 145 position: relative;
|
||||
^^^^^^^^^^^^^^^^^^ CJ @ 61:4-22 | ^^^^^^^^^^^^^^^^^^ CJ @ 145:2-20
|
||||
62 vertical-align: baseline; | 146 vertical-align: baseline;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ CK @ 62:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ CK @ 146:2-26
|
||||
| 147 }
|
||||
58 sub, sup { | 146 sub,
|
||||
^ CK @ 58:2 | ^ CK @ 146:0
|
||||
58 sub, sup { | 147 sup {
|
||||
^^^^^^^^^ CL @ 58:2-11 | ^^^^ CL @ 147:0-4
|
||||
59 font-size: 75%; | 148 font-size: 75%;
|
||||
^^^^^^^^^^^^^^ CM @ 59:4-18 | ^^^^^^^^^^^^^^ CM @ 148:2-16
|
||||
60 line-height: 0; | 149 line-height: 0;
|
||||
^^^^^^^^^^^^^^ CN @ 60:4-18 | ^^^^^^^^^^^^^^ CN @ 149:2-16
|
||||
61 position: relative; | 150 position: relative;
|
||||
^^^^^^^^^^^^^^^^^^ CO @ 61:4-22 | ^^^^^^^^^^^^^^^^^^ CO @ 150:2-20
|
||||
62 vertical-align: baseline; | 151 vertical-align: baseline;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ CP @ 62:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ CP @ 151:2-26
|
||||
| 152 }
|
||||
63 } |
|
||||
64 sub { | 149 sub {
|
||||
^^^^ CL @ 64:2-6 | ^^^^ CL @ 149:0-4
|
||||
65 bottom: -0.25em; | 150 bottom: -0.25em;
|
||||
^^^^^^^^^^^^^^^ CM @ 65:4-19 | ^^^^^^^^^^^^^^^ CM @ 150:2-17
|
||||
| 151 }
|
||||
64 sub { | 154 sub {
|
||||
^^^^ CQ @ 64:2-6 | ^^^^ CQ @ 154:0-4
|
||||
65 bottom: -0.25em; | 155 bottom: -0.25em;
|
||||
^^^^^^^^^^^^^^^ CR @ 65:4-19 | ^^^^^^^^^^^^^^^ CR @ 155:2-17
|
||||
| 156 }
|
||||
66 } |
|
||||
67 sup { | 153 sup {
|
||||
^^^^ CN @ 67:2-6 | ^^^^ CN @ 153:0-4
|
||||
68 top: -0.5em; | 154 top: -0.5em;
|
||||
^^^^^^^^^^^ CO @ 68:4-15 | ^^^^^^^^^^^ CO @ 154:2-13
|
||||
| 155 }
|
||||
67 sup { | 158 sup {
|
||||
^^^^ CS @ 67:2-6 | ^^^^ CS @ 158:0-4
|
||||
68 top: -0.5em; | 159 top: -0.5em;
|
||||
^^^^^^^^^^^ CT @ 68:4-15 | ^^^^^^^^^^^ CT @ 159:2-13
|
||||
| 160 }
|
||||
69 } |
|
||||
70 table { | 163 table {
|
||||
^^^^^^ CP @ 70:2-8 | ^^^^^^ CP @ 163:0-6
|
||||
71 text-indent: 0; | 164 text-indent: 0; /* 1 */
|
||||
^^^^^^^^^^^^^^ CQ @ 71:4-18 | ^^^^^^^^^^^^^^ CQ @ 164:2-16
|
||||
72 border-color: inherit; | 165 border-color: inherit; /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^^^ CR @ 72:4-25 | ^^^^^^^^^^^^^^^^^^^^^ CR @ 165:2-23
|
||||
73 border-collapse: collapse; | 166 border-collapse: collapse; /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^ CS @ 73:4-29 | ^^^^^^^^^^^^^^^^^^^^^^^^^ CS @ 166:2-27
|
||||
| 167 }
|
||||
70 table { | 168 table {
|
||||
^^^^^^ CU @ 70:2-8 | ^^^^^^ CU @ 168:0-6
|
||||
71 text-indent: 0; | 169 text-indent: 0; /* 1 */
|
||||
^^^^^^^^^^^^^^ CV @ 71:4-18 | ^^^^^^^^^^^^^^ CV @ 169:2-16
|
||||
72 border-color: inherit; | 170 border-color: inherit; /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^^^ CW @ 72:4-25 | ^^^^^^^^^^^^^^^^^^^^^ CW @ 170:2-23
|
||||
73 border-collapse: collapse; | 171 border-collapse: collapse; /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^ CX @ 73:4-29 | ^^^^^^^^^^^^^^^^^^^^^^^^^ CX @ 171:2-27
|
||||
| 172 }
|
||||
74 } |
|
||||
75 :-moz-focusring { | 173 :-moz-focusring {
|
||||
^^^^^^^^^^^^^^^^ CT @ 75:2-18 | ^^^^^^^^^^^^^^^^ CT @ 173:0-16
|
||||
76 outline: auto; | 174 outline: auto;
|
||||
^^^^^^^^^^^^^ CU @ 76:4-17 | ^^^^^^^^^^^^^ CU @ 174:2-15
|
||||
| 175 }
|
||||
75 :-moz-focusring:where(:not(iframe)) { | 178 :-moz-focusring:where(:not(iframe)) {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CY @ 75:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CY @ 178:0-36
|
||||
76 outline: auto; | 179 outline: auto;
|
||||
^^^^^^^^^^^^^ CZ @ 76:4-17 | ^^^^^^^^^^^^^ CZ @ 179:2-15
|
||||
| 180 }
|
||||
77 } |
|
||||
78 progress { | 181 progress {
|
||||
^^^^^^^^^ CV @ 78:2-11 | ^^^^^^^^^ CV @ 181:0-9
|
||||
79 vertical-align: baseline; | 182 vertical-align: baseline;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ CW @ 79:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ CW @ 182:2-26
|
||||
| 183 }
|
||||
78 progress { | 186 progress {
|
||||
^^^^^^^^^ DA @ 78:2-11 | ^^^^^^^^^ DA @ 186:0-9
|
||||
79 vertical-align: baseline; | 187 vertical-align: baseline;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ DB @ 79:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ DB @ 187:2-26
|
||||
| 188 }
|
||||
80 } |
|
||||
81 summary { | 189 summary {
|
||||
^^^^^^^^ CX @ 81:2-10 | ^^^^^^^^ CX @ 189:0-8
|
||||
82 display: list-item; | 190 display: list-item;
|
||||
^^^^^^^^^^^^^^^^^^ CY @ 82:4-22 | ^^^^^^^^^^^^^^^^^^ CY @ 190:2-20
|
||||
| 191 }
|
||||
81 summary { | 194 summary {
|
||||
^^^^^^^^ DC @ 81:2-10 | ^^^^^^^^ DC @ 194:0-8
|
||||
82 display: list-item; | 195 display: list-item;
|
||||
^^^^^^^^^^^^^^^^^^ DD @ 82:4-22 | ^^^^^^^^^^^^^^^^^^ DD @ 195:2-20
|
||||
| 196 }
|
||||
83 } |
|
||||
84 ol, ul, menu { | 197 ol,
|
||||
^ CZ @ 84:2 | ^ CZ @ 197:0
|
||||
84 ol, ul, menu { | 198 ul,
|
||||
^ DA @ 84:2 | ^ DA @ 198:0
|
||||
84 ol, ul, menu { | 199 menu {
|
||||
^^^^^^^^^^^^^ DB @ 84:2-15 | ^^^^^ DB @ 199:0-5
|
||||
85 list-style: none; | 200 list-style: none;
|
||||
^^^^^^^^^^^^^^^^ DC @ 85:4-20 | ^^^^^^^^^^^^^^^^ DC @ 200:2-18
|
||||
| 201 }
|
||||
84 ol, ul, menu { | 202 ol,
|
||||
^ DE @ 84:2 | ^ DE @ 202:0
|
||||
84 ol, ul, menu { | 203 ul,
|
||||
^ DF @ 84:2 | ^ DF @ 203:0
|
||||
84 ol, ul, menu { | 204 menu {
|
||||
^^^^^^^^^^^^^ DG @ 84:2-15 | ^^^^^ DG @ 204:0-5
|
||||
85 list-style: none; | 205 list-style: none;
|
||||
^^^^^^^^^^^^^^^^ DH @ 85:4-20 | ^^^^^^^^^^^^^^^^ DH @ 205:2-18
|
||||
| 206 }
|
||||
86 } |
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 209 img,
|
||||
^ DD @ 87:2 | ^ DD @ 209:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 210 svg,
|
||||
^ DE @ 87:2 | ^ DE @ 210:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 211 video,
|
||||
^ DF @ 87:2 | ^ DF @ 211:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 212 canvas,
|
||||
^ DG @ 87:2 | ^ DG @ 212:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 213 audio,
|
||||
^ DH @ 87:2 | ^ DH @ 213:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 214 iframe,
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 214 img,
|
||||
^ DI @ 87:2 | ^ DI @ 214:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 215 embed,
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 215 svg,
|
||||
^ DJ @ 87:2 | ^ DJ @ 215:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 216 object {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DK @ 87:2-56 | ^^^^^^^ DK @ 216:0-7
|
||||
88 display: block; | 217 display: block; /* 1 */
|
||||
^^^^^^^^^^^^^^ DL @ 88:4-18 | ^^^^^^^^^^^^^^ DL @ 217:2-16
|
||||
89 vertical-align: middle; | 218 vertical-align: middle; /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^ DM @ 89:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ DM @ 218:2-24
|
||||
| 219 }
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 216 video,
|
||||
^ DK @ 87:2 | ^ DK @ 216:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 217 canvas,
|
||||
^ DL @ 87:2 | ^ DL @ 217:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 218 audio,
|
||||
^ DM @ 87:2 | ^ DM @ 218:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 219 iframe,
|
||||
^ DN @ 87:2 | ^ DN @ 219:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 220 embed,
|
||||
^ DO @ 87:2 | ^ DO @ 220:0
|
||||
87 img, svg, video, canvas, audio, iframe, embed, object { | 221 object {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DP @ 87:2-56 | ^^^^^^^ DP @ 221:0-7
|
||||
88 display: block; | 222 display: block; /* 1 */
|
||||
^^^^^^^^^^^^^^ DQ @ 88:4-18 | ^^^^^^^^^^^^^^ DQ @ 222:2-16
|
||||
89 vertical-align: middle; | 223 vertical-align: middle; /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^ DR @ 89:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ DR @ 223:2-24
|
||||
| 224 }
|
||||
90 } |
|
||||
91 img, video { | 225 img,
|
||||
^ DN @ 91:2 | ^ DN @ 225:0
|
||||
91 img, video { | 226 video {
|
||||
^^^^^^^^^^^ DO @ 91:2-13 | ^^^^^^ DO @ 226:0-6
|
||||
92 max-width: 100%; | 227 max-width: 100%;
|
||||
^^^^^^^^^^^^^^^ DP @ 92:4-19 | ^^^^^^^^^^^^^^^ DP @ 227:2-17
|
||||
93 height: auto; | 228 height: auto;
|
||||
^^^^^^^^^^^^ DQ @ 93:4-16 | ^^^^^^^^^^^^ DQ @ 228:2-14
|
||||
| 229 }
|
||||
91 img, video { | 230 img,
|
||||
^ DS @ 91:2 | ^ DS @ 230:0
|
||||
91 img, video { | 231 video {
|
||||
^^^^^^^^^^^ DT @ 91:2-13 | ^^^^^^ DT @ 231:0-6
|
||||
92 max-width: 100%; | 232 max-width: 100%;
|
||||
^^^^^^^^^^^^^^^ DU @ 92:4-19 | ^^^^^^^^^^^^^^^ DU @ 232:2-17
|
||||
93 height: auto; | 233 height: auto;
|
||||
^^^^^^^^^^^^ DV @ 93:4-16 | ^^^^^^^^^^^^ DV @ 233:2-14
|
||||
| 234 }
|
||||
94 } |
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 238 button,
|
||||
^ DR @ 95:2 | ^ DR @ 238:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 239 input,
|
||||
^ DS @ 95:2 | ^ DS @ 239:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 240 select,
|
||||
^ DT @ 95:2 | ^ DT @ 240:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 241 optgroup,
|
||||
^ DU @ 95:2 | ^ DU @ 241:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 242 textarea,
|
||||
^ DV @ 95:2 | ^ DV @ 242:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 243 ::file-selector-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DW @ 95:2-68 | ^^^^^^^^^^^^^^^^^^^^^^^ DW @ 243:0-23
|
||||
96 font: inherit; | 244 font: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^ DX @ 96:4-17 | ^^^^^^^^^^^^^ DX @ 244:2-15
|
||||
97 font-feature-settings: inherit; | 245 font-feature-settings: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DY @ 97:4-34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DY @ 245:2-32
|
||||
98 font-variation-settings: inherit; | 246 font-variation-settings: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DZ @ 98:4-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DZ @ 246:2-34
|
||||
99 letter-spacing: inherit; | 247 letter-spacing: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ EA @ 99:4-27 | ^^^^^^^^^^^^^^^^^^^^^^^ EA @ 247:2-25
|
||||
100 color: inherit; | 248 color: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^ EB @ 100:4-18 | ^^^^^^^^^^^^^^ EB @ 248:2-16
|
||||
101 border-radius: 0; | 249 border-radius: 0; /* 2 */
|
||||
^^^^^^^^^^^^^^^^ EC @ 101:4-20 | ^^^^^^^^^^^^^^^^ EC @ 249:2-18
|
||||
102 background-color: transparent; | 250 background-color: transparent; /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ED @ 102:4-33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ED @ 250:2-31
|
||||
103 opacity: 1; | 251 opacity: 1; /* 4 */
|
||||
^^^^^^^^^^ EE @ 103:4-14 | ^^^^^^^^^^ EE @ 251:2-12
|
||||
| 252 }
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 243 button,
|
||||
^ DW @ 95:2 | ^ DW @ 243:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 244 input,
|
||||
^ DX @ 95:2 | ^ DX @ 244:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 245 select,
|
||||
^ DY @ 95:2 | ^ DY @ 245:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 246 optgroup,
|
||||
^ DZ @ 95:2 | ^ DZ @ 246:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 247 textarea,
|
||||
^ EA @ 95:2 | ^ EA @ 247:0
|
||||
95 button, input, select, optgroup, textarea, ::file-selector-button { | 248 ::file-selector-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EB @ 95:2-68 | ^^^^^^^^^^^^^^^^^^^^^^^ EB @ 248:0-23
|
||||
96 font: inherit; | 249 font: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^ EC @ 96:4-17 | ^^^^^^^^^^^^^ EC @ 249:2-15
|
||||
97 font-feature-settings: inherit; | 250 font-feature-settings: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ED @ 97:4-34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ED @ 250:2-32
|
||||
98 font-variation-settings: inherit; | 251 font-variation-settings: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EE @ 98:4-36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EE @ 251:2-34
|
||||
99 letter-spacing: inherit; | 252 letter-spacing: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ EF @ 99:4-27 | ^^^^^^^^^^^^^^^^^^^^^^^ EF @ 252:2-25
|
||||
100 color: inherit; | 253 color: inherit; /* 1 */
|
||||
^^^^^^^^^^^^^^ EG @ 100:4-18 | ^^^^^^^^^^^^^^ EG @ 253:2-16
|
||||
101 border-radius: 0; | 254 border-radius: 0; /* 2 */
|
||||
^^^^^^^^^^^^^^^^ EH @ 101:4-20 | ^^^^^^^^^^^^^^^^ EH @ 254:2-18
|
||||
102 background-color: transparent; | 255 background-color: transparent; /* 3 */
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EI @ 102:4-33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EI @ 255:2-31
|
||||
103 opacity: 1; | 256 opacity: 1; /* 4 */
|
||||
^^^^^^^^^^ EJ @ 103:4-14 | ^^^^^^^^^^ EJ @ 256:2-12
|
||||
| 257 }
|
||||
104 } |
|
||||
105 :where(select:is([multiple], [size])) optgroup { | 258 :where(select:is([multiple], [size])) optgroup {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EF @ 105:2-49 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EF @ 258:0-47
|
||||
106 font-weight: bolder; | 259 font-weight: bolder;
|
||||
^^^^^^^^^^^^^^^^^^^ EG @ 106:4-23 | ^^^^^^^^^^^^^^^^^^^ EG @ 259:2-21
|
||||
| 260 }
|
||||
105 :where(select:is([multiple], [size])) optgroup { | 263 :where(select:is([multiple], [size])) optgroup {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EK @ 105:2-49 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EK @ 263:0-47
|
||||
106 font-weight: bolder; | 264 font-weight: bolder;
|
||||
^^^^^^^^^^^^^^^^^^^ EL @ 106:4-23 | ^^^^^^^^^^^^^^^^^^^ EL @ 264:2-21
|
||||
| 265 }
|
||||
107 } |
|
||||
108 :where(select:is([multiple], [size])) optgroup option { | 266 :where(select:is([multiple], [size])) optgroup option {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EH @ 108:2-56 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EH @ 266:0-54
|
||||
109 padding-inline-start: 20px; | 267 padding-inline-start: 20px;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^ EI @ 109:4-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ EI @ 267:2-28
|
||||
| 268 }
|
||||
108 :where(select:is([multiple], [size])) optgroup option { | 271 :where(select:is([multiple], [size])) optgroup option {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM @ 108:2-56 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM @ 271:0-54
|
||||
109 padding-inline-start: 20px; | 272 padding-inline-start: 20px;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^ EN @ 109:4-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ EN @ 272:2-28
|
||||
| 273 }
|
||||
110 } |
|
||||
111 ::file-selector-button { | 274 ::file-selector-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ EJ @ 111:2-25 | ^^^^^^^^^^^^^^^^^^^^^^^ EJ @ 274:0-23
|
||||
112 margin-inline-end: 4px; | 275 margin-inline-end: 4px;
|
||||
^^^^^^^^^^^^^^^^^^^^^^ EK @ 112:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ EK @ 275:2-24
|
||||
| 276 }
|
||||
111 ::file-selector-button { | 279 ::file-selector-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^ EO @ 111:2-25 | ^^^^^^^^^^^^^^^^^^^^^^^ EO @ 279:0-23
|
||||
112 margin-inline-end: 4px; | 280 margin-inline-end: 4px;
|
||||
^^^^^^^^^^^^^^^^^^^^^^ EP @ 112:4-26 | ^^^^^^^^^^^^^^^^^^^^^^ EP @ 280:2-24
|
||||
| 281 }
|
||||
113 } |
|
||||
114 ::placeholder { | 282 ::placeholder {
|
||||
^^^^^^^^^^^^^^ EL @ 114:2-16 | ^^^^^^^^^^^^^^ EL @ 282:0-14
|
||||
115 opacity: 1; | 283 opacity: 1;
|
||||
^^^^^^^^^^ EM @ 115:4-14 | ^^^^^^^^^^ EM @ 283:2-12
|
||||
| 284 }
|
||||
114 ::placeholder { | 287 ::placeholder {
|
||||
^^^^^^^^^^^^^^ EQ @ 114:2-16 | ^^^^^^^^^^^^^^ EQ @ 287:0-14
|
||||
115 opacity: 1; | 288 opacity: 1;
|
||||
^^^^^^^^^^ ER @ 115:4-14 | ^^^^^^^^^^ ER @ 288:2-12
|
||||
| 289 }
|
||||
116 } |
|
||||
117 @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { | 291 @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or
|
||||
^ EN @ 117:2 | ^ EN @ 291:0
|
||||
117 @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { | 292 (contain-intrinsic-size: 1px) /* Safari 17+ */ {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... EO @ 117:2-92 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EO @ 292:0-49
|
||||
118 ::placeholder { | 293 ::placeholder {
|
||||
^^^^^^^^^^^^^^ EP @ 118:4-18 | ^^^^^^^^^^^^^^ EP @ 293:2-16
|
||||
119 color: currentcolor; | 294 color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||
^^^^^^^^^^^^^^^^^^^ EQ @ 119:6-25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 294:4-61
|
||||
| 295 }
|
||||
| 296 }
|
||||
117 @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { | 296 @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or
|
||||
^ ES @ 117:2 | ^ ES @ 296:0
|
||||
117 @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { | 297 (contain-intrinsic-size: 1px) /* Safari 17+ */ {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... ET @ 117:2-92 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 297:0-49
|
||||
118 ::placeholder { | 298 ::placeholder {
|
||||
^^^^^^^^^^^^^^ EU @ 118:4-18 | ^^^^^^^^^^^^^^ EU @ 298:2-16
|
||||
119 color: currentcolor; | 299 color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||
^^^^^^^^^^^^^^^^^^^ EV @ 119:6-25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 299:4-61
|
||||
| 300 }
|
||||
| 301 }
|
||||
120 @supports (color: color-mix(in lab, red, red)) { |
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 120:6-53 |
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 120:6-53 |
|
||||
121 color: color-mix(in oklab, currentcolor 50%, transparent); |
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EQ @ 121:8-65 |
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 121:8-65 |
|
||||
122 } |
|
||||
123 } |
|
||||
124 } |
|
||||
125 textarea { | 302 textarea {
|
||||
^^^^^^^^^ ER @ 125:2-11 | ^^^^^^^^^ ER @ 302:0-9
|
||||
126 resize: vertical; | 303 resize: vertical;
|
||||
^^^^^^^^^^^^^^^^ ES @ 126:4-20 | ^^^^^^^^^^^^^^^^ ES @ 303:2-18
|
||||
| 304 }
|
||||
125 textarea { | 307 textarea {
|
||||
^^^^^^^^^ EW @ 125:2-11 | ^^^^^^^^^ EW @ 307:0-9
|
||||
126 resize: vertical; | 308 resize: vertical;
|
||||
^^^^^^^^^^^^^^^^ EX @ 126:4-20 | ^^^^^^^^^^^^^^^^ EX @ 308:2-18
|
||||
| 309 }
|
||||
127 } |
|
||||
128 ::-webkit-search-decoration { | 310 ::-webkit-search-decoration {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 128:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ET @ 310:0-28
|
||||
129 -webkit-appearance: none; | 311 -webkit-appearance: none;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 129:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EU @ 311:2-26
|
||||
| 312 }
|
||||
128 ::-webkit-search-decoration { | 315 ::-webkit-search-decoration {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 128:2-30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 315:0-28
|
||||
129 -webkit-appearance: none; | 316 -webkit-appearance: none;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ EZ @ 129:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ EZ @ 316:2-26
|
||||
| 317 }
|
||||
130 } |
|
||||
131 ::-webkit-date-and-time-value { | 319 ::-webkit-date-and-time-value {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 131:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EV @ 319:0-30
|
||||
132 min-height: 1lh; | 320 min-height: 1lh; /* 1 */
|
||||
^^^^^^^^^^^^^^^ EW @ 132:4-19 | ^^^^^^^^^^^^^^^ EW @ 320:2-17
|
||||
133 text-align: inherit; | 321 text-align: inherit; /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^ EX @ 133:4-23 | ^^^^^^^^^^^^^^^^^^^ EX @ 321:2-21
|
||||
| 322 }
|
||||
131 ::-webkit-date-and-time-value { | 324 ::-webkit-date-and-time-value {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 131:2-32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 324:0-30
|
||||
132 min-height: 1lh; | 325 min-height: 1lh; /* 1 */
|
||||
^^^^^^^^^^^^^^^ FB @ 132:4-19 | ^^^^^^^^^^^^^^^ FB @ 325:2-17
|
||||
133 text-align: inherit; | 326 text-align: inherit; /* 2 */
|
||||
^^^^^^^^^^^^^^^^^^^ FC @ 133:4-23 | ^^^^^^^^^^^^^^^^^^^ FC @ 326:2-21
|
||||
| 327 }
|
||||
134 } |
|
||||
135 ::-webkit-datetime-edit { | 328 ::-webkit-datetime-edit {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 135:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ EY @ 328:0-24
|
||||
136 display: inline-flex; | 329 display: inline-flex;
|
||||
^^^^^^^^^^^^^^^^^^^^ EZ @ 136:4-24 | ^^^^^^^^^^^^^^^^^^^^ EZ @ 329:2-22
|
||||
| 330 }
|
||||
135 ::-webkit-datetime-edit { | 333 ::-webkit-datetime-edit {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ FD @ 135:2-26 | ^^^^^^^^^^^^^^^^^^^^^^^^ FD @ 333:0-24
|
||||
136 display: inline-flex; | 334 display: inline-flex;
|
||||
^^^^^^^^^^^^^^^^^^^^ FE @ 136:4-24 | ^^^^^^^^^^^^^^^^^^^^ FE @ 334:2-22
|
||||
| 335 }
|
||||
137 } |
|
||||
138 ::-webkit-datetime-edit-fields-wrapper { | 336 ::-webkit-datetime-edit-fields-wrapper {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 138:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA @ 336:0-39
|
||||
139 padding: 0; | 337 padding: 0;
|
||||
^^^^^^^^^^ FB @ 139:4-14 | ^^^^^^^^^^ FB @ 337:2-12
|
||||
| 338 }
|
||||
138 ::-webkit-datetime-edit-fields-wrapper { | 341 ::-webkit-datetime-edit-fields-wrapper {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FF @ 138:2-41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FF @ 341:0-39
|
||||
139 padding: 0; | 342 padding: 0;
|
||||
^^^^^^^^^^ FG @ 139:4-14 | ^^^^^^^^^^ FG @ 342:2-12
|
||||
| 343 }
|
||||
140 } |
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 340 ::-webkit-datetime-edit,
|
||||
^ FC @ 141:2 | ^ FC @ 340:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 341 ::-webkit-datetime-edit-year-field,
|
||||
^ FD @ 141:2 | ^ FD @ 341:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 342 ::-webkit-datetime-edit-month-field,
|
||||
^ FE @ 141:2 | ^ FE @ 342:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 343 ::-webkit-datetime-edit-day-field,
|
||||
^ FF @ 141:2 | ^ FF @ 343:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 344 ::-webkit-datetime-edit-hour-field,
|
||||
^ FG @ 141:2 | ^ FG @ 344:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit-minute-field,
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 345 ::-webkit-datetime-edit,
|
||||
^ FH @ 141:2 | ^ FH @ 345:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-second-field,
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 346 ::-webkit-datetime-edit-year-field,
|
||||
^ FI @ 141:2 | ^ FI @ 346:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-millisecond-field,
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 347 ::-webkit-datetime-edit-month-field,
|
||||
^ FJ @ 141:2 | ^ FJ @ 347:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-meridiem-field {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FK @ 141:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FK @ 348:0-39
|
||||
142 padding-block: 0; | 349 padding-block: 0;
|
||||
^^^^^^^^^^^^^^^^ FL @ 142:4-20 | ^^^^^^^^^^^^^^^^ FL @ 349:2-18
|
||||
| 350 }
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 348 ::-webkit-datetime-edit-day-field,
|
||||
^ FK @ 141:2 | ^ FK @ 348:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 349 ::-webkit-datetime-edit-hour-field,
|
||||
^ FL @ 141:2 | ^ FL @ 349:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 350 ::-webkit-datetime-edit-minute-field,
|
||||
^ FM @ 141:2 | ^ FM @ 350:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 351 ::-webkit-datetime-edit-second-field,
|
||||
^ FN @ 141:2 | ^ FN @ 351:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 352 ::-webkit-datetime-edit-millisecond-field,
|
||||
^ FO @ 141:2 | ^ FO @ 352:0
|
||||
141 ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month... | 353 ::-webkit-datetime-edit-meridiem-field {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FP @ 141:2-329 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FP @ 353:0-39
|
||||
142 padding-block: 0; | 354 padding-block: 0;
|
||||
^^^^^^^^^^^^^^^^ FQ @ 142:4-20 | ^^^^^^^^^^^^^^^^ FQ @ 354:2-18
|
||||
| 355 }
|
||||
143 } |
|
||||
144 ::-webkit-calendar-picker-indicator { | 356 ::-webkit-calendar-picker-indicator {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 144:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FM @ 356:0-36
|
||||
145 line-height: 1; | 357 line-height: 1;
|
||||
^^^^^^^^^^^^^^ FN @ 145:4-18 | ^^^^^^^^^^^^^^ FN @ 357:2-16
|
||||
| 358 }
|
||||
144 ::-webkit-calendar-picker-indicator { | 361 ::-webkit-calendar-picker-indicator {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FR @ 144:2-38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FR @ 361:0-36
|
||||
145 line-height: 1; | 362 line-height: 1;
|
||||
^^^^^^^^^^^^^^ FS @ 145:4-18 | ^^^^^^^^^^^^^^ FS @ 362:2-16
|
||||
| 363 }
|
||||
146 } |
|
||||
147 :-moz-ui-invalid { | 364 :-moz-ui-invalid {
|
||||
^^^^^^^^^^^^^^^^^ FO @ 147:2-19 | ^^^^^^^^^^^^^^^^^ FO @ 364:0-17
|
||||
148 box-shadow: none; | 365 box-shadow: none;
|
||||
^^^^^^^^^^^^^^^^ FP @ 148:4-20 | ^^^^^^^^^^^^^^^^ FP @ 365:2-18
|
||||
| 366 }
|
||||
147 :-moz-ui-invalid { | 369 :-moz-ui-invalid {
|
||||
^^^^^^^^^^^^^^^^^ FT @ 147:2-19 | ^^^^^^^^^^^^^^^^^ FT @ 369:0-17
|
||||
148 box-shadow: none; | 370 box-shadow: none;
|
||||
^^^^^^^^^^^^^^^^ FU @ 148:4-20 | ^^^^^^^^^^^^^^^^ FU @ 370:2-18
|
||||
| 371 }
|
||||
149 } |
|
||||
150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 372 button,
|
||||
^ FQ @ 150:2 | ^ FQ @ 372:0
|
||||
150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 373 input:where([type='button'], [type='reset'], [type='submit']),
|
||||
^ FR @ 150:2 | ^ FR @ 373:0
|
||||
150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 374 ::file-selector-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FS @ 150:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FS @ 374:0-23
|
||||
151 appearance: button; | 375 appearance: button;
|
||||
^^^^^^^^^^^^^^^^^^ FT @ 151:4-22 | ^^^^^^^^^^^^^^^^^^ FT @ 375:2-20
|
||||
| 376 }
|
||||
150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 377 button,
|
||||
^ FV @ 150:2 | ^ FV @ 377:0
|
||||
150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 378 input:where([type='button'], [type='reset'], [type='submit']),
|
||||
^ FW @ 150:2 | ^ FW @ 378:0
|
||||
150 button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-but... | 379 ::file-selector-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^... FX @ 150:2-96 | ^^^^^^^^^^^^^^^^^^^^^^^ FX @ 379:0-23
|
||||
151 appearance: button; | 380 appearance: button;
|
||||
^^^^^^^^^^^^^^^^^^ FY @ 151:4-22 | ^^^^^^^^^^^^^^^^^^ FY @ 380:2-20
|
||||
| 381 }
|
||||
152 } |
|
||||
153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 382 ::-webkit-inner-spin-button,
|
||||
^ FU @ 153:2 | ^ FU @ 382:0
|
||||
153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 383 ::-webkit-outer-spin-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 153:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FV @ 383:0-28
|
||||
154 height: auto; | 384 height: auto;
|
||||
^^^^^^^^^^^^ FW @ 154:4-16 | ^^^^^^^^^^^^ FW @ 384:2-14
|
||||
| 385 }
|
||||
153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 387 ::-webkit-inner-spin-button,
|
||||
^ FZ @ 153:2 | ^ FZ @ 387:0
|
||||
153 ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { | 388 ::-webkit-outer-spin-button {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GA @ 153:2-59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GA @ 388:0-28
|
||||
154 height: auto; | 389 height: auto;
|
||||
^^^^^^^^^^^^ GB @ 154:4-16 | ^^^^^^^^^^^^ GB @ 389:2-14
|
||||
| 390 }
|
||||
155 } |
|
||||
156 [hidden]:where(:not([hidden='until-found'])) { | 391 [hidden]:where(:not([hidden='until-found'])) {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 156:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FX @ 391:0-45
|
||||
157 display: none !important; | 392 display: none !important;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 157:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ FY @ 392:2-26
|
||||
| 393 }
|
||||
156 [hidden]:where(:not([hidden='until-found'])) { | 396 [hidden]:where(:not([hidden='until-found'])) {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GC @ 156:2-47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GC @ 396:0-45
|
||||
157 display: none !important; | 397 display: none !important;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^ GD @ 157:4-28 | ^^^^^^^^^^^^^^^^^^^^^^^^ GD @ 397:2-26
|
||||
| 398 }
|
||||
158 } |
|
||||
159 } |
|
||||
| --- index.css ---
|
||||
160 @layer utilities; | 5 @import './utilities.css' layer(utilities);
|
||||
^^^^^^^^^^^^^^^^ FZ @ 160:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FZ @ 5:0-42
|
||||
^^^^^^^^^^^^^^^^ GE @ 160:0-16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GE @ 5:0-42
|
||||
| --- input.css ---
|
||||
161 .foo { | 3 .foo {
|
||||
^^^^^ GA @ 161:0-5 | ^^^^^ GA @ 3:0-5
|
||||
^^^^^ GF @ 161:0-5 | ^^^^^ GF @ 3:0-5
|
||||
162 text-decoration-line: underline; | 4 @apply underline;
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GB @ 162:2-33 | ^^^^^^^^^ GB @ 4:9-18
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GG @ 162:2-33 | ^^^^^^^^^ GG @ 4:9-18
|
||||
| 5 }
|
||||
163 } |
|
||||
164 |
|
||||
|
|
@ -865,25 +875,19 @@ test('Source locations for `addBase` point to the `@plugin` that generated them'
|
|||
|
||||
expect(annotations).toMatchInlineSnapshot(`
|
||||
"
|
||||
output.css | input.css
|
||||
|
|
||||
1 @layer base { | 1 @plugin "./plugin.js";
|
||||
^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21
|
||||
2 body { |
|
||||
^^^^^ A @ 2:2-7 |
|
||||
3 color: red; |
|
||||
^^^^^^^^^^ A @ 3:4-14 |
|
||||
4 } |
|
||||
5 } |
|
||||
6 @layer base { | 2 @config "./config.js";
|
||||
^^^^^^^^^^^^ B @ 6:0-12 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21
|
||||
7 body { |
|
||||
^^^^^ B @ 7:2-7 |
|
||||
8 color: green; |
|
||||
^^^^^^^^^^^^ B @ 8:4-16 |
|
||||
9 } |
|
||||
10 } |
|
||||
11 |
|
||||
output.css | input.css
|
||||
|
|
||||
1 @layer base { | 1 @plugin "./plugin.js";
|
||||
^^^^^^^^^^^^ A @ 1:0-12 | ^^^^^^^^^^^^^^^^^^^^^ A @ 1:0-21
|
||||
2 body { |
|
||||
^^^^^ A @ 2:2-7 |
|
||||
3 color: red; |
|
||||
^^^^^^^^^^ A @ 3:4-14 |
|
||||
4 color: green; | 2 @config "./config.js";
|
||||
^^^^^^^^^^^^ B @ 4:4-16 | ^^^^^^^^^^^^^^^^^^^^^ B @ 2:0-21
|
||||
5 } |
|
||||
6 } |
|
||||
7 |
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11581,20 +11581,20 @@ test('scrollbar-width', async () => {
|
|||
test('scrollbar-gutter', async () => {
|
||||
expect(await run(['scrollbar-gutter-auto', 'scrollbar-gutter-stable', 'scrollbar-gutter-both']))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
.scrollbar-gutter-auto {
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
"
|
||||
.scrollbar-gutter-auto {
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
|
||||
.scrollbar-gutter-both {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
.scrollbar-gutter-both {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
.scrollbar-gutter-stable {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
"
|
||||
`)
|
||||
.scrollbar-gutter-stable {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
"
|
||||
`)
|
||||
expect(
|
||||
await run([
|
||||
'scrollbar-gutter',
|
||||
|
|
@ -16145,7 +16145,7 @@ test('mask-t-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
|
||||
--tw-mask-top-from-position: 0;
|
||||
--tw-mask-top-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -16405,7 +16405,7 @@ test('mask-t-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
|
||||
--tw-mask-top-to-position: 0;
|
||||
--tw-mask-top-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -16666,7 +16666,7 @@ test('mask-r-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
|
||||
--tw-mask-right-from-position: 0;
|
||||
--tw-mask-right-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -16927,7 +16927,7 @@ test('mask-r-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
|
||||
--tw-mask-right-to-position: 0;
|
||||
--tw-mask-right-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -17188,7 +17188,7 @@ test('mask-b-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
|
||||
--tw-mask-bottom-from-position: 0;
|
||||
--tw-mask-bottom-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -17449,7 +17449,7 @@ test('mask-b-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
|
||||
--tw-mask-bottom-to-position: 0;
|
||||
--tw-mask-bottom-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -17710,7 +17710,7 @@ test('mask-l-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
|
||||
--tw-mask-left-from-position: 0;
|
||||
--tw-mask-left-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -17971,7 +17971,7 @@ test('mask-l-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
|
||||
--tw-mask-left-to-position: 0;
|
||||
--tw-mask-left-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -18240,9 +18240,9 @@ test('mask-x-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
|
||||
--tw-mask-right-from-position: 0;
|
||||
--tw-mask-right-from-position: 0px;
|
||||
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
|
||||
--tw-mask-left-from-position: 0;
|
||||
--tw-mask-left-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -18547,9 +18547,9 @@ test('mask-x-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-right: linear-gradient(to right, var(--tw-mask-right-from-color) var(--tw-mask-right-from-position), var(--tw-mask-right-to-color) var(--tw-mask-right-to-position));
|
||||
--tw-mask-right-to-position: 0;
|
||||
--tw-mask-right-to-position: 0px;
|
||||
--tw-mask-left: linear-gradient(to left, var(--tw-mask-left-from-color) var(--tw-mask-left-from-position), var(--tw-mask-left-to-color) var(--tw-mask-left-to-position));
|
||||
--tw-mask-left-to-position: 0;
|
||||
--tw-mask-left-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -18854,9 +18854,9 @@ test('mask-y-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
|
||||
--tw-mask-top-from-position: 0;
|
||||
--tw-mask-top-from-position: 0px;
|
||||
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
|
||||
--tw-mask-bottom-from-position: 0;
|
||||
--tw-mask-bottom-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -19161,9 +19161,9 @@ test('mask-y-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear: var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top);
|
||||
--tw-mask-top: linear-gradient(to top, var(--tw-mask-top-from-color) var(--tw-mask-top-from-position), var(--tw-mask-top-to-color) var(--tw-mask-top-to-position));
|
||||
--tw-mask-top-to-position: 0;
|
||||
--tw-mask-top-to-position: 0px;
|
||||
--tw-mask-bottom: linear-gradient(to bottom, var(--tw-mask-bottom-from-color) var(--tw-mask-bottom-from-position), var(--tw-mask-bottom-to-color) var(--tw-mask-bottom-to-position));
|
||||
--tw-mask-bottom-to-position: 0;
|
||||
--tw-mask-bottom-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -19618,7 +19618,7 @@ test('mask-linear-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear-stops: var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position);
|
||||
--tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops));
|
||||
--tw-mask-linear-from-position: 0;
|
||||
--tw-mask-linear-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -19858,7 +19858,7 @@ test('mask-linear-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-linear-stops: var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position);
|
||||
--tw-mask-linear: linear-gradient(var(--tw-mask-linear-stops));
|
||||
--tw-mask-linear-to-position: 0;
|
||||
--tw-mask-linear-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -20347,7 +20347,7 @@ test('mask-radial-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-radial-stops: var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position);
|
||||
--tw-mask-radial: radial-gradient(var(--tw-mask-radial-stops));
|
||||
--tw-mask-radial-from-position: 0;
|
||||
--tw-mask-radial-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -20601,7 +20601,7 @@ test('mask-radial-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-radial-stops: var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position);
|
||||
--tw-mask-radial: radial-gradient(var(--tw-mask-radial-stops));
|
||||
--tw-mask-radial-to-position: 0;
|
||||
--tw-mask-radial-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -21014,7 +21014,7 @@ test('mask-conic-from', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-conic-stops: from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position);
|
||||
--tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops));
|
||||
--tw-mask-conic-from-position: 0;
|
||||
--tw-mask-conic-from-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -21254,7 +21254,7 @@ test('mask-conic-to', async () => {
|
|||
mask-image: var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic);
|
||||
--tw-mask-conic-stops: from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position);
|
||||
--tw-mask-conic: conic-gradient(var(--tw-mask-conic-stops));
|
||||
--tw-mask-conic-to-position: 0;
|
||||
--tw-mask-conic-to-position: 0px;
|
||||
-webkit-mask-composite: source-in;
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
|
|
@ -24053,6 +24053,7 @@ test('filter', async () => {
|
|||
'invert-[var(--value)]',
|
||||
'drop-shadow',
|
||||
'drop-shadow/25',
|
||||
'drop-shadow/12.5',
|
||||
'drop-shadow-xl',
|
||||
'drop-shadow-multi',
|
||||
'drop-shadow-[0_0_red]',
|
||||
|
|
@ -24150,6 +24151,13 @@ test('filter', async () => {
|
|||
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
|
||||
}
|
||||
|
||||
.drop-shadow\\/12\\.5 {
|
||||
--tw-drop-shadow-alpha: 12.5%;
|
||||
--tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .125)));
|
||||
--tw-drop-shadow: drop-shadow(var(--drop-shadow));
|
||||
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
|
||||
}
|
||||
|
||||
.drop-shadow\\/25 {
|
||||
--tw-drop-shadow-alpha: 25%;
|
||||
--tw-drop-shadow-size: drop-shadow(0 1px 1px var(--tw-drop-shadow-color, oklab(0% 0 0 / .25)));
|
||||
|
|
@ -26901,6 +26909,7 @@ test('text-shadow', async () => {
|
|||
'text-shadow-[shadow:var(--value)]',
|
||||
|
||||
'text-shadow-sm/25',
|
||||
'text-shadow-sm/12.5',
|
||||
'text-shadow-[12px_12px_#0088cc]/25',
|
||||
'text-shadow-[12px_12px_var(--value)]/25',
|
||||
'text-shadow-[10px_10px]/25',
|
||||
|
|
@ -26979,6 +26988,11 @@ test('text-shadow', async () => {
|
|||
text-shadow: 12px 12px var(--tw-text-shadow-color, oklab(59.9824% -.067 -.124 / .25));
|
||||
}
|
||||
|
||||
.text-shadow-sm\\/12\\.5 {
|
||||
--tw-text-shadow-alpha: 12.5%;
|
||||
text-shadow: 0px 1px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .125)), 0px 2px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .125));
|
||||
}
|
||||
|
||||
.text-shadow-sm\\/25 {
|
||||
--tw-text-shadow-alpha: 25%;
|
||||
text-shadow: 0px 1px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .25)), 0px 2px 2px var(--tw-text-shadow-color, oklab(0% 0 0 / .25));
|
||||
|
|
@ -27266,6 +27280,7 @@ test('shadow', async () => {
|
|||
'shadow-[shadow:var(--value)]',
|
||||
|
||||
'shadow-sm/25',
|
||||
'shadow-sm/12.5',
|
||||
'shadow-[12px_12px_#0088cc]/25',
|
||||
'shadow-[12px_12px_var(--value)]/25',
|
||||
'shadow-[10px_10px]/25',
|
||||
|
|
@ -27368,6 +27383,12 @@ test('shadow', async () => {
|
|||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-sm\\/12\\.5 {
|
||||
--tw-shadow-alpha: 12.5%;
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, oklab(0% 0 0 / .125)), 0 1px 2px -1px var(--tw-shadow-color, oklab(0% 0 0 / .125));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-sm\\/25 {
|
||||
--tw-shadow-alpha: 25%;
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, oklab(0% 0 0 / .25)), 0 1px 2px -1px var(--tw-shadow-color, oklab(0% 0 0 / .25));
|
||||
|
|
@ -27742,6 +27763,7 @@ test('inset-shadow', async () => {
|
|||
'inset-shadow-[12px_12px_#0088cc,12px_12px_var(--value,#0088cc)]',
|
||||
|
||||
'inset-shadow-sm/25',
|
||||
'inset-shadow-sm/12.5',
|
||||
'inset-shadow-[12px_12px_#0088cc]/25',
|
||||
'inset-shadow-[12px_12px_var(--value)]/25',
|
||||
'inset-shadow-[10px_10px]/25',
|
||||
|
|
@ -27857,6 +27879,12 @@ test('inset-shadow', async () => {
|
|||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.inset-shadow-sm\\/12\\.5 {
|
||||
--tw-inset-shadow-alpha: 12.5%;
|
||||
--tw-inset-shadow: inset 0 1px 1px var(--tw-inset-shadow-color, oklab(0% 0 0 / .125));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.inset-shadow-sm\\/25 {
|
||||
--tw-inset-shadow-alpha: 25%;
|
||||
--tw-inset-shadow: inset 0 1px 1px var(--tw-inset-shadow-color, oklab(0% 0 0 / .25));
|
||||
|
|
@ -30176,20 +30204,20 @@ describe('custom utilities', () => {
|
|||
`
|
||||
|
||||
expect(await run(['border--0', 'border--1', 'border--2'], input)).toMatchInlineSnapshot(`
|
||||
"
|
||||
.border--0 {
|
||||
border-color: var(--color-border-0, #e5e7eb);
|
||||
}
|
||||
"
|
||||
.border--0 {
|
||||
border-color: var(--color-border-0, #e5e7eb);
|
||||
}
|
||||
|
||||
.border--1 {
|
||||
border-color: var(--color-border-1, #d1d5db);
|
||||
}
|
||||
.border--1 {
|
||||
border-color: var(--color-border-1, #d1d5db);
|
||||
}
|
||||
|
||||
.border--2 {
|
||||
border-color: var(--color-border-2, #9ca3af);
|
||||
}
|
||||
"
|
||||
`)
|
||||
.border--2 {
|
||||
border-color: var(--color-border-2, #9ca3af);
|
||||
}
|
||||
"
|
||||
`)
|
||||
expect(await run(['border--3'], input)).toEqual('')
|
||||
})
|
||||
|
||||
|
|
@ -30285,20 +30313,20 @@ describe('custom utilities', () => {
|
|||
`
|
||||
|
||||
expect(await run(['example-1', 'example-76', 'example-971'], input)).toMatchInlineSnapshot(`
|
||||
"
|
||||
.example-1 {
|
||||
--resolved-value: 1;
|
||||
}
|
||||
"
|
||||
.example-1 {
|
||||
--resolved-value: 1;
|
||||
}
|
||||
|
||||
.example-76 {
|
||||
--resolved-value: 76;
|
||||
}
|
||||
.example-76 {
|
||||
--resolved-value: 76;
|
||||
}
|
||||
|
||||
.example-971 {
|
||||
--resolved-value: 971;
|
||||
}
|
||||
"
|
||||
`)
|
||||
.example-971 {
|
||||
--resolved-value: 971;
|
||||
}
|
||||
"
|
||||
`)
|
||||
expect(await run(['example-foo'], input)).toEqual('')
|
||||
})
|
||||
|
||||
|
|
@ -30622,20 +30650,20 @@ describe('custom utilities', () => {
|
|||
`
|
||||
|
||||
expect(await run(['example-a', 'example-76', 'example-[123]'], input)).toMatchInlineSnapshot(`
|
||||
"
|
||||
.example-76 {
|
||||
--resolved-value: 76;
|
||||
}
|
||||
"
|
||||
.example-76 {
|
||||
--resolved-value: 76;
|
||||
}
|
||||
|
||||
.example-\\[123\\] {
|
||||
--resolved-value: 123;
|
||||
}
|
||||
.example-\\[123\\] {
|
||||
--resolved-value: 123;
|
||||
}
|
||||
|
||||
.example-a {
|
||||
--resolved-value: var(--example-a, 8);
|
||||
}
|
||||
"
|
||||
`)
|
||||
.example-a {
|
||||
--resolved-value: var(--example-a, 8);
|
||||
}
|
||||
"
|
||||
`)
|
||||
expect(await run(['example-[#0088cc]', 'example-[1px]'], input)).toEqual('')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -519,6 +519,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: desc.themeKeys,
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
])
|
||||
|
|
@ -2490,6 +2491,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--border-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
hasDefaultValue: true,
|
||||
},
|
||||
|
|
@ -2960,6 +2962,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--background-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -3039,6 +3042,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--background-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -3306,6 +3310,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--background-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -3329,6 +3334,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--background-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
])
|
||||
|
|
@ -3843,6 +3849,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--fill', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
])
|
||||
|
|
@ -3894,6 +3901,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--stroke', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -4151,6 +4159,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--text-decoration-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -4609,7 +4618,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -4739,6 +4748,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--drop-shadow-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -5196,6 +5206,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--outline-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
hasDefaultValue: true,
|
||||
},
|
||||
|
|
@ -5386,6 +5397,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--text-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -5415,7 +5427,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -5508,6 +5520,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--text-shadow-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -5560,7 +5573,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -5661,6 +5674,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--box-shadow-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -5685,7 +5699,7 @@ export function createUtilities(theme: Theme) {
|
|||
if (candidate.modifier.kind === 'arbitrary') {
|
||||
alpha = candidate.modifier.value
|
||||
} else {
|
||||
if (isPositiveInteger(candidate.modifier.value)) {
|
||||
if (isValidOpacityValue(candidate.modifier.value)) {
|
||||
alpha = `${candidate.modifier.value}%`
|
||||
}
|
||||
}
|
||||
|
|
@ -5788,6 +5802,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--box-shadow-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -5868,6 +5883,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--ring-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -5941,6 +5957,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--ring-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
@ -6008,6 +6025,7 @@ export function createUtilities(theme: Theme) {
|
|||
{
|
||||
values: ['current', 'inherit', 'transparent'],
|
||||
valueThemeKeys: ['--ring-offset-color', '--color'],
|
||||
modifierThemeKeys: ['--opacity'],
|
||||
modifiers: Array.from({ length: 21 }, (_, index) => `${index * 5}`),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as ValueParser from '../value-parser'
|
||||
import { walk, WalkAction } from '../walk'
|
||||
import { DefaultMap } from './default-map'
|
||||
|
||||
export function extractUsedVariables(raw: string): string[] {
|
||||
let extractUsedVariablesCache = new DefaultMap((raw) => {
|
||||
let variables: string[] = []
|
||||
walk(ValueParser.parse(raw), (node) => {
|
||||
if (node.kind !== 'function' || node.value !== 'var') return
|
||||
|
|
@ -15,4 +16,8 @@ export function extractUsedVariables(raw: string): string[] {
|
|||
return WalkAction.Skip
|
||||
})
|
||||
return variables
|
||||
})
|
||||
|
||||
export function extractUsedVariables(raw: string): string[] {
|
||||
return extractUsedVariablesCache.get(raw)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2098,6 +2098,15 @@ test('data', async () => {
|
|||
'data-[potato=salad]/foo:flex',
|
||||
]),
|
||||
).toEqual('')
|
||||
|
||||
// Non-ASCII code points are valid in attribute names
|
||||
expect(await run(['data-[état]:flex', 'aria-[état]:flex'])).toMatchInlineSnapshot(`
|
||||
"
|
||||
.aria-\\[état\\]\\:flex[aria-état], .data-\\[état\\]\\:flex[data-état] {
|
||||
display: flex;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('portrait', async () => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
type Rule,
|
||||
type StyleRule,
|
||||
} from './ast'
|
||||
import * as AttributeSelectorParser from './attribute-selector-parser'
|
||||
import { type Variant } from './candidate'
|
||||
import { applyVariant } from './compile'
|
||||
import type { DesignSystem } from './design-system'
|
||||
|
|
@ -845,11 +846,15 @@ export function createVariants(theme: Theme): Variants {
|
|||
if (!variant.value || variant.modifier) return null
|
||||
|
||||
if (variant.value.kind === 'arbitrary') {
|
||||
ruleNode.nodes = [
|
||||
styleRule(`&[aria-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes),
|
||||
]
|
||||
let selector = `[aria-${quoteAttributeValue(variant.value.value)}]`
|
||||
let parsed = AttributeSelectorParser.parse(selector)
|
||||
if (parsed === null) return null
|
||||
ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)]
|
||||
} else {
|
||||
ruleNode.nodes = [styleRule(`&[aria-${variant.value.value}="true"]`, ruleNode.nodes)]
|
||||
let selector = `[aria-${variant.value.value}="true"]`
|
||||
let parsed = AttributeSelectorParser.parse(selector)
|
||||
if (parsed === null) return null
|
||||
ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)]
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -868,9 +873,11 @@ export function createVariants(theme: Theme): Variants {
|
|||
variants.functional('data', (ruleNode, variant) => {
|
||||
if (!variant.value || variant.modifier) return null
|
||||
|
||||
ruleNode.nodes = [
|
||||
styleRule(`&[data-${quoteAttributeValue(variant.value.value)}]`, ruleNode.nodes),
|
||||
]
|
||||
let selector = `[data-${quoteAttributeValue(variant.value.value)}]`
|
||||
let parsed = AttributeSelectorParser.parse(selector)
|
||||
if (parsed === null) return null
|
||||
|
||||
ruleNode.nodes = [styleRule(`&${selector}`, ruleNode.nodes)]
|
||||
})
|
||||
|
||||
variants.functional('nth', (ruleNode, variant) => {
|
||||
|
|
@ -1295,7 +1302,11 @@ export function substituteAtVariant(ast: AstNode[], designSystem: DesignSystem):
|
|||
}
|
||||
}
|
||||
|
||||
nodes.push(node)
|
||||
if (node.selector === '&') {
|
||||
nodes.push(...node.nodes)
|
||||
} else {
|
||||
nodes.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the variant at-rule node, to be the `&` rule node
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@theme default {
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,
|
||||
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
--color-gray-900: oklch(21% 0.034 264.665);
|
||||
--color-gray-950: oklch(13% 0.028 261.692);
|
||||
|
||||
--color-zinc-50: oklch(98.5% 0 0);
|
||||
--color-zinc-50: oklch(98.5% 0 none);
|
||||
--color-zinc-100: oklch(96.7% 0.001 286.375);
|
||||
--color-zinc-200: oklch(92% 0.004 286.32);
|
||||
--color-zinc-300: oklch(87.1% 0.006 286.286);
|
||||
|
|
@ -247,17 +247,17 @@
|
|||
--color-zinc-900: oklch(21% 0.006 285.885);
|
||||
--color-zinc-950: oklch(14.1% 0.005 285.823);
|
||||
|
||||
--color-neutral-50: oklch(98.5% 0 0);
|
||||
--color-neutral-100: oklch(97% 0 0);
|
||||
--color-neutral-200: oklch(92.2% 0 0);
|
||||
--color-neutral-300: oklch(87% 0 0);
|
||||
--color-neutral-400: oklch(70.8% 0 0);
|
||||
--color-neutral-500: oklch(55.6% 0 0);
|
||||
--color-neutral-600: oklch(43.9% 0 0);
|
||||
--color-neutral-700: oklch(37.1% 0 0);
|
||||
--color-neutral-800: oklch(26.9% 0 0);
|
||||
--color-neutral-900: oklch(20.5% 0 0);
|
||||
--color-neutral-950: oklch(14.5% 0 0);
|
||||
--color-neutral-50: oklch(98.5% 0 none);
|
||||
--color-neutral-100: oklch(97% 0 none);
|
||||
--color-neutral-200: oklch(92.2% 0 none);
|
||||
--color-neutral-300: oklch(87% 0 none);
|
||||
--color-neutral-400: oklch(70.8% 0 none);
|
||||
--color-neutral-500: oklch(55.6% 0 none);
|
||||
--color-neutral-600: oklch(43.9% 0 none);
|
||||
--color-neutral-700: oklch(37.1% 0 none);
|
||||
--color-neutral-800: oklch(26.9% 0 none);
|
||||
--color-neutral-900: oklch(20.5% 0 none);
|
||||
--color-neutral-950: oklch(14.5% 0 none);
|
||||
|
||||
--color-stone-50: oklch(98.5% 0.001 106.423);
|
||||
--color-stone-100: oklch(97% 0.001 106.424);
|
||||
|
|
@ -271,7 +271,7 @@
|
|||
--color-stone-900: oklch(21.6% 0.006 56.043);
|
||||
--color-stone-950: oklch(14.7% 0.004 49.25);
|
||||
|
||||
--color-mauve-50: oklch(98.5% 0 0);
|
||||
--color-mauve-50: oklch(98.5% 0 none);
|
||||
--color-mauve-100: oklch(96% 0.003 325.6);
|
||||
--color-mauve-200: oklch(92.2% 0.005 325.62);
|
||||
--color-mauve-300: oklch(86.5% 0.012 325.68);
|
||||
|
|
|
|||
|
|
@ -20,11 +20,5 @@
|
|||
"@types/react": "19.2.15",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "19.2.15",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,5 @@
|
|||
"@types/react-dom": "19.2.3",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "19.2.15",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
743
pnpm-lock.yaml
generated
743
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,10 +5,14 @@ packages:
|
|||
- 'playgrounds/*'
|
||||
- 'integrations'
|
||||
|
||||
patchedDependencies:
|
||||
'@parcel/watcher@2.5.1': patches/@parcel__watcher@2.5.1.patch
|
||||
lightningcss@1.32.0: patches/lightningcss@1.32.0.patch
|
||||
|
||||
catalog:
|
||||
'@types/node': 22.19.19
|
||||
'@types/node': 22.19.21
|
||||
dedent: 1.7.2
|
||||
enhanced-resolve: 5.21.6
|
||||
enhanced-resolve: ^5.24.1
|
||||
lightningcss-darwin-arm64: 1.32.0
|
||||
lightningcss-darwin-x64: 1.32.0
|
||||
lightningcss-linux-arm64-gnu: 1.32.0
|
||||
|
|
@ -17,7 +21,25 @@ catalog:
|
|||
lightningcss-linux-x64-musl: 1.32.0
|
||||
lightningcss-win32-x64-msvc: 1.32.0
|
||||
lightningcss: 1.32.0
|
||||
postcss: 8.5.15
|
||||
prettier: 3.8.3
|
||||
vite: 8.0.14
|
||||
webpack: 5.107.0
|
||||
postcss: ^8.5.16
|
||||
prettier: 3.9.4
|
||||
vite: ^8.1.2
|
||||
webpack: ^5.108.3
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- bun
|
||||
- esbuild
|
||||
- sharp
|
||||
- tree-sitter
|
||||
- tree-sitter-javascript
|
||||
- tree-sitter-typescript
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
bun: true
|
||||
esbuild: true
|
||||
sharp: true
|
||||
tree-sitter: true
|
||||
tree-sitter-javascript: true
|
||||
tree-sitter-typescript: true
|
||||
|
|
|
|||
|
|
@ -21,18 +21,25 @@ let workspaces = new Map()
|
|||
for (let path of paths) {
|
||||
let pkg = await fs.readFile(path, 'utf8').then(JSON.parse)
|
||||
if (pkg.private) continue
|
||||
workspaces.set(pkg.name, { version: pkg.version ?? '', dir: dirname(path) })
|
||||
workspaces.set(pkg.name, {
|
||||
version: pkg.version ?? '',
|
||||
dir: dirname(path),
|
||||
hasBundledDependencies: pkg.bundledDependencies?.length > 0 ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
// Clean dist folder
|
||||
await fs.rm(path.join(root, 'dist'), { recursive: true, force: true })
|
||||
|
||||
Promise.all(
|
||||
[...workspaces.entries()].map(async ([name, { dir }]) => {
|
||||
[...workspaces.entries()].map(async ([name, { dir, hasBundledDependencies }]) => {
|
||||
function pack() {
|
||||
return new Promise((resolve) => {
|
||||
// `bundledDependencies` can only be packed with the hoisted node linker
|
||||
// since pnpm v10. We opt into it for those packages only, instead of
|
||||
// changing the node linker for the entire workspace.
|
||||
exec(
|
||||
`pnpm pack --pack-gzip-level=0 --pack-destination="${path.join(root, 'dist').replace(/\\/g, '\\\\')}"`,
|
||||
`pnpm pack --pack-gzip-level=0 --pack-destination="${path.join(root, 'dist').replace(/\\/g, '\\\\')}"${hasBundledDependencies ? ' --config.node-linker=hoisted' : ''}`,
|
||||
{ cwd: dir },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue