Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Experimental_: Add `any-pointer-none`, `any-pointer-coarse`, and `any-pointer-fine` variants ([#16941](https://github.com/tailwindlabs/tailwindcss/pull/16941))
- _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370))
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))

### Fixed

Expand Down
1 change: 1 addition & 0 deletions packages/tailwindcss/src/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export const enableDetailsContent = process.env.FEATURES_ENV !== 'stable'
export const enableInvertedColors = process.env.FEATURES_ENV !== 'stable'
export const enablePointerVariants = process.env.FEATURES_ENV !== 'stable'
export const enableScripting = process.env.FEATURES_ENV !== 'stable'
export const enableSourceInline = process.env.FEATURES_ENV !== 'stable'
export const enableUserValid = process.env.FEATURES_ENV !== 'stable'
export const enableWrapAnywhere = process.env.FEATURES_ENV !== 'stable'
148 changes: 148 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3194,6 +3194,154 @@ describe('@source', () => {
{ pattern: './php/secr3t/smarty.php', base: '/root' },
])
})

describe('@source inline(…)', () => {
test('always includes the candidate', async () => {
let { build } = await compile(
css`
@source inline("underline");
@tailwind utilities;
`,
{ base: '/root' },
)

expect(build([])).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline;
}
"
`)
})

test('applies brace expansion', async () => {
let { build } = await compile(
css`
@theme {
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
--color-red-300: oklch(0.808 0.114 19.571);
--color-red-400: oklch(0.704 0.191 22.216);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-700: oklch(0.505 0.213 27.518);
--color-red-800: oklch(0.444 0.177 26.899);
--color-red-900: oklch(0.396 0.141 25.723);
--color-red-950: oklch(0.258 0.092 26.042);
}
@source inline("bg-red-{50,{100..900..100},950}");
@tailwind utilities;
`,
{ base: '/root' },
)

expect(build([])).toMatchInlineSnapshot(`
":root, :host {
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
--color-red-300: oklch(0.808 0.114 19.571);
--color-red-400: oklch(0.704 0.191 22.216);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-700: oklch(0.505 0.213 27.518);
--color-red-800: oklch(0.444 0.177 26.899);
--color-red-900: oklch(0.396 0.141 25.723);
--color-red-950: oklch(0.258 0.092 26.042);
}
.bg-red-50 {
background-color: var(--color-red-50);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-200 {
background-color: var(--color-red-200);
}
.bg-red-300 {
background-color: var(--color-red-300);
}
.bg-red-400 {
background-color: var(--color-red-400);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
.bg-red-600 {
background-color: var(--color-red-600);
}
.bg-red-700 {
background-color: var(--color-red-700);
}
.bg-red-800 {
background-color: var(--color-red-800);
}
.bg-red-900 {
background-color: var(--color-red-900);
}
.bg-red-950 {
background-color: var(--color-red-950);
}
"
`)
})

test('ignores invalid inline candidates', async () => {
let { build } = await compile(
css`
@source inline("my-cucumber");
@tailwind utilities;
`,
{ base: '/root' },
)

expect(build([])).toMatchInlineSnapshot(`""`)
})

test('can be negated', async () => {
let { build } = await compile(
css`
@theme {
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
}
@source not inline("container");
@tailwind utilities;
`,
{ base: '/root' },
)

expect(build(['container'])).toMatchInlineSnapshot(`""`)
})

test('applies brace expansion to negated sources', async () => {
let { build } = await compile(
css`
@theme {
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
--color-red-300: oklch(0.808 0.114 19.571);
--color-red-400: oklch(0.704 0.191 22.216);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-700: oklch(0.505 0.213 27.518);
--color-red-800: oklch(0.444 0.177 26.899);
--color-red-900: oklch(0.396 0.141 25.723);
--color-red-950: oklch(0.258 0.092 26.042);
}
@source not inline("bg-red-{50,{100..900..100},950}");
@tailwind utilities;
`,
{ base: '/root' },
)

expect(build(['bg-red-500', 'bg-red-700'])).toMatchInlineSnapshot(`""`)
})
})
})

describe('@custom-variant', () => {
Expand Down
52 changes: 49 additions & 3 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import { applyVariant, compileCandidates } from './compile'
import { substituteFunctions } from './css-functions'
import * as CSS from './css-parser'
import { buildDesignSystem, type DesignSystem } from './design-system'
import { enableSourceInline } from './feature-flags'
import { Theme, ThemeOptions } from './theme'
import { createCssUtility } from './utilities'
import { expand } from './utils/brace-expansion'
import { escape, unescape } from './utils/escape'
import { segment } from './utils/segment'
import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants'
Expand Down Expand Up @@ -127,6 +129,8 @@ async function parseCss(
let utilitiesNode = null as AtRule | null
let variantNodes: AtRule[] = []
let globs: { base: string; pattern: string }[] = []
let inlineCandidates: string[] = []
let ignoredCandidates: string[] = []
let root = null as Root

// Handle at-rules
Expand Down Expand Up @@ -208,15 +212,40 @@ async function parseCss(
throw new Error('`@source` cannot be nested.')
}

let inline = false
let not = false
let path = node.params

if (enableSourceInline) {
if (path[0] === 'n' && path.startsWith('not ')) {
not = true
path = path.slice(4)
}

if (path[0] === 'i' && path.startsWith('inline(')) {
inline = true
path = path.slice(7, -1)
}
}

if (
(path[0] === '"' && path[path.length - 1] !== '"') ||
(path[0] === "'" && path[path.length - 1] !== "'") ||
(path[0] !== "'" && path[0] !== '"')
) {
throw new Error('`@source` paths must be quoted.')
}
globs.push({ base: context.base as string, pattern: path.slice(1, -1) })

let source = path.slice(1, -1)

if (enableSourceInline && inline) {
let destination = not ? ignoredCandidates : inlineCandidates
for (let candidate of expand(source)) {
destination.push(candidate)
}
} else {
globs.push({ base: context.base as string, pattern: source })
}
replaceWith([])
return
}
Expand Down Expand Up @@ -505,6 +534,12 @@ async function parseCss(
designSystem.important = important
}

if (ignoredCandidates.length > 0) {
for (let candidate of ignoredCandidates) {
designSystem.invalidCandidates.add(candidate)
}
}

// Apply hooks from backwards compatibility layer. This function takes a lot
// of random arguments because it really just needs access to "the world" to
// do whatever ungodly things it needs to do to make things backwards
Expand Down Expand Up @@ -603,6 +638,7 @@ async function parseCss(
root,
utilitiesNode,
features,
inlineCandidates,
}
}

Expand All @@ -615,7 +651,8 @@ export async function compileAst(
features: Features
build(candidates: string[]): AstNode[]
}> {
let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(input, opts)
let { designSystem, ast, globs, root, utilitiesNode, features, inlineCandidates } =
await parseCss(input, opts)

if (process.env.NODE_ENV !== 'test') {
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
Expand All @@ -632,6 +669,14 @@ export async function compileAst(
let allValidCandidates = new Set<string>()
let compiled = null as AstNode[] | null
let previousAstNodeCount = 0
let defaultDidChange = false

for (let candidate of inlineCandidates) {
if (!designSystem.invalidCandidates.has(candidate)) {
allValidCandidates.add(candidate)
defaultDidChange = true
}
}

return {
globs,
Expand All @@ -647,7 +692,8 @@ export async function compileAst(
return compiled
}

let didChange = false
let didChange = defaultDidChange
defaultDidChange = false

// Add all new candidates unless we know that they are invalid.
let prevSize = allValidCandidates.size
Expand Down
14 changes: 14 additions & 0 deletions packages/tailwindcss/src/utils/brace-expansion.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// import braces from 'braces'
import { bench } from 'vitest'
import { expand } from './brace-expansion'

const PATTERN =
'{{xs,sm,md,lg}:,}{border-{x,y,t,r,b,l,s,e},bg,text,cursor,accent}-{{red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate,gray,zinc,neutral,stone}-{50,{100..900..100},950},black,white}{,/{0..100}}'

// bench('braces', () => {
// void braces.expand(PATTERN)
// })

bench('./brace-expansion', () => {
void expand(PATTERN)
})
80 changes: 80 additions & 0 deletions packages/tailwindcss/src/utils/brace-expansion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, test } from 'vitest'
import { expand } from './brace-expansion'

describe('expand(…)', () => {
test.each([
['a/b/c', ['a/b/c']],

// Groups
['a/{x,y,z}/b', ['a/x/b', 'a/y/b', 'a/z/b']],
['{a,b}/{x,y}', ['a/x', 'a/y', 'b/x', 'b/y']],
['{{xs,sm,md,lg}:,}hidden', ['xs:hidden', 'sm:hidden', 'md:hidden', 'lg:hidden', 'hidden']],

// Numeric ranges
['a/{0..5}/b', ['a/0/b', 'a/1/b', 'a/2/b', 'a/3/b', 'a/4/b', 'a/5/b']],
['a/{-5..0}/b', ['a/-5/b', 'a/-4/b', 'a/-3/b', 'a/-2/b', 'a/-1/b', 'a/0/b']],
['a/{0..-5}/b', ['a/0/b', 'a/-1/b', 'a/-2/b', 'a/-3/b', 'a/-4/b', 'a/-5/b']],

// Numeric range with padding
['a/{00..05}/b', ['a/00/b', 'a/01/b', 'a/02/b', 'a/03/b', 'a/04/b', 'a/05/b']],
[
'a{001..9}b',
['a001b', 'a002b', 'a003b', 'a004b', 'a005b', 'a006b', 'a007b', 'a008b', 'a009b'],
],

// Numeric range with step
['a/{0..5..2}/b', ['a/0/b', 'a/2/b', 'a/4/b']],
[
'bg-red-{100..900..100}',
[
'bg-red-100',
'bg-red-200',
'bg-red-300',
'bg-red-400',
'bg-red-500',
'bg-red-600',
'bg-red-700',
'bg-red-800',
'bg-red-900',
],
],

// Nested braces
['a{b,c,/{x,y}}/e', ['ab/e', 'ac/e', 'a/x/e', 'a/y/e']],
['a{b,c,/{x,y},{z,w}}/e', ['ab/e', 'ac/e', 'a/x/e', 'a/y/e', 'az/e', 'aw/e']],
['a{b,c,/{x,y},{0..2}}/e', ['ab/e', 'ac/e', 'a/x/e', 'a/y/e', 'a0/e', 'a1/e', 'a2/e']],
[
'bg-red-{50,{100..900..100},950}',
[
'bg-red-50',
'bg-red-100',
'bg-red-200',
'bg-red-300',
'bg-red-400',
'bg-red-500',
'bg-red-600',
'bg-red-700',
'bg-red-800',
'bg-red-900',
'bg-red-950',
],
],

// Should not try to expand ranges with decimals
['{1.1..2.2}', ['1.1..2.2']],
])('should expand %s', (input, expected) => {
expect(expand(input).sort()).toEqual(expected.sort())
})

test('throws on unbalanced braces', () => {
expect(() => expand('a{b,c{d,e},{f,g}h}x{y,z')).toThrowErrorMatchingInlineSnapshot(
`[Error: The pattern \`x{y,z\` is not balanced.]`,
)
})

test('throws when step is set to zero', () => {
expect(() => expand('a{0..5..0}/b')).toThrowErrorMatchingInlineSnapshot(
`[Error: Step cannot be zero in sequence expansion.]`,
)
})
})
Loading