From d0ade6fca8f1f8ff119de564dc6f11ae38341361 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 10 Sep 2024 18:25:19 +0200 Subject: [PATCH 01/51] Add `@import` resolver --- packages/tailwindcss/src/at-import.test.ts | 79 ++++ packages/tailwindcss/src/at-import.ts | 52 +++ packages/tailwindcss/src/index.ts | 20 +- playgrounds/vite/src/app.scss | 495 +++++++++++++++++++++ 4 files changed, 643 insertions(+), 3 deletions(-) create mode 100644 packages/tailwindcss/src/at-import.test.ts create mode 100644 packages/tailwindcss/src/at-import.ts create mode 100644 playgrounds/vite/src/app.scss diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts new file mode 100644 index 000000000000..aec9fbf480f7 --- /dev/null +++ b/packages/tailwindcss/src/at-import.test.ts @@ -0,0 +1,79 @@ +import { expect, it } from 'vitest' +import { compile } from './index' + +let css = String.raw + +async function run( + css: string, + resolveImport: (id: string, basedir: string) => Promise<{ content: string; basedir: string }>, +) { + let compiler = await compile(css, '/root', { resolveImport }) + return compiler.build([]) +} + +it('can resolve relative @imports', async () => { + let resolver = async (id: string, basedir: string) => { + expect(basedir).toBe('/root') + expect(id).toBe('./foo/bar.css') + return { + content: css` + .foo { + color: red; + } + `, + basedir: '/root/foo', + } + } + + await expect( + run( + css` + @import './foo/bar.css'; + `, + resolver, + ), + ).resolves.toMatchInlineSnapshot(` + ".foo { + color: red; + } + " + `) +}) + +it('can recursively resolve relative @imports', async () => { + let resolver = async (id: string, basedir: string) => { + if (basedir === '/root' && id === './foo/bar.css') { + return { + content: css` + @import './bar/baz.css'; + `, + basedir: '/root/foo', + } + } else if (basedir === '/root/foo' && id === './bar/baz.css') { + return { + content: css` + .baz { + color: blue; + } + `, + basedir: '/root/foo/bar', + } + } + + throw new Error(`Unexpected import: ${id}`) + } + + await expect( + run( + css` + @import './foo/bar.css'; + `, + resolver, + ), + ).resolves.toMatchInlineSnapshot(` + ".baz { + color: blue; + } + " + `) +}) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts new file mode 100644 index 000000000000..1f10e9e15674 --- /dev/null +++ b/packages/tailwindcss/src/at-import.ts @@ -0,0 +1,52 @@ +import { walk, WalkAction, type AstNode } from './ast' +import * as CSS from './css-parser' + +type ResolveImport = (id: string, basedir: string) => Promise<{ content: string; basedir: string }> + +export async function substituteAtImports( + ast: AstNode[], + basedir: string, + resolveImport: ResolveImport, +) { + let promises: Map> = new Map() + + walk(ast, (node) => { + // Find @import rules and start resolving them + if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { + let id = node.selector.slice('@import "'.length, -1) + promises.set(key(id, basedir), resolveAtImport(id, basedir, resolveImport)) + } + }) + + let entries = [...promises.entries()] + let resolvers = entries.map(async ([id, promise]) => [id, await promise] as const) + let resolved = await Promise.all(resolvers) + let unwrapped = new Map(resolved) + + walk(ast, (node, { replaceWith }) => { + // Replace all @import rules + if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { + let id = node.selector.slice('@import "'.length, -1) + let result = unwrapped.get(key(id, basedir)) + if (result) { + replaceWith(result) + return WalkAction.Skip + } + } + }) +} + +async function resolveAtImport( + id: string, + basedir: string, + resolveImport: ResolveImport, +): Promise { + const { content, basedir: nestedBaseDir } = await resolveImport(id, basedir) + let ast = CSS.parse(content) + await substituteAtImports(ast, nestedBaseDir, resolveImport) + return ast +} + +function key(id: string, basedir: string): string { + return `${id}:${basedir}` +} diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index ad441084c28c..0d54bea41819 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,6 +1,7 @@ import { version } from '../package.json' import { substituteAtApply } from './apply' import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' +import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' import { type Plugin } from './compat/plugin-api' @@ -17,6 +18,7 @@ const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { loadPlugin?: (path: string) => Promise loadConfig?: (path: string) => Promise + resolveImport?: (id: string, basedir: string) => Promise<{ content: string; basedir: string }> } function throwOnPlugin(): never { @@ -27,6 +29,10 @@ function throwOnConfig(): never { throw new Error('No `loadConfig` function provided to `compile`') } +function throwOnResolveImport(): never { + throw new Error('No `resolveImport` function provided to `compile`') +} + function parseThemeOptions(selector: string) { let options = ThemeOptions.NONE @@ -45,10 +51,17 @@ function parseThemeOptions(selector: string) { async function parseCss( css: string, - { loadPlugin = throwOnPlugin, loadConfig = throwOnConfig }: CompileOptions = {}, + basedir: string, + { + loadPlugin = throwOnPlugin, + loadConfig = throwOnConfig, + resolveImport = throwOnResolveImport, + }: CompileOptions = {}, ) { let ast = CSS.parse(css) + await substituteAtImports(ast, basedir, resolveImport) + // Find all `@theme` declarations let theme = new Theme() let customVariants: ((designSystem: DesignSystem) => void)[] = [] @@ -314,12 +327,13 @@ async function parseCss( export async function compile( css: string, + basedir: string, opts: CompileOptions = {}, ): Promise<{ globs: { origin?: string; pattern: string }[] build(candidates: string[]): string }> { - let { designSystem, ast, globs } = await parseCss(css, opts) + let { designSystem, ast, globs } = await parseCss(css, basedir, opts) let tailwindUtilitiesNode: Rule | null = null @@ -397,7 +411,7 @@ export async function compile( } export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) { - let result = await parseCss(css, opts) + let result = await parseCss(css, '', opts) return result.designSystem } diff --git a/playgrounds/vite/src/app.scss b/playgrounds/vite/src/app.scss new file mode 100644 index 000000000000..aeefee952e89 --- /dev/null +++ b/playgrounds/vite/src/app.scss @@ -0,0 +1,495 @@ +@use "sass:list"; +@use "sass:color"; + +@import 'tailwindcss/index.css'; +@tailwind utilities; +@theme default { + /* Defaults */ + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-family-sans); + --default-font-feature-settings: var(--font-family-sans--font-feature-settings); + --default-font-variation-settings: var(--font-family-sans--font-variation-settings); + --default-mono-font-family: var(--font-family-mono); + --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); + --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); + + /* Breakpoints */ + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; + + /* Colors */ + --color-black: #000; + --color-white: #fff; + --color-slate-50: #f8fafc; + --color-slate-100: #f1f5f9; + --color-slate-200: #e2e8f0; + --color-slate-300: #cbd5e1; + --color-slate-400: #94a3b8; + --color-slate-500: #64748b; + --color-slate-600: #475569; + --color-slate-700: #334155; + --color-slate-800: #1e293b; + --color-slate-900: #0f172a; + --color-slate-950: #020617; + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + --color-gray-950: #030712; + --color-zinc-50: #fafafa; + --color-zinc-100: #f4f4f5; + --color-zinc-200: #e4e4e7; + --color-zinc-300: #d4d4d8; + --color-zinc-400: #a1a1aa; + --color-zinc-500: #71717a; + --color-zinc-600: #52525b; + --color-zinc-700: #3f3f46; + --color-zinc-800: #27272a; + --color-zinc-900: #18181b; + --color-zinc-950: #09090b; + --color-neutral-50: #fafafa; + --color-neutral-100: #f5f5f5; + --color-neutral-200: #e5e5e5; + --color-neutral-300: #d4d4d4; + --color-neutral-400: #a3a3a3; + --color-neutral-500: #737373; + --color-neutral-600: #525252; + --color-neutral-700: #404040; + --color-neutral-800: #262626; + --color-neutral-900: #171717; + --color-neutral-950: #0a0a0a; + --color-stone-50: #fafaf9; + --color-stone-100: #f5f5f4; + --color-stone-200: #e7e5e4; + --color-stone-300: #d6d3d1; + --color-stone-400: #a8a29e; + --color-stone-500: #78716c; + --color-stone-600: #57534e; + --color-stone-700: #44403c; + --color-stone-800: #292524; + --color-stone-900: #1c1917; + --color-stone-950: #0c0a09; + --color-red-50: #fef2f2; + --color-red-100: #fee2e2; + --color-red-200: #fecaca; + --color-red-300: #fca5a5; + --color-red-400: #f87171; + --color-red-500: #ef4444; + --color-red-600: #dc2626; + --color-red-700: #b91c1c; + --color-red-800: #991b1b; + --color-red-900: #7f1d1d; + --color-red-950: #450a0a; + --color-orange-50: #fff7ed; + --color-orange-100: #ffedd5; + --color-orange-200: #fed7aa; + --color-orange-300: #fdba74; + --color-orange-400: #fb923c; + --color-orange-500: #f97316; + --color-orange-600: #ea580c; + --color-orange-700: #c2410c; + --color-orange-800: #9a3412; + --color-orange-900: #7c2d12; + --color-orange-950: #431407; + --color-amber-50: #fffbeb; + --color-amber-100: #fef3c7; + --color-amber-200: #fde68a; + --color-amber-300: #fcd34d; + --color-amber-400: #fbbf24; + --color-amber-500: #f59e0b; + --color-amber-600: #d97706; + --color-amber-700: #b45309; + --color-amber-800: #92400e; + --color-amber-900: #78350f; + --color-amber-950: #451a03; + --color-yellow-50: #fefce8; + --color-yellow-100: #fef9c3; + --color-yellow-200: #fef08a; + --color-yellow-300: #fde047; + --color-yellow-400: #facc15; + --color-yellow-500: #eab308; + --color-yellow-600: #ca8a04; + --color-yellow-700: #a16207; + --color-yellow-800: #854d0e; + --color-yellow-900: #713f12; + --color-yellow-950: #422006; + --color-lime-50: #f7fee7; + --color-lime-100: #ecfccb; + --color-lime-200: #d9f99d; + --color-lime-300: #bef264; + --color-lime-400: #a3e635; + --color-lime-500: #84cc16; + --color-lime-600: #65a30d; + --color-lime-700: #4d7c0f; + --color-lime-800: #3f6212; + --color-lime-900: #365314; + --color-lime-950: #1a2e05; + --color-green-50: #f0fdf4; + --color-green-100: #dcfce7; + --color-green-200: #bbf7d0; + --color-green-300: #86efac; + --color-green-400: #4ade80; + --color-green-500: #22c55e; + --color-green-600: #16a34a; + --color-green-700: #15803d; + --color-green-800: #166534; + --color-green-900: #14532d; + --color-green-950: #052e16; + --color-emerald-50: #ecfdf5; + --color-emerald-100: #d1fae5; + --color-emerald-200: #a7f3d0; + --color-emerald-300: #6ee7b7; + --color-emerald-400: #34d399; + --color-emerald-500: #10b981; + --color-emerald-600: #059669; + --color-emerald-700: #047857; + --color-emerald-800: #065f46; + --color-emerald-900: #064e3b; + --color-emerald-950: #022c22; + --color-teal-50: #f0fdfa; + --color-teal-100: #ccfbf1; + --color-teal-200: #99f6e4; + --color-teal-300: #5eead4; + --color-teal-400: #2dd4bf; + --color-teal-500: #14b8a6; + --color-teal-600: #0d9488; + --color-teal-700: #0f766e; + --color-teal-800: #115e59; + --color-teal-900: #134e4a; + --color-teal-950: #042f2e; + --color-cyan-50: #ecfeff; + --color-cyan-100: #cffafe; + --color-cyan-200: #a5f3fc; + --color-cyan-300: #67e8f9; + --color-cyan-400: #22d3ee; + --color-cyan-500: #06b6d4; + --color-cyan-600: #0891b2; + --color-cyan-700: #0e7490; + --color-cyan-800: #155e75; + --color-cyan-900: #164e63; + --color-cyan-950: #083344; + --color-sky-50: #f0f9ff; + --color-sky-100: #e0f2fe; + --color-sky-200: #bae6fd; + --color-sky-300: #7dd3fc; + --color-sky-400: #38bdf8; + --color-sky-500: #0ea5e9; + --color-sky-600: #0284c7; + --color-sky-700: #0369a1; + --color-sky-800: #075985; + --color-sky-900: #0c4a6e; + --color-sky-950: #082f49; + --color-blue-50: #eff6ff; + --color-blue-100: #dbeafe; + --color-blue-200: #bfdbfe; + --color-blue-300: #93c5fd; + --color-blue-400: #60a5fa; + --color-blue-500: #3b82f6; + --color-blue-600: #2563eb; + --color-blue-700: #1d4ed8; + --color-blue-800: #1e40af; + --color-blue-900: #1e3a8a; + --color-blue-950: #172554; + --color-indigo-50: #eef2ff; + --color-indigo-100: #e0e7ff; + --color-indigo-200: #c7d2fe; + --color-indigo-300: #a5b4fc; + --color-indigo-400: #818cf8; + --color-indigo-500: #6366f1; + --color-indigo-600: #4f46e5; + --color-indigo-700: #4338ca; + --color-indigo-800: #3730a3; + --color-indigo-900: #312e81; + --color-indigo-950: #1e1b4b; + --color-violet-50: #f5f3ff; + --color-violet-100: #ede9fe; + --color-violet-200: #ddd6fe; + --color-violet-300: #c4b5fd; + --color-violet-400: #a78bfa; + --color-violet-500: #8b5cf6; + --color-violet-600: #7c3aed; + --color-violet-700: #6d28d9; + --color-violet-800: #5b21b6; + --color-violet-900: #4c1d95; + --color-violet-950: #2e1065; + --color-purple-50: #faf5ff; + --color-purple-100: #f3e8ff; + --color-purple-200: #e9d5ff; + --color-purple-300: #d8b4fe; + --color-purple-400: #c084fc; + --color-purple-500: #a855f7; + --color-purple-600: #9333ea; + --color-purple-700: #7e22ce; + --color-purple-800: #6b21a8; + --color-purple-900: #581c87; + --color-purple-950: #3b0764; + --color-fuchsia-50: #fdf4ff; + --color-fuchsia-100: #fae8ff; + --color-fuchsia-200: #f5d0fe; + --color-fuchsia-300: #f0abfc; + --color-fuchsia-400: #e879f9; + --color-fuchsia-500: #d946ef; + --color-fuchsia-600: #c026d3; + --color-fuchsia-700: #a21caf; + --color-fuchsia-800: #86198f; + --color-fuchsia-900: #701a75; + --color-fuchsia-950: #4a044e; + --color-pink-50: #fdf2f8; + --color-pink-100: #fce7f3; + --color-pink-200: #fbcfe8; + --color-pink-300: #f9a8d4; + --color-pink-400: #f472b6; + --color-pink-500: #ec4899; + --color-pink-600: #db2777; + --color-pink-700: #be185d; + --color-pink-800: #9d174d; + --color-pink-900: #831843; + --color-pink-950: #500724; + --color-rose-50: #fff1f2; + --color-rose-100: #ffe4e6; + --color-rose-200: #fecdd3; + --color-rose-300: #fda4af; + --color-rose-400: #fb7185; + --color-rose-500: #f43f5e; + --color-rose-600: #e11d48; + --color-rose-700: #be123c; + --color-rose-800: #9f1239; + --color-rose-900: #881337; + --color-rose-950: #4c0519; + + /* Animations */ + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --animate-bounce: bounce 1s infinite; + + /* Blurs */ + --blur: 8px; + --blur-sm: 4px; + --blur-md: 12px; + --blur-lg: 16px; + --blur-xl: 24px; + --blur-2xl: 40px; + --blur-3xl: 64px; + + /* Radii */ + --radius: 0.25rem; + --radius-sm: 0.125rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --radius-4xl: 2rem; + + /* Shadows */ + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-xs: 0 1px rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); + --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + + /* Inset shadows */ + --inset-shadow-xs: inset 0 1px rgb(0 0 0 / 0.05); + --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); + --inset-shadow: inset 0 2px 4px rgb(0 0 0 / 0.05); + + /* Drop shadows */ + --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06); + --drop-shadow-sm: 0 1px 1px rgb(0 0 0 / 0.05); + --drop-shadow-md: 0 4px 3px rgb(0 0 0 / 0.07), 0 2px 2px rgb(0 0 0 / 0.06); + --drop-shadow-lg: 0 10px 8px rgb(0 0 0 / 0.04), 0 4px 3px rgb(0 0 0 / 0.1); + --drop-shadow-xl: 0 20px 13px rgb(0 0 0 / 0.03), 0 8px 5px rgb(0 0 0 / 0.08); + --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); + --drop-shadow-none: 0 0 #0000; + + /* Spacing */ + --spacing-px: 1px; + --spacing-0: 0px; + --spacing-0_5: 0.125rem; + --spacing-1: 0.25rem; + --spacing-1_5: 0.375rem; + --spacing-2: 0.5rem; + --spacing-2_5: 0.625rem; + --spacing-3: 0.75rem; + --spacing-3_5: 0.875rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-7: 1.75rem; + --spacing-8: 2rem; + --spacing-9: 2.25rem; + --spacing-10: 2.5rem; + --spacing-11: 2.75rem; + --spacing-12: 3rem; + --spacing-14: 3.5rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + --spacing-28: 7rem; + --spacing-32: 8rem; + --spacing-36: 9rem; + --spacing-40: 10rem; + --spacing-44: 11rem; + --spacing-48: 12rem; + --spacing-52: 13rem; + --spacing-56: 14rem; + --spacing-60: 15rem; + --spacing-64: 16rem; + --spacing-72: 18rem; + --spacing-80: 20rem; + --spacing-96: 24rem; + + /* Widths */ + --width-3xs: 16rem; + --width-2xs: 18rem; + --width-xs: 20rem; + --width-sm: 24rem; + --width-md: 28rem; + --width-lg: 32rem; + --width-xl: 36rem; + --width-2xl: 42rem; + --width-3xl: 48rem; + --width-4xl: 56rem; + --width-5xl: 64rem; + --width-6xl: 72rem; + --width-7xl: 80rem; + --width-prose: 65ch; + + /* Fonts */ + --font-family-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-family-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; + --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; + + /* Type scale */ + --font-size-xs: 0.75rem; + --font-size-xs--line-height: 1rem; + --font-size-sm: 0.875rem; + --font-size-sm--line-height: 1.25rem; + --font-size-base: 1rem; + --font-size-base--line-height: 1.5rem; + --font-size-lg: 1.125rem; + --font-size-lg--line-height: 1.75rem; + --font-size-xl: 1.25rem; + --font-size-xl--line-height: 1.75rem; + --font-size-2xl: 1.5rem; + --font-size-2xl--line-height: 2rem; + --font-size-3xl: 1.875rem; + --font-size-3xl--line-height: 2.25rem; + --font-size-4xl: 2.25rem; + --font-size-4xl--line-height: 2.5rem; + --font-size-5xl: 3rem; + --font-size-5xl--line-height: 1; + --font-size-6xl: 3.75rem; + --font-size-6xl--line-height: 1; + --font-size-7xl: 4.5rem; + --font-size-7xl--line-height: 1; + --font-size-8xl: 6rem; + --font-size-8xl--line-height: 1; + --font-size-9xl: 8rem; + --font-size-9xl--line-height: 1; + + /* Letter spacing */ + --letter-spacing-tighter: -0.05em; + --letter-spacing-tight: -0.025em; + --letter-spacing-normal: 0em; + --letter-spacing-wide: 0.025em; + --letter-spacing-wider: 0.05em; + --letter-spacing-widest: 0.1em; + + /* Line-height */ + --line-height-none: 1; + --line-height-tight: 1.25; + --line-height-snug: 1.375; + --line-height-normal: 1.5; + --line-height-relaxed: 1.625; + --line-height-loose: 2; + --line-height-3: 0.75rem; + --line-height-4: 1rem; + --line-height-5: 1.25rem; + --line-height-6: 1.5rem; + --line-height-7: 1.75rem; + --line-height-8: 2rem; + --line-height-9: 2.25rem; + --line-height-10: 2.5rem; + + /* 3D perspectives */ + --perspective-dramatic: 100px; + --perspective-near: 300px; + --perspective-normal: 500px; + --perspective-midrange: 800px; + --perspective-distant: 1200px; + + /* Transition timing functions */ + --transition-timing-function-linear: linear; + --transition-timing-function-in: cubic-bezier(0.4, 0, 1, 1); + --transition-timing-function-out: cubic-bezier(0, 0, 0.2, 1); + --transition-timing-function-in-out: cubic-bezier(0.4, 0, 0.2, 1); + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + @keyframes ping { + + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } + + @keyframes pulse { + 50% { + opacity: 0.5; + } + } + + @keyframes bounce { + + 0%, + 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + } +} +@plugin "./plugin.js"; + +$font-stack: Helvetica, Arial; +$primary-color: #333; + +body { + $font-stack: list.append($font-stack, sans-serif); + font: $font-stack; +} + +a { + color: $primary-color; + + &:hover { + color: color.scale($primary-color, $lightness: 20%); + } +} From e5768a10dd28ebd8bb7c566ed1649d4c2ffa163b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 11 Sep 2024 13:13:12 +0200 Subject: [PATCH 02/51] Add bench --- .../src/fixtures/src/a/index.css | 1 + .../src/fixtures/src/b/example.css | 1 + .../src/fixtures/src/b/index.css | 3 ++ .../src/fixtures/src/b/tw.css | 1 + .../src/fixtures/src/index.css | 2 + .../src/fixtures/src/invalid.css | 1 + .../src/fixtures/src/plugins-in-root.css | 5 ++ .../src/fixtures/src/plugins-in-sibling.css | 3 ++ packages/@tailwindcss-vite/src/index.bench.ts | 31 ++++++++++++ packages/@tailwindcss-vite/src/index.ts | 48 +++++++++++-------- 10 files changed, 76 insertions(+), 20 deletions(-) create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/a/index.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/b/example.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/b/index.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/b/tw.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/index.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/invalid.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css create mode 100644 packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css create mode 100644 packages/@tailwindcss-vite/src/index.bench.ts diff --git a/packages/@tailwindcss-vite/src/fixtures/src/a/index.css b/packages/@tailwindcss-vite/src/fixtures/src/a/index.css new file mode 100644 index 000000000000..967fcc0b332d --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/a/index.css @@ -0,0 +1 @@ +@import '../b/example.css'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/b/example.css b/packages/@tailwindcss-vite/src/fixtures/src/b/example.css new file mode 100644 index 000000000000..1d637a65c092 --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/b/example.css @@ -0,0 +1 @@ +@import './tw.css'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/b/index.css b/packages/@tailwindcss-vite/src/fixtures/src/b/index.css new file mode 100644 index 000000000000..bb7dac823454 --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/b/index.css @@ -0,0 +1,3 @@ +.red { + @apply bg-red-500; +} diff --git a/packages/@tailwindcss-vite/src/fixtures/src/b/tw.css b/packages/@tailwindcss-vite/src/fixtures/src/b/tw.css new file mode 100644 index 000000000000..73a943cd0413 --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/b/tw.css @@ -0,0 +1 @@ +@import "tailwindcss" diff --git a/packages/@tailwindcss-vite/src/fixtures/src/index.css b/packages/@tailwindcss-vite/src/fixtures/src/index.css new file mode 100644 index 000000000000..7ee439bdae04 --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/index.css @@ -0,0 +1,2 @@ +@import "./a"; +@import "./b"; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/invalid.css b/packages/@tailwindcss-vite/src/fixtures/src/invalid.css new file mode 100644 index 000000000000..b69d455c0db2 --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/invalid.css @@ -0,0 +1 @@ +@import '../../example-project/src/invalid.css'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css b/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css new file mode 100644 index 000000000000..d6d5f082c364 --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css @@ -0,0 +1,5 @@ +@import './plugins-in-sibling.css'; + +@plugin './plugin-in-root.ts'; +@plugin '../plugin-in-root.ts'; +@plugin 'plugin-in-root'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css b/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css new file mode 100644 index 000000000000..5df3cb06176a --- /dev/null +++ b/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css @@ -0,0 +1,3 @@ +@plugin './plugin-in-sibling.ts'; +@plugin '../plugin-in-sibling.ts'; +@plugin 'plugin-in-sibling'; diff --git a/packages/@tailwindcss-vite/src/index.bench.ts b/packages/@tailwindcss-vite/src/index.bench.ts new file mode 100644 index 000000000000..8e0719f9fb62 --- /dev/null +++ b/packages/@tailwindcss-vite/src/index.bench.ts @@ -0,0 +1,31 @@ +import fs from 'node:fs' +import { bench, describe } from 'vitest' + +import path from 'node:path' +import { Root } from '.' + +let emptySetGetter = () => new Set() + +let base = path.join(__dirname, 'fixtures/src') +let filepath = path.join(base, 'index.css') +let contents = fs.readFileSync(filepath, 'utf-8') + +describe('compare', () => { + bench('postcss-import', async () => { + try { + let root = new Root(filepath, emptySetGetter, base, 'postcss') + await root.generate(contents, () => {}) + } catch (e) { + console.error(e) + } + }) + + bench('tailwindcss-import', async () => { + try { + let root = new Root(filepath, emptySetGetter, base, 'tailwindcss') + await root.generate(contents, () => {}) + } catch (e) { + console.error(e) + } + }) +}) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 8a1c60932e00..29905c693b8c 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -6,8 +6,6 @@ import fixRelativePathsPlugin, { normalizePath } from 'internal-postcss-fix-rela import { Features, transform } from 'lightningcss' import fs from 'node:fs/promises' import path from 'path' -import postcss from 'postcss' -import postcssImport from 'postcss-import' import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' export default function tailwindcss(): Plugin[] { @@ -330,7 +328,7 @@ class DefaultMap extends Map { } } -class Root { +export class Root { // Content is only used in serve mode where we need to capture the initial // contents of the root file so that we can restore it during the // `renderStart` hook. @@ -361,6 +359,7 @@ class Root { private id: string, private getSharedCandidates: () => Set, private base: string, + private atImportResolver: 'postcss' | 'tailwindcss' = 'postcss', ) {} // Generate the CSS for the root file. This can return false if the file is @@ -378,28 +377,37 @@ class Root { clearRequireCache(Array.from(this.dependencies)) this.dependencies = new Set([idToPath(inputPath)]) - let postcssCompiled = await postcss([ - postcssImport({ - load: (path) => { - this.dependencies.add(path) - addWatchFile(path) - return fs.readFile(path, 'utf8') - }, - }), - fixRelativePathsPlugin(), - ]).process(content, { - from: inputPath, - to: inputPath, - }) - let css = postcssCompiled.css + let css = content + if (this.atImportResolver === 'postcss') { + const [{ default: postcss }, { default: postcssImport }] = await Promise.all([ + import('postcss'), + import('postcss-import'), + ]) + + let postcssCompiled = await postcss([ + postcssImport({ + load: (path) => { + this.dependencies.add(path) + addWatchFile(path) + return fs.readFile(path, 'utf8') + }, + }), + fixRelativePathsPlugin(), + ]).process(content, { + from: inputPath, + to: inputPath, + }) + + css = postcssCompiled.css + } // This is done inside the Root#generate() method so that we can later use // information from the Tailwind compiler to determine if the file is a // CSS root (necessary because we will probably inline the `@import` // resolution at some point). - if (!isCssRootFile(css)) { - return false - } + // if (!isCssRootFile(css)) { + // return false + // } this.compiler = await compile(css, { base: inputBase, From fa14f5455c7cfef6ea6d1d625f56345e76bedd92 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 11 Sep 2024 13:13:20 +0200 Subject: [PATCH 03/51] Parse @import params --- packages/@tailwindcss-node/package.json | 1 + packages/@tailwindcss-node/src/compile.ts | 33 +++- .../example-project/src/relative-import.css | 5 +- packages/tailwindcss/src/at-import.test.ts | 159 +++++++++++++++++- packages/tailwindcss/src/at-import.ts | 106 +++++++++++- packages/tailwindcss/src/index.ts | 4 +- packages/tailwindcss/src/test-utils/run.ts | 4 +- pnpm-lock.yaml | 3 + 8 files changed, 298 insertions(+), 17 deletions(-) diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 652e65538982..97f63f77e40a 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -40,6 +40,7 @@ "tailwindcss": "workspace:^" }, "dependencies": { + "enhanced-resolve": "^5.17.1", "jiti": "^2.0.0-beta.3" } } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 78c68d9ae06b..97ff1b93bd97 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -1,4 +1,7 @@ +import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' +import fs from 'node:fs' +import fsPromises from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' import { compile as _compile } from 'tailwindcss' @@ -8,7 +11,7 @@ export async function compile( css: string, { base, onDependency }: { base: string; onDependency: (path: string) => void }, ) { - return await _compile(css, { + return await _compile(css, base, { loadPlugin: async (pluginPath) => { if (pluginPath[0] !== '.') { return importModule(pluginPath).then((m) => m.default ?? m) @@ -44,6 +47,16 @@ export async function compile( } return module.default ?? module }, + + async resolveImport(id, basedir) { + let full = await resolveCssId(id, basedir) + if (!full) throw new Error(`Could not resolve '${id}' from '${basedir}'`) + let file = await fsPromises.readFile(full, 'utf-8') + return { + content: file, + basedir: path.dirname(full), + } + }, }) } @@ -62,3 +75,21 @@ async function importModule(path: string): Promise { throw error } } + +const resolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], +}) +export function resolveCssId(id: string, base: string) { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return resolved + } + } + + return resolver.resolveSync({}, base, id) +} diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css index 48a30ab4dca7..088dd2a05919 100644 --- a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css @@ -1 +1,4 @@ -@plugin '../plugin.js'; +/* @plugin '../plugin.js'; */ +.foo { + @apply text-red-500 +} diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index aec9fbf480f7..085c542e9ba5 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,17 +1,19 @@ -import { expect, it } from 'vitest' +import { expect, test } from 'vitest' import { compile } from './index' +import { optimizeCss } from './test-utils/run' let css = String.raw async function run( css: string, resolveImport: (id: string, basedir: string) => Promise<{ content: string; basedir: string }>, + candidates: string[] = [], ) { let compiler = await compile(css, '/root', { resolveImport }) - return compiler.build([]) + return optimizeCss(compiler.build(candidates)) } -it('can resolve relative @imports', async () => { +test('can resolve relative @imports', async () => { let resolver = async (id: string, basedir: string) => { expect(basedir).toBe('/root') expect(id).toBe('./foo/bar.css') @@ -40,7 +42,7 @@ it('can resolve relative @imports', async () => { `) }) -it('can recursively resolve relative @imports', async () => { +test('can recursively resolve relative @imports', async () => { let resolver = async (id: string, basedir: string) => { if (basedir === '/root' && id === './foo/bar.css') { return { @@ -72,8 +74,155 @@ it('can recursively resolve relative @imports', async () => { ), ).resolves.toMatchInlineSnapshot(` ".baz { - color: blue; + color: #00f; } " `) }) + +let exampleCSS = css` + a { + color: red; + } +` +let resolver = async (id: string) => { + return { + content: exampleCSS, + basedir: '/root', + } +} + +// Examples from https://developer.mozilla.org/en-US/docs/Web/CSS/@import +test.each([ + // @media + [ + css` + @import url('example.css') print; + `, + optimizeCss(css` + @media print { + ${exampleCSS} + } + `), + ], + [ + css` + @import url('example.css') print, screen; + `, + optimizeCss(css` + @media print, screen { + ${exampleCSS} + } + `), + ], + [ + css` + @import 'example.css' screen; + `, + optimizeCss(css` + @media screen { + ${exampleCSS} + } + `), + ], + [ + css` + @import url('example.css') screen and (orientation: landscape); + `, + optimizeCss(css` + @media screen and (orientation: landscape) { + ${exampleCSS} + } + `), + ], + + // @supports + [ + css` + @import url('example.css') supports(display: grid) screen and (max-width: 400px); + `, + optimizeCss(css` + @supports (display: grid) { + @media screen and (max-width: 400px) { + ${exampleCSS} + } + } + `), + ], + [ + css` + @import url('example.css') supports((not (display: grid)) and (display: flex)) screen and + (max-width: 400px); + `, + optimizeCss(css` + @supports (not (display: grid)) and (display: flex) { + @media screen and (max-width: 400px) { + ${exampleCSS} + } + } + `), + ], + [ + // prettier-ignore + css` + @import url('example.css') + supports((selector(h2 > p)) and (font-tech(color-COLRv1))); + `, + optimizeCss(css` + @supports (selector(h2 > p)) and (font-tech(color-COLRv1)) { + ${exampleCSS} + } + `), + ], + + // @layer + [ + css` + @import 'example.css' layer(utilities); + `, + optimizeCss(css` + @layer utilities { + ${exampleCSS} + } + `), + ], + [ + css` + @import 'example.css' layer(); + `, + optimizeCss(css` + @layer { + ${exampleCSS} + } + `), + ], +])('resolves %s', async (input, output) => { + await expect(run(input, resolver)).resolves.toBe(output) +}) + +test('supports theme(reference) imports', async () => { + expect( + run( + css` + @tailwind utilities; + @import 'example.css' theme(reference); + `, + () => + Promise.resolve({ + content: css` + @theme { + --color-red-500: red; + } + `, + basedir: '', + }), + ['text-red-500'], + ), + ).resolves.toBe( + optimizeCss(css` + .text-red-500 { + color: var(--color-red-500, red); + } + `), + ) +}) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 1f10e9e15674..cacb3e22eb60 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -1,5 +1,6 @@ -import { walk, WalkAction, type AstNode } from './ast' +import { rule, walk, WalkAction, type AstNode } from './ast' import * as CSS from './css-parser' +import * as ValueParser from './value-parser' type ResolveImport = (id: string, basedir: string) => Promise<{ content: string; basedir: string }> @@ -13,8 +14,14 @@ export async function substituteAtImports( walk(ast, (node) => { // Find @import rules and start resolving them if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { - let id = node.selector.slice('@import "'.length, -1) - promises.set(key(id, basedir), resolveAtImport(id, basedir, resolveImport)) + let { uri } = parseImportParams(ValueParser.parse(node.selector.slice(8))) + + // Skip `@import`ing data URIs + if (uri.startsWith('data:')) { + return + } + + promises.set(key(uri, basedir), resolveAtImport(uri, basedir, resolveImport)) } }) @@ -26,10 +33,12 @@ export async function substituteAtImports( walk(ast, (node, { replaceWith }) => { // Replace all @import rules if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { - let id = node.selector.slice('@import "'.length, -1) - let result = unwrapped.get(key(id, basedir)) - if (result) { - replaceWith(result) + let { uri, layer, media, supports } = parseImportParams( + ValueParser.parse(node.selector.slice(8)), + ) + let importedAst = unwrapped.get(key(uri, basedir)) + if (importedAst) { + replaceWith(buildImportNodes(importedAst, layer, media, supports)) return WalkAction.Skip } } @@ -50,3 +59,86 @@ async function resolveAtImport( function key(id: string, basedir: string): string { return `${id}:${basedir}` } + +// c.f. https://github.com/postcss/postcss-import/blob/master/lib/parse-statements.js +function parseImportParams(params: ValueParser.ValueAstNode[]) { + let uri + let layer: string | null = null + let media: string | null = null + let supports: string | null = null + + for (let i = 0; i < params.length; i++) { + const node = params[i] + + if (node.kind === 'separator') continue + + if (node.kind === 'word' && !uri) { + if (!node.value) throw new Error(`Unable to find uri`) + if (node.value[0] !== '"' && node.value[0] !== "'") throw new Error('Unable to find uri') + + uri = node.value.slice(1, -1) + continue + } + + if (node.kind === 'function' && /^url$/i.test(node.value)) { + if (uri) throw new Error("Multiple url's") + if (!node.nodes?.[0]?.value) throw new Error('Unable to find uri') + if (node.nodes[0].value[0] !== '"' && node.nodes[0].value[0] !== "'") + throw new Error('Unable to find uri') + + uri = node.nodes[0].value.slice(1, -1) + continue + } + + if (!uri) throw new Error('Unable to find uri') + + if ((node.kind === 'word' || node.kind === 'function') && /^layer$/i.test(node.value)) { + if (layer) throw new Error('Multiple layers') + if (supports) throw new Error('layers must be defined before support conditions') + + if ('nodes' in node) { + layer = ValueParser.toCss(node.nodes) + } else { + layer = '' + } + + continue + } + + if (node.kind === 'function' && /^supports$/i.test(node.value)) { + if (supports) throw new Error('Multiple support conditions') + supports = ValueParser.toCss(node.nodes) + continue + } + + media = ValueParser.toCss(params.slice(i)) + break + } + + if (!uri) throw new Error('Unable to find uri') + + return { uri, layer, media, supports } +} + +function buildImportNodes( + importedAst: AstNode[], + layer: string | null, + media: string | null, + supports: string | null, +): AstNode[] { + let root = importedAst + + if (layer !== null) { + root = [rule('@layer ' + layer, root)] + } + + if (media !== null) { + root = [rule('@media ' + media, root)] + } + + if (supports !== null) { + root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] + } + + return root +} diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 0d54bea41819..b2967d9e4ce0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -60,7 +60,9 @@ async function parseCss( ) { let ast = CSS.parse(css) - await substituteAtImports(ast, basedir, resolveImport) + if (css.includes('@import')) { + await substituteAtImports(ast, basedir, resolveImport) + } // Find all `@theme` declarations let theme = new Theme() diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index d5fd54c03fbd..5af54b592677 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -2,12 +2,12 @@ import { Features, transform } from 'lightningcss' import { compile } from '..' export async function compileCss(css: string, candidates: string[] = []) { - let { build } = await compile(css) + let { build } = await compile(css, '') return optimizeCss(build(candidates)).trim() } export async function run(candidates: string[]) { - let { build } = await compile('@tailwind utilities;') + let { build } = await compile('@tailwind utilities;', '') return optimizeCss(build(candidates)).trim() } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3735d13ea5d4..287241ada722 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,9 @@ importers: packages/@tailwindcss-node: dependencies: + enhanced-resolve: + specifier: ^5.17.1 + version: 5.17.1 jiti: specifier: ^2.0.0-beta.3 version: 2.0.0-beta.3 From 911f2f05804f6418ca4f8e95f793aeba66ef227e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 17 Sep 2024 17:19:45 +0200 Subject: [PATCH 04/51] wip --- packages/tailwindcss/src/at-import.ts | 6 +- .../tailwindcss/src/compat/config.test.ts | 50 ++++++------- .../tailwindcss/src/compat/plugin-api.test.ts | 72 ++++++++++++++----- packages/tailwindcss/src/index.ts | 2 +- 4 files changed, 82 insertions(+), 48 deletions(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index cacb3e22eb60..f3b7ff443c4e 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -16,10 +16,8 @@ export async function substituteAtImports( if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { let { uri } = parseImportParams(ValueParser.parse(node.selector.slice(8))) - // Skip `@import`ing data URIs - if (uri.startsWith('data:')) { - return - } + // Skip importing data URIs + if (uri.startsWith('data:')) return promises.set(key(uri, basedir), resolveAtImport(uri, basedir, resolveImport)) } diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index acfaebc9e411..db8f8e9ab6fe 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -11,7 +11,7 @@ test('Config files can add content', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ content: ['./file.txt'] }), }) @@ -24,7 +24,7 @@ test('Config files can change dark mode (media)', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ darkMode: 'media' }), }) @@ -44,7 +44,7 @@ test('Config files can change dark mode (selector)', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ darkMode: 'selector' }), }) @@ -64,7 +64,7 @@ test('Config files can change dark mode (variant)', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ darkMode: ['variant', '&:where(:not(.light))'] }), }) @@ -84,7 +84,7 @@ test('Config files can add plugins', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ plugins: [ plugin(function ({ addUtilities }) { @@ -112,7 +112,7 @@ test('Plugins loaded from config files can contribute to the config', async () = @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ plugins: [ plugin(() => {}, { @@ -138,7 +138,7 @@ test('Config file presets can contribute to the config', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ presets: [ { @@ -164,7 +164,7 @@ test('Config files can affect the theme', async () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { extend: { @@ -205,7 +205,7 @@ test('Variants in CSS overwrite variants from plugins', async () => { @variant light (&:is(.my-light)); ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ darkMode: ['variant', '&:is(.dark)'], plugins: [ @@ -252,7 +252,7 @@ describe('theme callbacks', () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { extend: { @@ -360,7 +360,7 @@ describe('theme overrides order', () => { @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { extend: { @@ -403,7 +403,7 @@ describe('theme overrides order', () => { @plugin "./plugin.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { extend: { @@ -523,7 +523,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -559,7 +559,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -596,7 +596,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -633,7 +633,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -677,7 +677,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -714,7 +714,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -750,7 +750,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -781,7 +781,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -817,7 +817,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -854,7 +854,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -891,7 +891,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -935,7 +935,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { @@ -972,7 +972,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { fontFamily: { diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index f0d0b1102f1d..e14511a49375 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -14,7 +14,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ addBase, theme }) { @@ -77,7 +77,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -122,7 +122,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -166,7 +166,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -217,7 +217,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin(function ({ addUtilities, theme }) { addUtilities({ @@ -257,7 +257,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -308,7 +308,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -352,7 +352,7 @@ describe('theme', async () => { } ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin(function ({ matchUtilities, theme }) { matchUtilities( @@ -407,7 +407,7 @@ describe('theme', async () => { } ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -458,7 +458,7 @@ describe('theme', async () => { } ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -504,7 +504,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, { + await compile(input, '/root', { loadPlugin: async () => { return plugin( function ({ theme }) { @@ -536,7 +536,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let { build } = await compile(input, { + let { build } = await compile(input, '/root', { loadPlugin: async () => { return plugin(function ({ matchUtilities, theme }) { function utility(name: string, themeKey: string) { @@ -780,7 +780,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, { + await compile(input, '/root', { loadPlugin: async () => { return plugin( ({ theme }) => { @@ -819,7 +819,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, { + await compile(input, '/root', { loadPlugin: async () => { return plugin(({ theme }) => { fn(theme('transitionTimingFunction.DEFAULT')) @@ -847,7 +847,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, { + await compile(input, '/root', { loadPlugin: async () => { return plugin(({ theme }) => { fn(theme('color.red.100')) @@ -872,7 +872,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, { + await compile(input, '/root', { loadPlugin: async () => { return plugin(({ theme }) => { fn(theme('i.do.not.exist')) @@ -895,7 +895,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let { build } = await compile(input, { + let { build } = await compile(input, '/root', { loadPlugin: async () => { return plugin(({ addUtilities, matchUtilities }) => { addUtilities({ @@ -947,7 +947,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let { build } = await compile(input, { + let { build } = await compile(input, '/root', { loadPlugin: async () => { return plugin(function ({ matchUtilities }) { function utility(name: string, themeKey: string) { @@ -1166,6 +1166,7 @@ describe('addVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1197,6 +1198,7 @@ describe('addVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1229,6 +1231,7 @@ describe('addVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1263,6 +1266,7 @@ describe('addVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1311,6 +1315,7 @@ describe('addVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1353,6 +1358,7 @@ describe('addVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1392,6 +1398,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1423,6 +1430,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1458,6 +1466,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1500,6 +1509,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1536,6 +1546,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1587,6 +1598,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1616,6 +1628,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1665,6 +1678,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1717,6 +1731,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1788,6 +1803,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1841,6 +1857,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1910,6 +1927,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1979,6 +1997,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2056,6 +2075,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2087,6 +2107,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2108,6 +2129,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2137,6 +2159,7 @@ describe('matchVariant', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2172,6 +2195,7 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2210,6 +2234,7 @@ describe('addUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2237,6 +2262,7 @@ describe('addUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2273,6 +2299,7 @@ describe('addUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2304,6 +2331,7 @@ describe('addUtilities()', () => { @tailwind utilities; } `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2342,6 +2370,7 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2395,6 +2424,7 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2421,6 +2451,7 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2461,6 +2492,7 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2581,6 +2613,7 @@ describe('matchUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, + '/base', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3134,6 +3167,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/base', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3223,6 +3257,7 @@ describe('addComponents()', () => { @plugin "my-plugin"; @tailwind utilities; `, + '/base', { async loadPlugin() { return ({ addComponents }: PluginAPI) => { @@ -3288,6 +3323,7 @@ describe('prefix()', () => { css` @plugin "my-plugin"; `, + '/base', { async loadPlugin() { return ({ prefix }: PluginAPI) => { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index b2967d9e4ce0..0d4341b5b44c 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -413,7 +413,7 @@ export async function compile( } export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) { - let result = await parseCss(css, '', opts) + let result = await parseCss(css, '/* @TODO */', opts) return result.designSystem } From e3b067d12a5ef85f52a1799b4d8a53af1baeb431 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 14:48:50 +0200 Subject: [PATCH 05/51] Make tests pass again --- .../@tailwindcss-postcss/src/index.test.ts | 22 +- .../tailwindcss/src/compat/config.test.ts | 2 +- .../tailwindcss/src/compat/plugin-api.test.ts | 10 + .../tailwindcss/src/css-functions.test.ts | 3 + packages/tailwindcss/src/index.test.ts | 349 ++++++++++-------- packages/tailwindcss/src/plugin.test.ts | 4 +- packages/tailwindcss/src/test-utils/run.ts | 4 +- 7 files changed, 239 insertions(+), 155 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index ab1c25dd415d..1639ec5e5b71 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -169,6 +169,7 @@ describe('plugins', () => { let result = await processor.process( css` + @import 'tailwindcss/theme' theme(reference); @import 'tailwindcss/utilities'; @import '../example-project/src/relative-import.css'; `, @@ -176,18 +177,27 @@ describe('plugins', () => { ) expect(result.css.trim()).toMatchInlineSnapshot(` - ".underline { + ".text-2xl { + font-size: var(--font-size-2xl, 1.5rem); + line-height: var(--font-size-2xl--line-height, 2rem); + } + + .text-black\\/50 { + color: color-mix(in srgb, var(--color-black, #000) 50%, transparent); + } + + .underline { text-decoration-line: underline; } - @media (inverted-colors: inverted) { - .inverted\\:flex { - display: flex; + @media (width >= 96rem) { + .\\32 xl\\:font-bold { + font-weight: 700; } } - .hocus\\:underline:focus, .hocus\\:underline:hover { - text-decoration-line: underline; + .foo { + color: var(--color-red-500, #ef4444); }" `) }) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index db8f8e9ab6fe..6c984130e191 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -999,7 +999,7 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s @config "./config.js"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadConfig: async () => ({ theme: { extend: { diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index e14511a49375..041d51b5395e 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -2538,6 +2538,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2659,6 +2660,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2725,6 +2727,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2795,6 +2798,7 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2846,6 +2850,7 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2880,6 +2885,7 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2920,6 +2926,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3039,6 +3046,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3112,6 +3120,7 @@ describe('matchUtilities()', () => { --opacity-my-opacity: 0.5; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3233,6 +3242,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + '/root', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index b40dc9a2b3ce..f7c5f6688884 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -335,6 +335,7 @@ describe('theme function', () => { font-family: theme(fontFamily.sans); } `, + '/root', { loadConfig: async () => ({}), }, @@ -794,6 +795,7 @@ describe('in plugins', () => { @tailwind utilities; } `, + '/root', { async loadPlugin() { return plugin(({ addBase, addUtilities }) => { @@ -849,6 +851,7 @@ describe('in JS config files', () => { @tailwind utilities; } `, + '/root', { loadConfig: async () => ({ theme: { diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index fb226708e689..76eb1b8aa6c1 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1428,6 +1428,7 @@ describe('Parsing themes values from CSS', () => { @plugin "my-plugin"; @tailwind utilities; `, + '/root', { loadPlugin: async () => { return plugin(({}) => {}, { @@ -1471,6 +1472,7 @@ describe('Parsing themes values from CSS', () => { @config "./my-config.js"; @tailwind utilities; `, + '/root', { loadConfig: async () => { return { @@ -1510,6 +1512,7 @@ describe('plugins', () => { css` @plugin; `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1526,6 +1529,7 @@ describe('plugins', () => { css` @plugin ''; `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1544,6 +1548,7 @@ describe('plugins', () => { @plugin "my-plugin"; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1564,6 +1569,7 @@ describe('plugins', () => { color: red; } `, + '/root', { loadPlugin: async () => { return plugin.withOptions((options) => { @@ -1615,6 +1621,7 @@ describe('plugins', () => { is-arr-mixed: null, true, false, 1234567, 1.35, foo, 'bar', 'true'; } `, + '/root', { loadPlugin: async () => { return plugin.withOptions((options) => { @@ -1654,6 +1661,7 @@ describe('plugins', () => { } } `, + '/root', { loadPlugin: async () => { return plugin.withOptions((options) => { @@ -1691,6 +1699,7 @@ describe('plugins', () => { color: red; } `, + '/root', { loadPlugin: async () => { return plugin(({ addUtilities }) => { @@ -1716,6 +1725,7 @@ describe('plugins', () => { --color: [ 'red', 'green', 'blue']; } `, + '/root', { loadPlugin: async () => plugin(() => {}), }, @@ -1737,6 +1747,7 @@ describe('plugins', () => { }; } `, + '/root', { loadPlugin: async () => plugin(() => {}), }, @@ -1762,6 +1773,7 @@ describe('plugins', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1793,6 +1805,7 @@ describe('plugins', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1825,6 +1838,7 @@ describe('plugins', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1859,6 +1873,7 @@ describe('plugins', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1907,6 +1922,7 @@ describe('plugins', () => { @tailwind utilities; } `, + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1944,7 +1960,7 @@ describe('plugins', () => { @tailwind utilities; } `, - + '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1981,18 +1997,24 @@ describe('plugins', () => { describe('@source', () => { test('emits @source files', async () => { - let { globs } = await compile(css` - @source "./foo/bar/*.ts"; - `) + let { globs } = await compile( + css` + @source "./foo/bar/*.ts"; + `, + '/root', + ) expect(globs).toEqual([{ pattern: './foo/bar/*.ts' }]) }) test('emits multiple @source files', async () => { - let { globs } = await compile(css` - @source "./foo/**/*.ts"; - @source "./php/secr3t/smarty.php"; - `) + let { globs } = await compile( + css` + @source "./foo/**/*.ts"; + @source "./php/secr3t/smarty.php"; + `, + '/root', + ) expect(globs).toEqual([{ pattern: './foo/**/*.ts' }, { pattern: './php/secr3t/smarty.php' }]) }) @@ -2042,13 +2064,16 @@ describe('@variant', () => { describe('body-less syntax', () => { test('selector variant', async () => { - let { build } = await compile(css` - @variant hocus (&:hover, &:focus); + let { build } = await compile( + css` + @variant hocus (&:hover, &:focus); - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['hocus:underline', 'group-hocus:flex']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2065,13 +2090,16 @@ describe('@variant', () => { }) test('at-rule variant', async () => { - let { build } = await compile(css` - @variant any-hover (@media (any-hover: hover)); + let { build } = await compile( + css` + @variant any-hover (@media (any-hover: hover)); - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['any-hover:hover:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2088,17 +2116,20 @@ describe('@variant', () => { describe('body with @slot syntax', () => { test('selector with @slot', async () => { - let { build } = await compile(css` - @variant selected { - &[data-selected] { - @slot; + let { build } = await compile( + css` + @variant selected { + &[data-selected] { + @slot; + } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['selected:underline', 'group-selected:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2115,18 +2146,21 @@ describe('@variant', () => { }) test('grouped selectors with @slot', async () => { - let { build } = await compile(css` - @variant hocus { - &:hover, - &:focus { - @slot; + let { build } = await compile( + css` + @variant hocus { + &:hover, + &:focus { + @slot; + } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2143,21 +2177,24 @@ describe('@variant', () => { }) test('multiple selectors with @slot', async () => { - let { build } = await compile(css` - @variant hocus { - &:hover { - @slot; - } + let { build } = await compile( + css` + @variant hocus { + &:hover { + @slot; + } - &:focus { - @slot; + &:focus { + @slot; + } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2174,20 +2211,23 @@ describe('@variant', () => { }) test('nested selector with @slot', async () => { - let { build } = await compile(css` - @variant custom-before { - & { - --has-before: 1; - &::before { - @slot; + let { build } = await compile( + css` + @variant custom-before { + & { + --has-before: 1; + &::before { + @slot; + } } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['custom-before:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2204,23 +2244,26 @@ describe('@variant', () => { }) test('grouped nested selectors with @slot', async () => { - let { build } = await compile(css` - @variant custom-before { - & { - --has-before: 1; - &::before { - &:hover, - &:focus { - @slot; + let { build } = await compile( + css` + @variant custom-before { + & { + --has-before: 1; + &::before { + &:hover, + &:focus { + @slot; + } } } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['custom-before:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2237,23 +2280,26 @@ describe('@variant', () => { }) test('nested multiple selectors with @slot', async () => { - let { build } = await compile(css` - @variant hocus { - &:hover { - @media (hover: hover) { + let { build } = await compile( + css` + @variant hocus { + &:hover { + @media (hover: hover) { + @slot; + } + } + + &:focus { @slot; } } - &:focus { - @slot; + @layer utilities { + @tailwind utilities; } - } - - @layer utilities { - @tailwind utilities; - } - `) + `, + '/root', + ) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2282,19 +2328,22 @@ describe('@variant', () => { }) test('selector nested under at-rule with @slot', async () => { - let { build } = await compile(css` - @variant hocus { - @media (hover: hover) { - &:hover { - @slot; + let { build } = await compile( + css` + @variant hocus { + @media (hover: hover) { + &:hover { + @slot; + } } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2315,17 +2364,20 @@ describe('@variant', () => { }) test('at-rule with @slot', async () => { - let { build } = await compile(css` - @variant any-hover { - @media (any-hover: hover) { - @slot; + let { build } = await compile( + css` + @variant any-hover { + @media (any-hover: hover) { + @slot; + } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['any-hover:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2340,21 +2392,24 @@ describe('@variant', () => { }) test('multiple at-rules with @slot', async () => { - let { build } = await compile(css` - @variant desktop { - @media (any-hover: hover) { - @slot; - } + let { build } = await compile( + css` + @variant desktop { + @media (any-hover: hover) { + @slot; + } - @media (pointer: fine) { - @slot; + @media (pointer: fine) { + @slot; + } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['desktop:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2375,23 +2430,26 @@ describe('@variant', () => { }) test('nested at-rules with @slot', async () => { - let { build } = await compile(css` - @variant custom-variant { - @media (orientation: landscape) { - @media screen { - @slot; - } + let { build } = await compile( + css` + @variant custom-variant { + @media (orientation: landscape) { + @media screen { + @slot; + } - @media print { - display: none; + @media print { + display: none; + } } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['custom-variant:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2414,20 +2472,23 @@ describe('@variant', () => { }) test('at-rule and selector with @slot', async () => { - let { build } = await compile(css` - @variant custom-dark { - @media (prefers-color-scheme: dark) { - @slot; - } - &:is(.dark *) { - @slot; + let { build } = await compile( + css` + @variant custom-dark { + @media (prefers-color-scheme: dark) { + @slot; + } + &:is(.dark *) { + @slot; + } } - } - @layer utilities { - @tailwind utilities; - } - `) + @layer utilities { + @tailwind utilities; + } + `, + '/root', + ) let compiled = build(['custom-dark:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2513,7 +2574,7 @@ test('addBase', async () => { @tailwind utilities; } `, - + '/root', { loadPlugin: async () => { return ({ addBase }: PluginAPI) => { diff --git a/packages/tailwindcss/src/plugin.test.ts b/packages/tailwindcss/src/plugin.test.ts index 5af7ff1d4599..29780e4f008b 100644 --- a/packages/tailwindcss/src/plugin.test.ts +++ b/packages/tailwindcss/src/plugin.test.ts @@ -9,7 +9,7 @@ test('plugin', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin(function ({ addBase }) { addBase({ @@ -36,7 +36,7 @@ test('plugin.withOptions', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, { + let compiler = await compile(input, '/root', { loadPlugin: async () => { return plugin.withOptions(function (opts = { foo: '1px' }) { return function ({ addBase }) { diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index 5af54b592677..fb81a5b2e94e 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -2,12 +2,12 @@ import { Features, transform } from 'lightningcss' import { compile } from '..' export async function compileCss(css: string, candidates: string[] = []) { - let { build } = await compile(css, '') + let { build } = await compile(css, '/root') return optimizeCss(build(candidates)).trim() } export async function run(candidates: string[]) { - let { build } = await compile('@tailwind utilities;', '') + let { build } = await compile('@tailwind utilities;', '/root') return optimizeCss(build(candidates)).trim() } From 89f9db6aaed077a3c6bdf1d222e2e98bf8e11026 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 15:24:40 +0200 Subject: [PATCH 06/51] Add context node to AST --- packages/tailwindcss/src/ast.test.ts | 53 +++++++++++++++++++++++++++- packages/tailwindcss/src/ast.ts | 37 +++++++++++++++++-- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index da0b19204375..21915cba4a4c 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,5 +1,5 @@ import { expect, it } from 'vitest' -import { toCss } from './ast' +import { context, decl, rule, toCss, walk } from './ast' import * as CSS from './css-parser' it('should pretty print an AST', () => { @@ -13,3 +13,54 @@ it('should pretty print an AST', () => { " `) }) + +it('allows the placement of context nodes', () => { + const ast = [ + rule('.foo', [decl('color', 'red')]), + context({ context: 'a' }, [ + rule('.bar', [ + decl('color', 'blue'), + context({ context: 'b' }, [ + // + rule('.baz', [decl('color', 'green')]), + ]), + ]), + ]), + ] + + let redContext + let blueContext + let greenContext + + walk(ast, (node, { context }) => { + if (node.kind !== 'declaration') return + switch (node.value) { + case 'red': + redContext = context + break + case 'blue': + blueContext = context + break + case 'green': + greenContext = context + break + } + }) + + expect(redContext).toEqual({}) + expect(blueContext).toEqual({ context: 'a' }) + expect(greenContext).toEqual({ context: 'b' }) + + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { + color: red; + } + .bar { + color: blue; + .baz { + color: green; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index fa5dc6f1b277..72c969ac3dfb 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -16,7 +16,14 @@ export type Comment = { value: string } -export type AstNode = Rule | Declaration | Comment +export type Context = { + kind: 'context' + context: Record + nodes: AstNode[] +} + +export type AstNode = Rule | Declaration | Comment | Context +type VisitableAstNode = Exclude export function rule(selector: string, nodes: AstNode[]): Rule { return { @@ -42,6 +49,14 @@ export function comment(value: string): Comment { } } +export function context(context: Record, nodes: AstNode[]): Context { + return { + kind: 'context', + context, + nodes, + } +} + export enum WalkAction { /** Continue walking, which is the default */ Continue, @@ -59,13 +74,21 @@ export function walk( node: AstNode, utils: { parent: AstNode | null - replaceWith(newNode: AstNode | AstNode[]): void + replaceWith(newNode: VisitableAstNode | VisitableAstNode[]): void + context: Record }, ) => void | WalkAction, parent: AstNode | null = null, + context: Record = {}, ) { for (let i = 0; i < ast.length; i++) { let node = ast[i] + + if (node.kind === 'context') { + walk(node.nodes, visit, node, { ...context, ...node.context }) + continue + } + let status = visit(node, { parent, @@ -76,6 +99,7 @@ export function walk( // will process this position (containing the replaced node) again. i-- }, + context, }) ?? WalkAction.Continue // Stop the walk entirely @@ -85,7 +109,7 @@ export function walk( if (status === WalkAction.Skip) continue if (node.kind === 'rule') { - walk(node.nodes, visit, node) + walk(node.nodes, visit, node, context) } } } @@ -171,6 +195,13 @@ export function toCss(ast: AstNode[]) { css += `${indent}/*${node.value}*/\n` } + // Context Node + else if (node.kind === 'context') { + for (let child of node.nodes) { + css += stringify(child, depth) + } + } + // Declaration else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) { css += `${indent}${node.property}: ${node.value}${node.important ? '!important' : ''};\n` From 3f065c1cc220baa1abb9ee48f7b1545909d33a8b Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 15:44:50 +0200 Subject: [PATCH 07/51] Make resolvers return a new base --- packages/@tailwindcss-node/src/compile.ts | 43 +++++++------------ packages/tailwindcss/src/at-import.test.ts | 24 +++++------ packages/tailwindcss/src/at-import.ts | 14 +++--- .../src/compat/apply-compat-hooks.ts | 31 ++++++------- packages/tailwindcss/src/index.ts | 27 +++++------- 5 files changed, 61 insertions(+), 78 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 97ff1b93bd97..e8e5d62c8907 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' import fs from 'node:fs' import fsPromises from 'node:fs/promises' -import path from 'node:path' +import path, { basename } from 'node:path' import { pathToFileURL } from 'node:url' import { compile as _compile } from 'tailwindcss' import { getModuleDependencies } from './get-module-dependencies' @@ -12,12 +12,16 @@ export async function compile( { base, onDependency }: { base: string; onDependency: (path: string) => void }, ) { return await _compile(css, base, { - loadPlugin: async (pluginPath) => { - if (pluginPath[0] !== '.') { - return importModule(pluginPath).then((m) => m.default ?? m) + async resolveModule(id, base) { + if (id[0] !== '.') { + let resolvedPath = path.resolve(base, id) + return { + base: basename(resolvedPath), + module: importModule(pathToFileURL(resolvedPath).href).then((m) => m.default ?? m), + } } - let resolvedPath = path.resolve(base, pluginPath) + let resolvedPath = path.resolve(base, id) let [module, moduleDependencies] = await Promise.all([ importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), getModuleDependencies(resolvedPath), @@ -27,34 +31,19 @@ export async function compile( for (let file of moduleDependencies) { onDependency(file) } - return module.default ?? module - }, - - loadConfig: async (configPath) => { - if (configPath[0] !== '.') { - return importModule(configPath).then((m) => m.default ?? m) - } - - let resolvedPath = path.resolve(base, configPath) - let [module, moduleDependencies] = await Promise.all([ - importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), - getModuleDependencies(resolvedPath), - ]) - - onDependency(resolvedPath) - for (let file of moduleDependencies) { - onDependency(file) + return { + base: basename(resolvedPath), + module: module.default ?? module, } - return module.default ?? module }, async resolveImport(id, basedir) { - let full = await resolveCssId(id, basedir) - if (!full) throw new Error(`Could not resolve '${id}' from '${basedir}'`) - let file = await fsPromises.readFile(full, 'utf-8') + let resolvedPath = await resolveCssId(id, basedir) + if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${basedir}'`) + let file = await fsPromises.readFile(resolvedPath, 'utf-8') return { + base: path.dirname(resolvedPath), content: file, - basedir: path.dirname(full), } }, }) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 085c542e9ba5..5a159d4b8190 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -6,7 +6,7 @@ let css = String.raw async function run( css: string, - resolveImport: (id: string, basedir: string) => Promise<{ content: string; basedir: string }>, + resolveImport: (id: string, base: string) => Promise<{ content: string; base: string }>, candidates: string[] = [], ) { let compiler = await compile(css, '/root', { resolveImport }) @@ -14,8 +14,8 @@ async function run( } test('can resolve relative @imports', async () => { - let resolver = async (id: string, basedir: string) => { - expect(basedir).toBe('/root') + let resolver = async (id: string, base: string) => { + expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { content: css` @@ -23,7 +23,7 @@ test('can resolve relative @imports', async () => { color: red; } `, - basedir: '/root/foo', + base: '/root/foo', } } @@ -43,22 +43,22 @@ test('can resolve relative @imports', async () => { }) test('can recursively resolve relative @imports', async () => { - let resolver = async (id: string, basedir: string) => { - if (basedir === '/root' && id === './foo/bar.css') { + let resolver = async (id: string, base: string) => { + if (base === '/root' && id === './foo/bar.css') { return { content: css` @import './bar/baz.css'; `, - basedir: '/root/foo', + base: '/root/foo', } - } else if (basedir === '/root/foo' && id === './bar/baz.css') { + } else if (base === '/root/foo' && id === './bar/baz.css') { return { content: css` .baz { color: blue; } `, - basedir: '/root/foo/bar', + base: '/root/foo/bar', } } @@ -85,10 +85,10 @@ let exampleCSS = css` color: red; } ` -let resolver = async (id: string) => { +let resolver = async () => { return { content: exampleCSS, - basedir: '/root', + base: '/root', } } @@ -214,7 +214,7 @@ test('supports theme(reference) imports', async () => { --color-red-500: red; } `, - basedir: '', + base: '', }), ['text-red-500'], ), diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index f3b7ff443c4e..581a34aaa1d5 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -2,11 +2,11 @@ import { rule, walk, WalkAction, type AstNode } from './ast' import * as CSS from './css-parser' import * as ValueParser from './value-parser' -type ResolveImport = (id: string, basedir: string) => Promise<{ content: string; basedir: string }> +type ResolveImport = (id: string, basedir: string) => Promise<{ base: string; content: string }> export async function substituteAtImports( ast: AstNode[], - basedir: string, + base: string, resolveImport: ResolveImport, ) { let promises: Map> = new Map() @@ -19,7 +19,7 @@ export async function substituteAtImports( // Skip importing data URIs if (uri.startsWith('data:')) return - promises.set(key(uri, basedir), resolveAtImport(uri, basedir, resolveImport)) + promises.set(key(uri, base), resolveAtImport(uri, base, resolveImport)) } }) @@ -34,7 +34,7 @@ export async function substituteAtImports( let { uri, layer, media, supports } = parseImportParams( ValueParser.parse(node.selector.slice(8)), ) - let importedAst = unwrapped.get(key(uri, basedir)) + let importedAst = unwrapped.get(key(uri, base)) if (importedAst) { replaceWith(buildImportNodes(importedAst, layer, media, supports)) return WalkAction.Skip @@ -45,12 +45,12 @@ export async function substituteAtImports( async function resolveAtImport( id: string, - basedir: string, + base: string, resolveImport: ResolveImport, ): Promise { - const { content, basedir: nestedBaseDir } = await resolveImport(id, basedir) + const { content, base: nestedBase } = await resolveImport(id, base) let ast = CSS.parse(content) - await substituteAtImports(ast, nestedBaseDir, resolveImport) + await substituteAtImports(ast, nestedBase, resolveImport) return ast } diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 686b37dc8f6b..dfed85781219 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -16,20 +16,18 @@ import { registerThemeVariantOverrides } from './theme-variants' export async function applyCompatibilityHooks({ designSystem, ast, - loadPlugin, - loadConfig, + resolveModule, globs, }: { designSystem: DesignSystem ast: AstNode[] - loadPlugin: (path: string) => Promise - loadConfig: (path: string) => Promise + resolveModule: (path: string, base: string) => Promise<{ module: any; base: string }> globs: { origin?: string; pattern: string }[] }) { - let pluginPaths: [string, CssPluginOptions | null][] = [] - let configPaths: string[] = [] + let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = [] + let configPaths: { id: string; base: string }[] = [] - walk(ast, (node, { parent, replaceWith }) => { + walk(ast, (node, { parent, replaceWith, context }) => { if (node.kind !== 'rule' || node.selector[0] !== '@') return // Collect paths from `@plugin` at-rules @@ -86,7 +84,10 @@ export async function applyCompatibilityHooks({ options[decl.property] = parts.length === 1 ? parts[0] : parts } - pluginPaths.push([pluginPath, Object.keys(options).length > 0 ? options : null]) + pluginPaths.push([ + { id: pluginPath, base: (context as any).base }, + Object.keys(options).length > 0 ? options : null, + ]) replaceWith([]) return @@ -102,7 +103,7 @@ export async function applyCompatibilityHooks({ throw new Error('`@config` cannot be nested.') } - configPaths.push(node.selector.slice(9, -1)) + configPaths.push({ id: node.selector.slice(9, -1), base: (context as any).base }) replaceWith([]) return } @@ -143,15 +144,15 @@ export async function applyCompatibilityHooks({ if (!pluginPaths.length && !configPaths.length) return let configs = await Promise.all( - configPaths.map(async (configPath) => ({ - path: configPath, - config: await loadConfig(configPath), + configPaths.map(async ({ id, base }) => ({ + path: id, + config: (await resolveModule(id, base)).module as UserConfig, })), ) let pluginDetails = await Promise.all( - pluginPaths.map(async ([pluginPath, pluginOptions]) => ({ - path: pluginPath, - plugin: await loadPlugin(pluginPath), + pluginPaths.map(async ([{ id, base }, pluginOptions]) => ({ + path: id, + plugin: (await resolveModule(id, base)).module as Plugin, options: pluginOptions, })), ) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 0d4341b5b44c..f9b7b42d445b 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -4,7 +4,6 @@ import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' -import { type Plugin } from './compat/plugin-api' import { compileCandidates } from './compile' import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions' import * as CSS from './css-parser' @@ -16,17 +15,12 @@ export type Config = UserConfig const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { - loadPlugin?: (path: string) => Promise - loadConfig?: (path: string) => Promise - resolveImport?: (id: string, basedir: string) => Promise<{ content: string; basedir: string }> + resolveModule?: (id: string, base: string) => Promise<{ base: string; module: unknown }> + resolveImport?: (id: string, base: string) => Promise<{ content: string; base: string }> } -function throwOnPlugin(): never { - throw new Error('No `loadPlugin` function provided to `compile`') -} - -function throwOnConfig(): never { - throw new Error('No `loadConfig` function provided to `compile`') +function throwOnResolveModule(): never { + throw new Error('No `resolveModule` function provided to `compile`') } function throwOnResolveImport(): never { @@ -51,17 +45,16 @@ function parseThemeOptions(selector: string) { async function parseCss( css: string, - basedir: string, + base: string, { - loadPlugin = throwOnPlugin, - loadConfig = throwOnConfig, + resolveModule = throwOnResolveModule, resolveImport = throwOnResolveImport, }: CompileOptions = {}, ) { let ast = CSS.parse(css) if (css.includes('@import')) { - await substituteAtImports(ast, basedir, resolveImport) + await substituteAtImports(ast, base, resolveImport) } // Find all `@theme` declarations @@ -249,7 +242,7 @@ async function parseCss( // 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 // compatible without polluting core. - await applyCompatibilityHooks({ designSystem, ast, loadPlugin, loadConfig, globs }) + await applyCompatibilityHooks({ designSystem, ast, resolveModule, globs }) for (let customVariant of customVariants) { customVariant(designSystem) @@ -329,13 +322,13 @@ async function parseCss( export async function compile( css: string, - basedir: string, + base: string, opts: CompileOptions = {}, ): Promise<{ globs: { origin?: string; pattern: string }[] build(candidates: string[]): string }> { - let { designSystem, ast, globs } = await parseCss(css, basedir, opts) + let { designSystem, ast, globs } = await parseCss(css, base, opts) let tailwindUtilitiesNode: Rule | null = null From ed5070e50a06460b32ef2d667f13d48be435a0cc Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 18 Sep 2024 16:03:47 +0200 Subject: [PATCH 08/51] Handle relative paths in imported files --- packages/tailwindcss/src/ast.ts | 10 ++--- packages/tailwindcss/src/at-import.test.ts | 38 ++++++++++++++++++- packages/tailwindcss/src/at-import.ts | 17 +++++---- .../src/compat/apply-compat-hooks.ts | 4 +- packages/tailwindcss/src/index.ts | 4 +- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 72c969ac3dfb..4ac91b23dcc9 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -23,7 +23,6 @@ export type Context = { } export type AstNode = Rule | Declaration | Comment | Context -type VisitableAstNode = Exclude export function rule(selector: string, nodes: AstNode[]): Rule { return { @@ -74,7 +73,7 @@ export function walk( node: AstNode, utils: { parent: AstNode | null - replaceWith(newNode: VisitableAstNode | VisitableAstNode[]): void + replaceWith(newNode: AstNode | AstNode[]): void context: Record }, ) => void | WalkAction, @@ -84,11 +83,6 @@ export function walk( for (let i = 0; i < ast.length; i++) { let node = ast[i] - if (node.kind === 'context') { - walk(node.nodes, visit, node, { ...context, ...node.context }) - continue - } - let status = visit(node, { parent, @@ -110,6 +104,8 @@ export function walk( if (node.kind === 'rule') { walk(node.nodes, visit, node, context) + } else if (node.kind === 'context') { + walk(node.nodes, visit, node, { ...context, ...node.context }) } } } diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 5a159d4b8190..3b544109fa6d 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { expect, test, vi } from 'vitest' import { compile } from './index' import { optimizeCss } from './test-utils/run' @@ -7,9 +7,11 @@ let css = String.raw async function run( css: string, resolveImport: (id: string, base: string) => Promise<{ content: string; base: string }>, + resolveModule: (id: string, base: string) => Promise<{ module: unknown; base: string }> = () => + Promise.reject(new Error('Unexpected module')), candidates: string[] = [], ) { - let compiler = await compile(css, '/root', { resolveImport }) + let compiler = await compile(css, '/root', { resolveImport, resolveModule }) return optimizeCss(compiler.build(candidates)) } @@ -216,6 +218,7 @@ test('supports theme(reference) imports', async () => { `, base: '', }), + () => Promise.reject(new Error('Unexpected module')), ['text-red-500'], ), ).resolves.toBe( @@ -226,3 +229,34 @@ test('supports theme(reference) imports', async () => { `), ) }) + +test('updates the base when loading modules inside nested files', async () => { + let resolveImport = () => + Promise.resolve({ + content: css` + @config './nested-config.js'; + @plugin './nested-plugin.js'; + `, + base: '/root/foo', + }) + let resolveModule = vi.fn().mockResolvedValue({ base: '', module: () => {} }) + + expect( + ( + await run( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + resolveImport, + resolveModule, + ) + ).trim(), + ).toBe('') + + expect(resolveModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo') + expect(resolveModule).toHaveBeenNthCalledWith(2, './root-config.js', '/root') + expect(resolveModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo') + expect(resolveModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root') +}) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 581a34aaa1d5..f1fe52bac0c5 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -1,4 +1,4 @@ -import { rule, walk, WalkAction, type AstNode } from './ast' +import { context, rule, walk, WalkAction, type AstNode } from './ast' import * as CSS from './css-parser' import * as ValueParser from './value-parser' @@ -9,7 +9,7 @@ export async function substituteAtImports( base: string, resolveImport: ResolveImport, ) { - let promises: Map> = new Map() + let promises: Map> = new Map() walk(ast, (node) => { // Find @import rules and start resolving them @@ -34,9 +34,9 @@ export async function substituteAtImports( let { uri, layer, media, supports } = parseImportParams( ValueParser.parse(node.selector.slice(8)), ) - let importedAst = unwrapped.get(key(uri, base)) - if (importedAst) { - replaceWith(buildImportNodes(importedAst, layer, media, supports)) + let imported = unwrapped.get(key(uri, base)) + if (imported) { + replaceWith(buildImportNodes(imported.ast, imported.base, layer, media, supports)) return WalkAction.Skip } } @@ -47,11 +47,11 @@ async function resolveAtImport( id: string, base: string, resolveImport: ResolveImport, -): Promise { +): Promise<{ ast: AstNode[]; base: string }> { const { content, base: nestedBase } = await resolveImport(id, base) let ast = CSS.parse(content) await substituteAtImports(ast, nestedBase, resolveImport) - return ast + return { ast, base: nestedBase } } function key(id: string, basedir: string): string { @@ -120,6 +120,7 @@ function parseImportParams(params: ValueParser.ValueAstNode[]) { function buildImportNodes( importedAst: AstNode[], + base: string, layer: string | null, media: string | null, supports: string | null, @@ -138,5 +139,5 @@ function buildImportNodes( root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] } - return root + return [context({ base }, root)] } diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index dfed85781219..0fb304e22e6d 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -32,7 +32,7 @@ export async function applyCompatibilityHooks({ // Collect paths from `@plugin` at-rules if (node.selector === '@plugin' || node.selector.startsWith('@plugin ')) { - if (parent !== null) { + if (parent !== null && parent.kind !== 'context') { throw new Error('`@plugin` cannot be nested.') } @@ -99,7 +99,7 @@ export async function applyCompatibilityHooks({ throw new Error('`@config` cannot have a body.') } - if (parent !== null) { + if (parent !== null && parent.kind !== 'context') { throw new Error('`@config` cannot be nested.') } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f9b7b42d445b..313d80ad69cb 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,6 +1,6 @@ import { version } from '../package.json' import { substituteAtApply } from './apply' -import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' +import { comment, context, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' @@ -51,7 +51,7 @@ async function parseCss( resolveImport = throwOnResolveImport, }: CompileOptions = {}, ) { - let ast = CSS.parse(css) + let ast = [context({ base }, CSS.parse(css))] if (css.includes('@import')) { await substituteAtImports(ast, base, resolveImport) From 75e888b4a29fabae64c9d8123e13f4a70c15c116 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 12:31:12 +0200 Subject: [PATCH 09/51] Reanem API to loadModule/loadStylesheet --- packages/@tailwindcss-node/src/compile.ts | 4 ++-- packages/tailwindcss/src/at-import.test.ts | 22 +++++++++---------- packages/tailwindcss/src/at-import.ts | 12 +++++----- .../src/compat/apply-compat-hooks.ts | 8 +++---- packages/tailwindcss/src/index.ts | 21 ++++++++---------- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index e8e5d62c8907..1e13de8ed2f6 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -12,7 +12,7 @@ export async function compile( { base, onDependency }: { base: string; onDependency: (path: string) => void }, ) { return await _compile(css, base, { - async resolveModule(id, base) { + async loadModule(id, base) { if (id[0] !== '.') { let resolvedPath = path.resolve(base, id) return { @@ -37,7 +37,7 @@ export async function compile( } }, - async resolveImport(id, basedir) { + async loadStylesheet(id, basedir) { let resolvedPath = await resolveCssId(id, basedir) if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${basedir}'`) let file = await fsPromises.readFile(resolvedPath, 'utf-8') diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 3b544109fa6d..1cdb37f2dea5 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -6,12 +6,12 @@ let css = String.raw async function run( css: string, - resolveImport: (id: string, base: string) => Promise<{ content: string; base: string }>, - resolveModule: (id: string, base: string) => Promise<{ module: unknown; base: string }> = () => + loadStylesheet: (id: string, base: string) => Promise<{ content: string; base: string }>, + loadModule: (id: string, base: string) => Promise<{ module: unknown; base: string }> = () => Promise.reject(new Error('Unexpected module')), candidates: string[] = [], ) { - let compiler = await compile(css, '/root', { resolveImport, resolveModule }) + let compiler = await compile(css, '/root', { loadStylesheet, loadModule }) return optimizeCss(compiler.build(candidates)) } @@ -231,7 +231,7 @@ test('supports theme(reference) imports', async () => { }) test('updates the base when loading modules inside nested files', async () => { - let resolveImport = () => + let loadStylesheet = () => Promise.resolve({ content: css` @config './nested-config.js'; @@ -239,7 +239,7 @@ test('updates the base when loading modules inside nested files', async () => { `, base: '/root/foo', }) - let resolveModule = vi.fn().mockResolvedValue({ base: '', module: () => {} }) + let loadModule = vi.fn().mockResolvedValue({ base: '', module: () => {} }) expect( ( @@ -249,14 +249,14 @@ test('updates the base when loading modules inside nested files', async () => { @config './root-config.js'; @plugin './root-plugin.js'; `, - resolveImport, - resolveModule, + loadStylesheet, + loadModule, ) ).trim(), ).toBe('') - expect(resolveModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo') - expect(resolveModule).toHaveBeenNthCalledWith(2, './root-config.js', '/root') - expect(resolveModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo') - expect(resolveModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root') + expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo') + expect(loadModule).toHaveBeenNthCalledWith(2, './root-config.js', '/root') + expect(loadModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo') + expect(loadModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root') }) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index f1fe52bac0c5..8ee181d15b76 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -2,12 +2,12 @@ import { context, rule, walk, WalkAction, type AstNode } from './ast' import * as CSS from './css-parser' import * as ValueParser from './value-parser' -type ResolveImport = (id: string, basedir: string) => Promise<{ base: string; content: string }> +type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }> export async function substituteAtImports( ast: AstNode[], base: string, - resolveImport: ResolveImport, + loadStylesheet: LoadStylesheet, ) { let promises: Map> = new Map() @@ -19,7 +19,7 @@ export async function substituteAtImports( // Skip importing data URIs if (uri.startsWith('data:')) return - promises.set(key(uri, base), resolveAtImport(uri, base, resolveImport)) + promises.set(key(uri, base), resolveAtImport(uri, base, loadStylesheet)) } }) @@ -46,11 +46,11 @@ export async function substituteAtImports( async function resolveAtImport( id: string, base: string, - resolveImport: ResolveImport, + loadStylesheet: LoadStylesheet, ): Promise<{ ast: AstNode[]; base: string }> { - const { content, base: nestedBase } = await resolveImport(id, base) + const { content, base: nestedBase } = await loadStylesheet(id, base) let ast = CSS.parse(content) - await substituteAtImports(ast, nestedBase, resolveImport) + await substituteAtImports(ast, nestedBase, loadStylesheet) return { ast, base: nestedBase } } diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 0fb304e22e6d..0f2960c99f60 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -16,12 +16,12 @@ import { registerThemeVariantOverrides } from './theme-variants' export async function applyCompatibilityHooks({ designSystem, ast, - resolveModule, + loadModule, globs, }: { designSystem: DesignSystem ast: AstNode[] - resolveModule: (path: string, base: string) => Promise<{ module: any; base: string }> + loadModule: (path: string, base: string) => Promise<{ module: any; base: string }> globs: { origin?: string; pattern: string }[] }) { let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = [] @@ -146,13 +146,13 @@ export async function applyCompatibilityHooks({ let configs = await Promise.all( configPaths.map(async ({ id, base }) => ({ path: id, - config: (await resolveModule(id, base)).module as UserConfig, + config: (await loadModule(id, base)).module as UserConfig, })), ) let pluginDetails = await Promise.all( pluginPaths.map(async ([{ id, base }, pluginOptions]) => ({ path: id, - plugin: (await resolveModule(id, base)).module as Plugin, + plugin: (await loadModule(id, base)).module as Plugin, options: pluginOptions, })), ) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 313d80ad69cb..e7ea67de2fce 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -15,16 +15,16 @@ export type Config = UserConfig const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { - resolveModule?: (id: string, base: string) => Promise<{ base: string; module: unknown }> - resolveImport?: (id: string, base: string) => Promise<{ content: string; base: string }> + loadModule?: (id: string, base: string) => Promise<{ module: unknown; base: string }> + loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> } -function throwOnResolveModule(): never { - throw new Error('No `resolveModule` function provided to `compile`') +function throwOnLoadModule(): never { + throw new Error('No `loadModule` function provided to `compile`') } -function throwOnResolveImport(): never { - throw new Error('No `resolveImport` function provided to `compile`') +function throwOnLoadStylesheet(): never { + throw new Error('No `loadStylesheet` function provided to `compile`') } function parseThemeOptions(selector: string) { @@ -46,15 +46,12 @@ function parseThemeOptions(selector: string) { async function parseCss( css: string, base: string, - { - resolveModule = throwOnResolveModule, - resolveImport = throwOnResolveImport, - }: CompileOptions = {}, + { loadModule = throwOnLoadModule, loadStylesheet = throwOnLoadStylesheet }: CompileOptions = {}, ) { let ast = [context({ base }, CSS.parse(css))] if (css.includes('@import')) { - await substituteAtImports(ast, base, resolveImport) + await substituteAtImports(ast, base, loadStylesheet) } // Find all `@theme` declarations @@ -242,7 +239,7 @@ async function parseCss( // 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 // compatible without polluting core. - await applyCompatibilityHooks({ designSystem, ast, resolveModule, globs }) + await applyCompatibilityHooks({ designSystem, ast, loadModule, globs }) for (let customVariant of customVariants) { customVariant(designSystem) From 9853d474ae19d8b68ded08ca7ec92ab910c7f835 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 13:23:02 +0200 Subject: [PATCH 10/51] Fix config resolver to keep track of proper bases --- packages/tailwindcss/src/at-import.test.ts | 83 ++- .../src/compat/apply-compat-hooks.ts | 50 +- .../tailwindcss/src/compat/config.test.ts | 473 ++++++++++-------- .../src/compat/config/resolve-config.test.ts | 9 + .../src/compat/config/resolve-config.ts | 9 +- packages/tailwindcss/src/index.ts | 24 +- 6 files changed, 418 insertions(+), 230 deletions(-) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 1cdb37f2dea5..7754b2239791 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,5 +1,6 @@ import { expect, test, vi } from 'vitest' -import { compile } from './index' +import { compile, type Config } from './index' +import plugin from './plugin' import { optimizeCss } from './test-utils/run' let css = String.raw @@ -260,3 +261,83 @@ test('updates the base when loading modules inside nested files', async () => { expect(loadModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo') expect(loadModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root') }) + +test('emits the right base for @source directives inside nested files', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @source './nested/**/*.css'; + `, + base: '/root/foo', + }) + + let compiler = await compile( + css` + @import './foo/bar.css'; + @source './root/**/*.css'; + `, + '/root', + { loadStylesheet }, + ) + + expect(compiler.globs).toEqual([ + // + { pattern: './nested/**/*.css', base: '/root/foo' }, + { pattern: './root/**/*.css', base: '/root' }, + ]) +}) + +test('emits the right base for @source found inside JS configs and plugins from nested imports', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @config './nested-config.js'; + @plugin './nested-plugin.js'; + `, + base: '/root/foo', + }) + let loadModule = vi.fn().mockImplementation((id: string) => { + let base = id.includes('nested') ? '/root/foo' : '/root' + if (id.includes('config')) { + let glob = id.includes('nested') ? './nested-config/*.html' : './root-config/*.html' + let pluginGlob = id.includes('nested') + ? './nested-config-plugin/*.html' + : './root-config-plugin/*.html' + return { + module: { + content: [glob], + plugins: [plugin(() => {}, { content: [pluginGlob] })], + } satisfies Config, + base: base + '-config', + } + } else { + let glob = id.includes('nested') ? './nested-plugin/*.html' : './root-plugin/*.html' + return { + module: plugin(() => {}, { content: [glob] }), + base: base + '-plugin', + } + } + }) + + let compiler = await compile( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + '/root', + { loadStylesheet, loadModule }, + ) + + expect(compiler.globs).toEqual([ + // + { pattern: './nested-plugin/*.html', base: '/root/foo-plugin' }, + { pattern: './root-plugin/*.html', base: '/root-plugin' }, + + { pattern: './nested-config-plugin/*.html', base: '/root/foo-config' }, + { pattern: './nested-config/*.html', base: '/root/foo-config' }, + + { pattern: './root-config-plugin/*.html', base: '/root-config' }, + { pattern: './root-config/*.html', base: '/root-config' }, + ]) +}) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 0f2960c99f60..7eeb52900174 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -143,38 +143,48 @@ export async function applyCompatibilityHooks({ // any additional backwards compatibility hooks. if (!pluginPaths.length && !configPaths.length) return - let configs = await Promise.all( - configPaths.map(async ({ id, base }) => ({ - path: id, - config: (await loadModule(id, base)).module as UserConfig, - })), - ) - let pluginDetails = await Promise.all( - pluginPaths.map(async ([{ id, base }, pluginOptions]) => ({ - path: id, - plugin: (await loadModule(id, base)).module as Plugin, - options: pluginOptions, - })), - ) + let [configs, pluginDetails] = await Promise.all([ + Promise.all( + configPaths.map(async ({ id, base }) => { + let loaded = await loadModule(id, base) + return { + path: id, + base: loaded.base, + config: loaded.module as UserConfig, + } + }), + ), + Promise.all( + pluginPaths.map(async ([{ id, base }, pluginOptions]) => { + let loaded = await loadModule(id, base) + return { + path: id, + base: loaded.base, + plugin: loaded.module as Plugin, + options: pluginOptions, + } + }), + ), + ]) - let plugins = pluginDetails.map((detail) => { + let pluginConfigs = pluginDetails.map((detail) => { if (!detail.options) { - return detail.plugin + return { config: { plugins: [detail.plugin] }, base: detail.base } } if ('__isOptionsFunction' in detail.plugin) { - return detail.plugin(detail.options) + return { config: { plugins: [detail.plugin(detail.options)] }, base: detail.base } } throw new Error(`The plugin "${detail.path}" does not accept options`) }) - let userConfig = [{ config: { plugins } }, ...configs] + let userConfig = [...pluginConfigs, ...configs] let resolvedConfig = resolveConfig(designSystem, [ - { config: createCompatConfig(designSystem.theme) }, + { config: createCompatConfig(designSystem.theme), base: '' }, ...userConfig, - { config: { plugins: [darkModePlugin] } }, + { config: { plugins: [darkModePlugin] }, base: '' }, ]) let resolvedUserConfig = resolveConfig(designSystem, userConfig) @@ -222,7 +232,7 @@ export async function applyCompatibilityHooks({ ) } - globs.push({ origin: file.base, pattern: file.pattern }) + globs.push(file) } } diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index 6c984130e191..c90558a5537e 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { compile } from '..' +import { compile, type Config } from '..' import plugin from '../plugin' import { flattenColorPalette } from './flatten-color-palette' @@ -12,7 +12,7 @@ test('Config files can add content', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ content: ['./file.txt'] }), + loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }), }) expect(compiler.globs).toEqual([{ origin: './config.js', pattern: './file.txt' }]) @@ -25,7 +25,7 @@ test('Config files can change dark mode (media)', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ darkMode: 'media' }), + loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }), }) expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` @@ -45,7 +45,7 @@ test('Config files can change dark mode (selector)', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ darkMode: 'selector' }), + loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }), }) expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` @@ -65,7 +65,10 @@ test('Config files can change dark mode (variant)', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ darkMode: ['variant', '&:where(:not(.light))'] }), + loadModule: async () => ({ + module: { darkMode: ['variant', '&:where(:not(.light))'] }, + base: '/root', + }), }) expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` @@ -85,16 +88,19 @@ test('Config files can add plugins', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - plugins: [ - plugin(function ({ addUtilities }) { - addUtilities({ - '.no-scrollbar': { - 'scrollbar-width': 'none', - }, - }) - }), - ], + loadModule: async () => ({ + module: { + plugins: [ + plugin(function ({ addUtilities }) { + addUtilities({ + '.no-scrollbar': { + 'scrollbar-width': 'none', + }, + }) + }), + ], + }, + base: '/root', }), }) @@ -113,12 +119,15 @@ test('Plugins loaded from config files can contribute to the config', async () = ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - plugins: [ - plugin(() => {}, { - darkMode: ['variant', '&:where(:not(.light))'], - }), - ], + loadModule: async () => ({ + module: { + plugins: [ + plugin(() => {}, { + darkMode: ['variant', '&:where(:not(.light))'], + }), + ], + }, + base: '/root', }), }) @@ -139,12 +148,15 @@ test('Config file presets can contribute to the config', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - presets: [ - { - darkMode: ['variant', '&:where(:not(.light))'], - }, - ], + loadModule: async () => ({ + module: { + presets: [ + { + darkMode: ['variant', '&:where(:not(.light))'], + }, + ], + }, + base: '/root', }), }) @@ -165,24 +177,27 @@ test('Config files can affect the theme', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - primary: '#c0ffee', + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + primary: '#c0ffee', + }, }, }, - }, - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.scrollbar-primary': { - scrollbarColor: theme('colors.primary'), - }, - }) - }), - ], + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.scrollbar-primary': { + scrollbarColor: theme('colors.primary'), + }, + }) + }), + ], + }, + base: '/root', }), }) @@ -206,13 +221,16 @@ test('Variants in CSS overwrite variants from plugins', async () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - darkMode: ['variant', '&:is(.dark)'], - plugins: [ - plugin(function ({ addVariant }) { - addVariant('light', '&:is(.light)') - }), - ], + loadModule: async () => ({ + module: { + darkMode: ['variant', '&:is(.dark)'], + plugins: [ + plugin(function ({ addVariant }) { + addVariant('light', '&:is(.light)') + }), + ], + }, + base: '/root', }), }) @@ -253,49 +271,52 @@ describe('theme callbacks', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - extend: { - fontSize: { - base: ['200rem', { lineHeight: '201rem' }], - md: ['200rem', { lineHeight: '201rem' }], - xl: ['200rem', { lineHeight: '201rem' }], - }, - - // Direct access - lineHeight: ({ theme }) => ({ - base: theme('fontSize.base[1].lineHeight'), - md: theme('fontSize.md[1].lineHeight'), - xl: theme('fontSize.xl[1].lineHeight'), - }), - - // Tuple access - typography: ({ theme }) => ({ - '[class~=lead-base]': { - fontSize: theme('fontSize.base')[0], - ...theme('fontSize.base')[1], - }, - '[class~=lead-md]': { - fontSize: theme('fontSize.md')[0], - ...theme('fontSize.md')[1], + loadModule: async () => ({ + module: { + theme: { + extend: { + fontSize: { + base: ['200rem', { lineHeight: '201rem' }], + md: ['200rem', { lineHeight: '201rem' }], + xl: ['200rem', { lineHeight: '201rem' }], }, - '[class~=lead-xl]': { - fontSize: theme('fontSize.xl')[0], - ...theme('fontSize.xl')[1], - }, - }), + + // Direct access + lineHeight: ({ theme }) => ({ + base: theme('fontSize.base[1].lineHeight'), + md: theme('fontSize.md[1].lineHeight'), + xl: theme('fontSize.xl[1].lineHeight'), + }), + + // Tuple access + typography: ({ theme }) => ({ + '[class~=lead-base]': { + fontSize: theme('fontSize.base')[0], + ...theme('fontSize.base')[1], + }, + '[class~=lead-md]': { + fontSize: theme('fontSize.md')[0], + ...theme('fontSize.md')[1], + }, + '[class~=lead-xl]': { + fontSize: theme('fontSize.xl')[0], + ...theme('fontSize.xl')[1], + }, + }), + }, }, - }, - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.prose': { - ...theme('typography'), - }, - }) - }), - ], + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.prose': { + ...theme('typography'), + }, + }) + }), + ], + } satisfies Config, + base: '/root', }), }) @@ -361,15 +382,18 @@ describe('theme overrides order', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - red: 'very-red', - blue: 'very-blue', + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + red: 'very-red', + blue: 'very-blue', + }, }, }, }, + base: '/root', }), }) @@ -404,35 +428,43 @@ describe('theme overrides order', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - slate: { - 200: '#200200', - 400: '#200400', - 600: '#200600', - }, - }, - }, - }, - }), - - loadPlugin: async () => { - return plugin(({ matchUtilities, theme }) => { - matchUtilities( - { - 'hover-bg': (value) => { - return { - '&:hover': { - backgroundColor: value, + loadModule: async (id) => { + if (id.includes('config.js')) { + return { + module: { + theme: { + extend: { + colors: { + slate: { + 200: '#200200', + 400: '#200400', + 600: '#200600', + }, }, - } + }, }, - }, - { values: flattenColorPalette(theme('colors')) }, - ) - }) + } satisfies Config, + base: '/root', + } + } else { + return { + module: plugin(({ matchUtilities, theme }) => { + matchUtilities( + { + 'hover-bg': (value) => { + return { + '&:hover': { + backgroundColor: value, + }, + } + }, + }, + { values: flattenColorPalette(theme('colors')) }, + ) + }), + base: '/root', + } + } }, }) @@ -524,12 +556,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: 'Potato Sans', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', + }, }, }, + base: '/root', }), }) @@ -560,12 +595,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], + }, }, }, + base: '/root', }), }) @@ -597,12 +635,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], + }, }, }, + base: '/root', }), }) @@ -634,15 +675,18 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: [ - 'Potato Sans', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: [ + 'Potato Sans', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], + }, }, }, + base: '/root', }), }) @@ -678,12 +722,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: 'Potato Sans', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', + }, }, }, + base: '/root', }), }) @@ -715,12 +762,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, }, }, + base: '/root', }), }) @@ -751,12 +801,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: { foo: 'bar', banana: 'sandwich' }, + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: { foo: 'bar', banana: 'sandwich' }, + }, }, }, + base: '/root', }), }) @@ -782,12 +835,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: 'Potato Mono', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', + }, }, }, + base: '/root', }), }) @@ -818,12 +874,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], + }, }, }, + base: '/root', }), }) @@ -855,12 +914,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], + }, }, }, + base: '/root', }), }) @@ -892,15 +954,18 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: [ - 'Potato Mono', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: [ + 'Potato Mono', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], + }, }, }, + base: '/root', }), }) @@ -936,12 +1001,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: 'Potato Mono', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', + }, }, }, + base: '/root', }), }) @@ -973,12 +1041,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: { foo: 'bar', banana: 'sandwich' }, + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: { foo: 'bar', banana: 'sandwich' }, + }, }, }, + base: '/root', }), }) @@ -1000,21 +1071,24 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s ` let compiler = await compile(input, '/root', { - loadConfig: async () => ({ - theme: { - extend: { - aria: { - polite: 'live="polite"', - }, - supports: { - 'child-combinator': 'selector(h2 > p)', - foo: 'bar', - }, - data: { - checked: 'ui~="checked"', + loadModule: async () => ({ + module: { + theme: { + extend: { + aria: { + polite: 'live="polite"', + }, + supports: { + 'child-combinator': 'selector(h2 > p)', + foo: 'bar', + }, + data: { + checked: 'ui~="checked"', + }, }, }, }, + base: '/root', }), }) @@ -1095,15 +1169,18 @@ test('merges css breakpoints with js config screens', async () => { @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: '44rem', + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', + }, }, }, }, + base: '/root', }), }) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.test.ts b/packages/tailwindcss/src/compat/config/resolve-config.test.ts index 818ff8373d85..895939bcaa4a 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.test.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.test.ts @@ -19,6 +19,7 @@ test('top level theme keys are replaced', () => { }, }, }, + base: '/root', }, { config: { @@ -28,6 +29,7 @@ test('top level theme keys are replaced', () => { }, }, }, + base: '/root', }, { config: { @@ -37,6 +39,7 @@ test('top level theme keys are replaced', () => { }, }, }, + base: '/root', }, ]) @@ -68,6 +71,7 @@ test('theme can be extended', () => { }, }, }, + base: '/root', }, { config: { @@ -79,6 +83,7 @@ test('theme can be extended', () => { }, }, }, + base: '/root', }, ]) @@ -112,6 +117,7 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }, + base: '/root', }, { config: { @@ -124,6 +130,7 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }, + base: '/root', }, { config: { @@ -135,6 +142,7 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }, + base: '/root', }, ]) @@ -192,6 +200,7 @@ test('theme keys can read from the CSS theme', () => { }), }, }, + base: '/root', }, ]) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 268e6883a9dc..1f59b50cf149 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -12,6 +12,7 @@ import { export interface ConfigFile { path?: string + base: string config: UserConfig } @@ -103,7 +104,7 @@ export interface PluginUtils { theme(keypath: string, defaultValue?: any): any } -function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): void { +function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFile): void { let plugins: PluginWithConfig[] = [] // Normalize plugins so they share the same shape @@ -133,7 +134,7 @@ function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): v } for (let preset of config.presets ?? []) { - extractConfigs(ctx, { path, config: preset }) + extractConfigs(ctx, { path, base, config: preset }) } // Apply configs from plugins @@ -141,7 +142,7 @@ function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): v ctx.plugins.push(plugin) if (plugin.config) { - extractConfigs(ctx, { path, config: plugin.config }) + extractConfigs(ctx, { path, base, config: plugin.config }) } } @@ -150,7 +151,7 @@ function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): v let files = Array.isArray(content) ? content : content.files for (let file of files) { - ctx.content.files.push(typeof file === 'object' ? file : { base: path!, pattern: file }) + ctx.content.files.push(typeof file === 'object' ? file : { base, pattern: file }) } // Then apply the "user" config diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index e7ea67de2fce..d3fc05d3714b 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,6 +1,16 @@ import { version } from '../package.json' import { substituteAtApply } from './apply' -import { comment, context, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' +import { + comment, + context, + decl, + rule, + toCss, + walk, + WalkAction, + type AstNode, + type Rule, +} from './ast' import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' @@ -48,7 +58,7 @@ async function parseCss( base: string, { loadModule = throwOnLoadModule, loadStylesheet = throwOnLoadStylesheet }: CompileOptions = {}, ) { - let ast = [context({ base }, CSS.parse(css))] + let ast = [context({ base }, CSS.parse(css))] as AstNode[] if (css.includes('@import')) { await substituteAtImports(ast, base, loadStylesheet) @@ -60,9 +70,9 @@ async function parseCss( let customUtilities: ((designSystem: DesignSystem) => void)[] = [] let firstThemeRule: Rule | null = null let keyframesRules: Rule[] = [] - let globs: { origin?: string; pattern: string }[] = [] + let globs: { base: string; pattern: string }[] = [] - walk(ast, (node, { parent, replaceWith }) => { + walk(ast, (node, { parent, replaceWith, context }) => { if (node.kind !== 'rule') return // Collect custom `@utility` at-rules @@ -97,7 +107,7 @@ async function parseCss( throw new Error('`@source` cannot have a body.') } - if (parent !== null) { + if (parent !== null && parent.kind !== 'context') { throw new Error('`@source` cannot be nested.') } @@ -109,14 +119,14 @@ async function parseCss( ) { throw new Error('`@source` paths must be quoted.') } - globs.push({ pattern: path.slice(1, -1) }) + globs.push({ base: context.base as string, pattern: path.slice(1, -1) }) replaceWith([]) return } // Register custom variants from `@variant` at-rules if (node.selector.startsWith('@variant ')) { - if (parent !== null) { + if (parent !== null && parent.kind !== 'context') { throw new Error('`@variant` cannot be nested.') } From fa4b781223b1542f190747e6389c39e73e4674a0 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 13:25:23 +0200 Subject: [PATCH 11/51] Fix css function tests --- .../tailwindcss/src/css-functions.test.ts | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index f7c5f6688884..a5c149ad4b3a 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -337,7 +337,7 @@ describe('theme function', () => { `, '/root', { - loadConfig: async () => ({}), + loadModule: async () => ({ module: {}, base: '/root' }), }, ) @@ -797,23 +797,26 @@ describe('in plugins', () => { `, '/root', { - async loadPlugin() { - return plugin(({ addBase, addUtilities }) => { - addBase({ - '.my-base-rule': { - color: 'theme(colors.red)', - 'outline-color': 'theme(colors.orange / 15%)', - 'background-color': 'theme(--color-blue)', - 'border-color': 'theme(--color-pink / 10%)', - }, - }) + async loadModule() { + return { + module: plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + color: 'theme(colors.red)', + 'outline-color': 'theme(colors.orange / 15%)', + 'background-color': 'theme(--color-blue)', + 'border-color': 'theme(--color-pink / 10%)', + }, + }) - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }) + addUtilities({ + '.my-utility': { + color: 'theme(colors.red)', + }, + }) + }), + base: '/root', + } }, }, ) @@ -853,31 +856,34 @@ describe('in JS config files', () => { `, '/root', { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - primary: 'theme(colors.red)', - secondary: 'theme(--color-orange)', + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + primary: 'theme(colors.red)', + secondary: 'theme(--color-orange)', + }, }, }, + plugins: [ + plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + background: 'theme(colors.primary)', + color: 'theme(colors.secondary)', + }, + }) + + addUtilities({ + '.my-utility': { + color: 'theme(colors.red)', + }, + }) + }), + ], }, - plugins: [ - plugin(({ addBase, addUtilities }) => { - addBase({ - '.my-base-rule': { - background: 'theme(colors.primary)', - color: 'theme(colors.secondary)', - }, - }) - - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }), - ], + base: '/root', }), }, ) From bd605bb43e26a18121d5bc79a2e184f90b87c0cb Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 13:49:15 +0200 Subject: [PATCH 12/51] Fix tailwindcss/src/index.test.ts tests --- packages/tailwindcss/src/index.test.ts | 173 ++++++++++++++----------- packages/tailwindcss/src/index.ts | 3 +- 2 files changed, 100 insertions(+), 76 deletions(-) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 76eb1b8aa6c1..5180b6250dba 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1430,17 +1430,20 @@ describe('Parsing themes values from CSS', () => { `, '/root', { - loadPlugin: async () => { - return plugin(({}) => {}, { - theme: { - extend: { - colors: { - red: 'tomato', - orange: '#f28500', + loadModule: async () => { + return { + module: plugin(({}) => {}, { + theme: { + extend: { + colors: { + red: 'tomato', + orange: '#f28500', + }, }, }, - }, - }) + }), + base: '/root', + } }, }, ) @@ -1474,16 +1477,19 @@ describe('Parsing themes values from CSS', () => { `, '/root', { - loadConfig: async () => { + loadModule: async () => { return { - theme: { - extend: { - colors: { - red: 'tomato', - orange: '#f28500', + module: { + theme: { + extend: { + colors: { + red: 'tomato', + orange: '#f28500', + }, }, }, }, + base: '/root', } }, }, @@ -1514,11 +1520,12 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`)) @@ -1531,11 +1538,12 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`)) @@ -1550,11 +1558,12 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` cannot be nested.]`)) @@ -1571,8 +1580,8 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return plugin.withOptions((options) => { + loadModule: async () => ({ + module: plugin.withOptions((options) => { expect(options).toEqual({ color: 'red', }) @@ -1584,8 +1593,9 @@ describe('plugins', () => { }, }) } - }) - }, + }), + base: '/root', + }), }, ) @@ -1623,8 +1633,8 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return plugin.withOptions((options) => { + loadModule: async () => ({ + module: plugin.withOptions((options) => { expect(options).toEqual({ 'is-null': null, 'is-true': true, @@ -1643,8 +1653,9 @@ describe('plugins', () => { }) return () => {} - }) - }, + }), + base: '/root', + }), }, ) }) @@ -1663,8 +1674,8 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return plugin.withOptions((options) => { + loadModule: async () => ({ + module: plugin.withOptions((options) => { return ({ addUtilities }) => { addUtilities({ '.text-primary': { @@ -1672,8 +1683,9 @@ describe('plugins', () => { }, }) } - }) - }, + }), + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1701,15 +1713,16 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return plugin(({ addUtilities }) => { + loadModule: async () => ({ + module: plugin(({ addUtilities }) => { addUtilities({ '.text-primary': { color: 'red', }, }) - }) - }, + }), + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1727,7 +1740,7 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => plugin(() => {}), + loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1749,7 +1762,7 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => plugin(() => {}), + loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1775,11 +1788,12 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1807,11 +1821,12 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', ['&:hover', '&:focus']) - } - }, + }, + base: '/root', + }), }, ) @@ -1840,14 +1855,15 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '&:hover': '@slot', '&:focus': '@slot', }) - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1875,16 +1891,17 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '@media (hover: hover)': { '&:hover': '@slot', }, '&:focus': '@slot', }) - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1924,8 +1941,8 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '&': { '--custom-property': '@slot', @@ -1933,8 +1950,9 @@ describe('plugins', () => { '&:focus': '@slot', }, }) - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline']) @@ -1962,11 +1980,12 @@ describe('plugins', () => { `, '/root', { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('dark', '&:is([data-theme=dark] *)') - } - }, + }, + base: '/root', + }), }, ) let compiled = build( @@ -2004,7 +2023,7 @@ describe('@source', () => { '/root', ) - expect(globs).toEqual([{ pattern: './foo/bar/*.ts' }]) + expect(globs).toEqual([{ pattern: './foo/bar/*.ts', base: '/root' }]) }) test('emits multiple @source files', async () => { @@ -2016,7 +2035,10 @@ describe('@source', () => { '/root', ) - expect(globs).toEqual([{ pattern: './foo/**/*.ts' }, { pattern: './php/secr3t/smarty.php' }]) + expect(globs).toEqual([ + { pattern: './foo/**/*.ts', base: '/root' }, + { pattern: './php/secr3t/smarty.php', base: '/root' }, + ]) }) }) @@ -2576,15 +2598,16 @@ test('addBase', async () => { `, '/root', { - loadPlugin: async () => { - return ({ addBase }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addBase }: PluginAPI) => { addBase({ body: { 'font-feature-settings': '"tnum"', }, }) - } - }, + }, + base: '/root', + }), }, ) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d3fc05d3714b..8a20f5e0a2fc 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -14,6 +14,7 @@ import { import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' +import { type Plugin } from './compat/plugin-api' import { compileCandidates } from './compile' import { substituteFunctions, THEME_FUNCTION_INVOCATION } from './css-functions' import * as CSS from './css-parser' @@ -25,7 +26,7 @@ export type Config = UserConfig const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { - loadModule?: (id: string, base: string) => Promise<{ module: unknown; base: string }> + loadModule?: (id: string, base: string) => Promise<{ module: Plugin | Config; base: string }> loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> } From 0d0c0e602afd86a227896adb383ecc6045b9b022 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 13:49:57 +0200 Subject: [PATCH 13/51] Fix compat/config.test.ts --- packages/tailwindcss/src/compat/config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index c90558a5537e..c808ac682318 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -15,7 +15,7 @@ test('Config files can add content', async () => { loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }), }) - expect(compiler.globs).toEqual([{ origin: './config.js', pattern: './file.txt' }]) + expect(compiler.globs).toEqual([{ base: '/root', pattern: './file.txt' }]) }) test('Config files can change dark mode (media)', async () => { From 51f7c55a22e34d82a33384c33ba0f2ab81dc85fa Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 13:53:31 +0200 Subject: [PATCH 14/51] Fix screens-config.test.ts --- .../src/compat/screens-config.test.ts | 137 ++++++++++-------- 1 file changed, 79 insertions(+), 58 deletions(-) diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 05f725a09b53..e82ea012ffeb 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -19,15 +19,18 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: '44rem', + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', + }, }, }, }, + base: '/root', }), }) @@ -99,18 +102,21 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - xs: '30rem', - sm: '40rem', - md: '48rem', - lg: '60rem', + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + xs: '30rem', + sm: '40rem', + md: '48rem', + lg: '60rem', + }, }, }, }, + base: '/root', }), }) @@ -194,15 +200,18 @@ test('JS config `screens` only setup, even if those match the default-theme expo @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - screens: { - sm: '40rem', - md: '48rem', - lg: '64rem', + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + screens: { + sm: '40rem', + md: '48rem', + lg: '64rem', + }, }, }, + base: '/root', }), }) @@ -270,15 +279,18 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, }, }, + base: '/root', }), }) @@ -373,17 +385,20 @@ test('JS config with `theme: { extends }` should not include the `default-config @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, }, }, }, + base: '/root', }), }) @@ -448,23 +463,26 @@ describe('complex screen configs', () => { @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: { max: '639px' }, - md: [ - // - { min: '668px', max: '767px' }, - '868px', - ], - lg: { min: '868px' }, - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: { max: '639px' }, + md: [ + // + { min: '668px', max: '767px' }, + '868px', + ], + lg: { min: '868px' }, + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, + }, }, }, }, + base: '/root', }), }) @@ -532,16 +550,19 @@ describe('complex screen configs', () => { @tailwind utilities; ` - let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: '40rem', - portrait: { raw: 'screen and (orientation: portrait)' }, + let compiler = await compile(input, '/root', { + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '40rem', + portrait: { raw: 'screen and (orientation: portrait)' }, + }, }, }, }, + base: '/root', }), }) From 45e7db3aebdd0490c824f5211586157372b485be Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 14:05:00 +0200 Subject: [PATCH 15/51] Work around plugin-api.test.ts changes for now --- .../tailwindcss/src/compat/plugin-api.test.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 041d51b5395e..8ebfebb6d25f 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1,12 +1,26 @@ import { describe, expect, test, vi } from 'vitest' -import { compile } from '..' +import { compile as coreCompile } from '..' import plugin from '../plugin' import { optimizeCss } from '../test-utils/run' import defaultTheme from './default-theme' -import type { CssInJs, PluginAPI } from './plugin-api' +import type { CssInJs, PluginAPI, PluginWithConfig } from './plugin-api' const css = String.raw +// TODO: Expand the API changes into the tests below +function compile( + css: string, + base: string, + { loadPlugin }: { loadPlugin: () => Promise }, +) { + return coreCompile(css, base, { + async loadModule(id, base) { + let plugin = await loadPlugin() + return { module: plugin, base } + }, + }) +} + describe('theme', async () => { test('plugin theme can contain objects', async () => { let input = css` From 0c4de34009f852ee784f9e3bfa7cd5dd121891da Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 14:20:41 +0200 Subject: [PATCH 16/51] Fix node API by using require.resolve over path.resolve, whoops/ --- packages/@tailwindcss-node/src/compile.ts | 8 +++++--- .../@tailwindcss-postcss/src/index.test.ts | 16 +++++++++++++++- packages/@tailwindcss-postcss/src/index.ts | 7 +------ packages/@tailwindcss-vite/src/index.ts | 7 +------ packages/tailwindcss/src/index.ts | 2 +- packages/tailwindcss/src/plugin.test.ts | 18 ++++++++++-------- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 1e13de8ed2f6..4016edfec304 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -14,14 +14,16 @@ export async function compile( return await _compile(css, base, { async loadModule(id, base) { if (id[0] !== '.') { - let resolvedPath = path.resolve(base, id) + let resolvedPath = require.resolve(id, { paths: [base] }) + + let module = await importModule(pathToFileURL(resolvedPath).href) return { base: basename(resolvedPath), - module: importModule(pathToFileURL(resolvedPath).href).then((m) => m.default ?? m), + module: module.default ?? module, } } - let resolvedPath = path.resolve(base, id) + let resolvedPath = require.resolve(id, { paths: [base] }) let [module, moduleDependencies] = await Promise.all([ importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), getModuleDependencies(resolvedPath), diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 1639ec5e5b71..db671dcf703b 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -179,7 +179,7 @@ describe('plugins', () => { expect(result.css.trim()).toMatchInlineSnapshot(` ".text-2xl { font-size: var(--font-size-2xl, 1.5rem); - line-height: var(--font-size-2xl--line-height, 2rem); + line-height: var(--tw-leading, var(--font-size-2xl--line-height, 2rem)); } .text-black\\/50 { @@ -192,12 +192,26 @@ describe('plugins', () => { @media (width >= 96rem) { .\\32 xl\\:font-bold { + --tw-font-weight: 700; font-weight: 700; } } .foo { color: var(--color-red-500, #ef4444); + } + + @supports (-moz-orient: inline) { + @layer base { + *, :before, :after, ::backdrop { + --tw-font-weight: initial; + } + } + } + + @property --tw-font-weight { + syntax: "*"; + inherits: false }" `) }) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 4f9eccbb56ef..421611d9ee79 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -141,12 +141,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // Look for candidates used to generate the CSS let scanner = new Scanner({ detectSources: { base }, - sources: context.compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the config - // file where it is specified. - base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, - pattern, - })), + sources: context.compiler.globs, }) // diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 29905c693b8c..3d538a1eb266 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -418,12 +418,7 @@ export class Root { }) this.scanner = new Scanner({ - sources: this.compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the config - // file where it is specified. - base: origin ? path.dirname(path.resolve(inputBase, origin)) : inputBase, - pattern, - })), + sources: this.compiler.globs, }) } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 8a20f5e0a2fc..1e5b718ac366 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -333,7 +333,7 @@ export async function compile( base: string, opts: CompileOptions = {}, ): Promise<{ - globs: { origin?: string; pattern: string }[] + globs: { base: string; pattern: string }[] build(candidates: string[]): string }> { let { designSystem, ast, globs } = await parseCss(css, base, opts) diff --git a/packages/tailwindcss/src/plugin.test.ts b/packages/tailwindcss/src/plugin.test.ts index 29780e4f008b..d9a966d3333d 100644 --- a/packages/tailwindcss/src/plugin.test.ts +++ b/packages/tailwindcss/src/plugin.test.ts @@ -10,15 +10,16 @@ test('plugin', async () => { ` let compiler = await compile(input, '/root', { - loadPlugin: async () => { - return plugin(function ({ addBase }) { + loadModule: async () => ({ + module: plugin(function ({ addBase }) { addBase({ body: { margin: '0', }, }) - }) - }, + }), + base: '/root', + }), }) expect(compiler.build([])).toMatchInlineSnapshot(` @@ -37,8 +38,8 @@ test('plugin.withOptions', async () => { ` let compiler = await compile(input, '/root', { - loadPlugin: async () => { - return plugin.withOptions(function (opts = { foo: '1px' }) { + loadModule: async () => ({ + module: plugin.withOptions(function (opts = { foo: '1px' }) { return function ({ addBase }) { addBase({ body: { @@ -46,8 +47,9 @@ test('plugin.withOptions', async () => { }, }) } - }) - }, + }), + base: '/root', + }), }) expect(compiler.build([])).toMatchInlineSnapshot(` From 615cc9948bbe1296af3769f585998bb90b09102e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 14:49:10 +0200 Subject: [PATCH 17/51] Fix @tailwindcss/postcss --- integrations/postcss/index.test.ts | 80 +++++++++++++++++++ packages/@tailwindcss-node/src/compile.ts | 30 +++++-- packages/@tailwindcss-postcss/package.json | 2 - .../@tailwindcss-postcss/src/index.test.ts | 4 +- packages/@tailwindcss-postcss/src/index.ts | 31 ++----- pnpm-lock.yaml | 28 +------ 6 files changed, 111 insertions(+), 64 deletions(-) diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index d4e4f4d394ba..273f07f73cfd 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -79,6 +79,86 @@ test( }, ) +test( + 'production build with `postcss-import` (string)', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': json` + { + "dependencies": { + "postcss": "^8", + "postcss-cli": "^10", + "postcss-import": "^16", + "tailwindcss": "workspace:^", + "@tailwindcss/postcss": "workspace:^" + } + } + `, + 'project-a/postcss.config.js': js` + module.exports = { + plugins: { + 'postcss-import': {}, + '@tailwindcss/postcss': {}, + }, + } + `, + 'project-a/index.html': html` +
+ `, + 'project-a/plugin.js': js` + module.exports = function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, + 'project-a/tailwind.config.js': js` + module.exports = { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + @plugin '../plugin.js'; + `, + 'project-a/src/index.js': js` + const className = "content-['a/src/index.js']" + module.exports = { className } + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec }) => { + await exec('pnpm postcss src/index.css --output dist/out.css', { + cwd: path.join(root, 'project-a'), + }) + + await fs.expectFileToContain('project-a/dist/out.css', [ + candidate`underline`, + candidate`flex`, + candidate`content-['a/src/index.js']`, + candidate`content-['b/src/index.js']`, + candidate`inverted:flex`, + candidate`hocus:underline`, + ]) + }, +) + test( 'production build (ESM)', { diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 4016edfec304..fd64e7c4bafc 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' import fs from 'node:fs' import fsPromises from 'node:fs/promises' -import path, { basename } from 'node:path' +import path, { dirname } from 'node:path' import { pathToFileURL } from 'node:url' import { compile as _compile } from 'tailwindcss' import { getModuleDependencies } from './get-module-dependencies' @@ -14,16 +14,22 @@ export async function compile( return await _compile(css, base, { async loadModule(id, base) { if (id[0] !== '.') { - let resolvedPath = require.resolve(id, { paths: [base] }) + let resolvedPath = resolveJsId(id, base) + if (!resolvedPath) { + throw new Error(`Could not resolve '${id}' from '${base}'`) + } let module = await importModule(pathToFileURL(resolvedPath).href) return { - base: basename(resolvedPath), + base: dirname(resolvedPath), module: module.default ?? module, } } - let resolvedPath = require.resolve(id, { paths: [base] }) + let resolvedPath = resolveJsId(id, base) + if (!resolvedPath) { + throw new Error(`Could not resolve '${id}' from '${base}'`) + } let [module, moduleDependencies] = await Promise.all([ importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), getModuleDependencies(resolvedPath), @@ -34,7 +40,7 @@ export async function compile( onDependency(file) } return { - base: basename(resolvedPath), + base: dirname(resolvedPath), module: module.default ?? module, } }, @@ -67,14 +73,14 @@ async function importModule(path: string): Promise { } } -const resolver = EnhancedResolve.ResolverFactory.createResolver({ +const cssResolver = EnhancedResolve.ResolverFactory.createResolver({ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), useSyncFileSystemCalls: true, extensions: ['.css'], mainFields: ['style'], conditionNames: ['style'], }) -export function resolveCssId(id: string, base: string) { +function resolveCssId(id: string, base: string) { if (typeof globalThis.__tw_resolve === 'function') { let resolved = globalThis.__tw_resolve(id, base) if (resolved) { @@ -82,5 +88,13 @@ export function resolveCssId(id: string, base: string) { } } - return resolver.resolveSync({}, base, id) + return cssResolver.resolveSync({}, base, id) +} + +const jsResolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, +}) +function resolveJsId(id: string, base: string) { + return jsResolver.resolveSync({}, base, id) } diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index 94d561f597bc..6c167243d9d6 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -33,12 +33,10 @@ "@tailwindcss/node": "workspace:^", "@tailwindcss/oxide": "workspace:^", "lightningcss": "catalog:", - "postcss-import": "^16.1.0", "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3", "postcss": "^8.4.41", "internal-example-plugin": "workspace:*", "internal-postcss-fix-relative-paths": "workspace:^" diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index db671dcf703b..a7c55f1dbcb5 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -197,9 +197,7 @@ describe('plugins', () => { } } - .foo { - color: var(--color-red-500, #ef4444); - } + @apply text-red-500; @supports (-moz-orient: inline) { @layer base { diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 421611d9ee79..32db243895b5 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -5,8 +5,7 @@ import fs from 'fs' import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' import path from 'path' -import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss' -import postcssImport from 'postcss-import' +import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' /** * A Map that can generate default values for keys that don't exist. @@ -51,30 +50,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } }) - let hasApply: boolean, hasTailwind: boolean - return { postcssPlugin: '@tailwindcss/postcss', plugins: [ - // We need to run `postcss-import` first to handle `@import` rules. - postcssImport(), + // We need to handle the case where `postcss-import` might have run before the Tailwind CSS + // plugin is run. In this case, we need to manually fix relative paths before processing it + // in core. fixRelativePathsPlugin(), { postcssPlugin: 'tailwindcss', - Once() { - // Reset some state between builds - hasApply = false - hasTailwind = false - }, - AtRule(rule: AtRule) { - if (rule.name === 'apply') { - hasApply = true - } else if (rule.name === 'tailwind') { - hasApply = true - hasTailwind = true - } - }, async OnceExit(root, { result }) { let inputFile = result.opts.from ?? '' let context = cache.get(inputFile) @@ -133,9 +118,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } - // Do nothing if neither `@tailwind` nor `@apply` is used - if (!hasTailwind && !hasApply) return - let css = '' // Look for candidates used to generate the CSS @@ -144,7 +126,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { sources: context.compiler.globs, }) - // let candidates = scanner.scan() // Add all found files as direct dependencies @@ -172,10 +153,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { if (rebuildStrategy === 'full') { context.compiler = await createCompiler() - css = context.compiler.build(hasTailwind ? candidates : []) - } else if (rebuildStrategy === 'incremental') { - css = context.compiler.build!(candidates) } + css = context.compiler.build(candidates) // Replace CSS if (css !== context.css && optimize) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 287241ada722..5854cc7a1d15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,9 +197,6 @@ importers: lightningcss: specifier: 'catalog:' version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) tailwindcss: specifier: workspace:^ version: link:../tailwindcss @@ -207,9 +204,6 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 internal-example-plugin: specifier: workspace:* version: link:../internal-example-plugin @@ -1058,7 +1052,6 @@ packages: '@parcel/watcher-darwin-arm64@2.4.2-alpha.0': resolution: {integrity: sha512-2xH4Ve7OKjIh+4YRfTN3HGJa2W8KTPLOALHZj5fxcbTPwaVxdpIRItDrcikUx2u3AzGAFme7F+AZZXHnf0F15Q==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.4.1': @@ -1070,7 +1063,6 @@ packages: '@parcel/watcher-darwin-x64@2.4.2-alpha.0': resolution: {integrity: sha512-xtjmXUH4YZVah5+7Q0nb+fpRP5qZn9cFfuPuZ4k77UfUGVwhacgZyIRQgIOwMP3GkgW4TsrKQaw1KIe7L1ZqcQ==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.4.1': @@ -1094,7 +1086,6 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.4.2-alpha.0': resolution: {integrity: sha512-vIIOcZf+fgsRReIK3Fw0WINvGo9UwiXfisnqYRzfpNByRZvkEPkGTIVe8iiDp72NhPTVmwIvBqM6yKDzIaw8GQ==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.4.1': @@ -1106,7 +1097,6 @@ packages: '@parcel/watcher-linux-arm64-musl@2.4.2-alpha.0': resolution: {integrity: sha512-gXqEAoLG9bBCbQNUgqjSOxHcjpmCZmYT9M8UvrdTMgMYgXgiWcR8igKlPRd40mCIRZSkMpN2ScSy2WjQ0bQZnQ==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.4.1': @@ -1118,7 +1108,6 @@ packages: '@parcel/watcher-linux-x64-glibc@2.4.2-alpha.0': resolution: {integrity: sha512-/WJJ3Y46ubwQW+Z+mzpzK3pvqn/AT7MA63NB0+k9GTLNxJQZNREensMtpJ/FJ+LVIiraEHTY22KQrsx9+DeNbw==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.4.1': @@ -1130,7 +1119,6 @@ packages: '@parcel/watcher-linux-x64-musl@2.4.2-alpha.0': resolution: {integrity: sha512-1dz4fTM5HaANk3RSRmdhALT+bNqTHawVDL1D77HwV/FuF/kSjlM3rGrJuFaCKwQ5E8CInHCcobqMN8Jh8LYaRg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-win32-arm64@2.4.1': @@ -1154,7 +1142,6 @@ packages: '@parcel/watcher-win32-x64@2.4.2-alpha.0': resolution: {integrity: sha512-U2abMKF7JUiIxQkos19AvTLFcnl2Xn8yIW1kzu+7B0Lux4Gkuu/BUDBroaM1s6+hwgK63NOLq9itX2Y3GwUThg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [win32] '@parcel/watcher@2.4.1': @@ -1490,13 +1477,11 @@ packages: bun@1.1.22: resolution: {integrity: sha512-G2HCPhzhjDc2jEDkZsO9vwPlpHrTm7a8UVwx9oNS5bZqo5OcSK5GPuWYDWjj7+37bRk5OVLfeIvUMtSrbKeIjQ==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true bun@1.1.26: resolution: {integrity: sha512-dWSewAqE7sVbYmflJxgG47dW4vmsbar7VAnQ4ao45y3ulr3n7CwdsMLFnzd28jhPRtF+rsaVK2y4OLIkP3OD4A==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -2263,13 +2248,11 @@ packages: lightningcss-darwin-arm64@1.26.0: resolution: {integrity: sha512-n4TIvHO1NY1ondKFYpL2ZX0bcC2y6yjXMD6JfyizgR8BCFNEeArINDzEaeqlfX9bXz73Bpz/Ow0nu+1qiDrBKg==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.26.0: resolution: {integrity: sha512-Rf9HuHIDi1R6/zgBkJh25SiJHF+dm9axUZW/0UoYCW1/8HV0gMI0blARhH4z+REmWiU1yYT/KyNF3h7tHyRXUg==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.26.0: @@ -2287,25 +2270,21 @@ packages: lightningcss-linux-arm64-gnu@1.26.0: resolution: {integrity: sha512-iJmZM7fUyVjH+POtdiCtExG+67TtPUTer7K/5A8DIfmPfrmeGvzfRyBltGhQz13Wi15K1lf2cPYoRaRh6vcwNA==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [linux] lightningcss-linux-arm64-musl@1.26.0: resolution: {integrity: sha512-XxoEL++tTkyuvu+wq/QS8bwyTXZv2y5XYCMcWL45b8XwkiS8eEEEej9BkMGSRwxa5J4K+LDeIhLrS23CpQyfig==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [linux] lightningcss-linux-x64-gnu@1.26.0: resolution: {integrity: sha512-1dkTfZQAYLj8MUSkd6L/+TWTG8V6Kfrzfa0T1fSlXCXQHrt1HC1/UepXHtKHDt/9yFwyoeayivxXAsApVxn6zA==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [linux] lightningcss-linux-x64-musl@1.26.0: resolution: {integrity: sha512-yX3Rk9m00JGCUzuUhFEojY+jf/6zHs3XU8S8Vk+FRbnr4St7cjyMXdNjuA2LjiT8e7j8xHRCH8hyZ4H/btRE4A==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [linux] lightningcss-win32-arm64-msvc@1.26.0: @@ -2317,7 +2296,6 @@ packages: lightningcss-win32-x64-msvc@1.26.0: resolution: {integrity: sha512-pYS3EyGP3JRhfqEFYmfFDiZ9/pVNfy8jVIYtrx9TVNusVyDK3gpW1w/rbvroQ4bDJi7grdUtyrYU6V2xkY/bBw==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [win32] lightningcss@1.26.0: @@ -4445,7 +4423,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -4469,7 +4447,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -4491,7 +4469,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 From 58657bf9677588d674adbcfffb82cb3d21cf5547 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 14:55:13 +0200 Subject: [PATCH 18/51] Update @tailwindcss/vite --- packages/@tailwindcss-vite/package.json | 4 -- .../src/fixtures/src/a/index.css | 1 - .../src/fixtures/src/b/example.css | 1 - .../src/fixtures/src/b/index.css | 3 -- .../src/fixtures/src/b/tw.css | 1 - .../src/fixtures/src/index.css | 2 - .../src/fixtures/src/invalid.css | 1 - .../src/fixtures/src/plugins-in-root.css | 5 -- .../src/fixtures/src/plugins-in-sibling.css | 3 -- packages/@tailwindcss-vite/src/index.bench.ts | 31 ------------ packages/@tailwindcss-vite/src/index.ts | 50 +------------------ pnpm-lock.yaml | 9 ---- 12 files changed, 2 insertions(+), 109 deletions(-) delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/a/index.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/b/example.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/b/index.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/b/tw.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/index.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/invalid.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css delete mode 100644 packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css delete mode 100644 packages/@tailwindcss-vite/src/index.bench.ts diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json index a74b4c3c81d7..20a7de978cea 100644 --- a/packages/@tailwindcss-vite/package.json +++ b/packages/@tailwindcss-vite/package.json @@ -31,14 +31,10 @@ "@tailwindcss/node": "workspace:^", "@tailwindcss/oxide": "workspace:^", "lightningcss": "catalog:", - "postcss": "^8.4.41", - "postcss-import": "^16.1.0", "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3", - "internal-postcss-fix-relative-paths": "workspace:^", "vite": "catalog:" }, "peerDependencies": { diff --git a/packages/@tailwindcss-vite/src/fixtures/src/a/index.css b/packages/@tailwindcss-vite/src/fixtures/src/a/index.css deleted file mode 100644 index 967fcc0b332d..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/a/index.css +++ /dev/null @@ -1 +0,0 @@ -@import '../b/example.css'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/b/example.css b/packages/@tailwindcss-vite/src/fixtures/src/b/example.css deleted file mode 100644 index 1d637a65c092..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/b/example.css +++ /dev/null @@ -1 +0,0 @@ -@import './tw.css'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/b/index.css b/packages/@tailwindcss-vite/src/fixtures/src/b/index.css deleted file mode 100644 index bb7dac823454..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/b/index.css +++ /dev/null @@ -1,3 +0,0 @@ -.red { - @apply bg-red-500; -} diff --git a/packages/@tailwindcss-vite/src/fixtures/src/b/tw.css b/packages/@tailwindcss-vite/src/fixtures/src/b/tw.css deleted file mode 100644 index 73a943cd0413..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/b/tw.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss" diff --git a/packages/@tailwindcss-vite/src/fixtures/src/index.css b/packages/@tailwindcss-vite/src/fixtures/src/index.css deleted file mode 100644 index 7ee439bdae04..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/index.css +++ /dev/null @@ -1,2 +0,0 @@ -@import "./a"; -@import "./b"; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/invalid.css b/packages/@tailwindcss-vite/src/fixtures/src/invalid.css deleted file mode 100644 index b69d455c0db2..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/invalid.css +++ /dev/null @@ -1 +0,0 @@ -@import '../../example-project/src/invalid.css'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css b/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css deleted file mode 100644 index d6d5f082c364..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-root.css +++ /dev/null @@ -1,5 +0,0 @@ -@import './plugins-in-sibling.css'; - -@plugin './plugin-in-root.ts'; -@plugin '../plugin-in-root.ts'; -@plugin 'plugin-in-root'; diff --git a/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css b/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css deleted file mode 100644 index 5df3cb06176a..000000000000 --- a/packages/@tailwindcss-vite/src/fixtures/src/plugins-in-sibling.css +++ /dev/null @@ -1,3 +0,0 @@ -@plugin './plugin-in-sibling.ts'; -@plugin '../plugin-in-sibling.ts'; -@plugin 'plugin-in-sibling'; diff --git a/packages/@tailwindcss-vite/src/index.bench.ts b/packages/@tailwindcss-vite/src/index.bench.ts deleted file mode 100644 index 8e0719f9fb62..000000000000 --- a/packages/@tailwindcss-vite/src/index.bench.ts +++ /dev/null @@ -1,31 +0,0 @@ -import fs from 'node:fs' -import { bench, describe } from 'vitest' - -import path from 'node:path' -import { Root } from '.' - -let emptySetGetter = () => new Set() - -let base = path.join(__dirname, 'fixtures/src') -let filepath = path.join(base, 'index.css') -let contents = fs.readFileSync(filepath, 'utf-8') - -describe('compare', () => { - bench('postcss-import', async () => { - try { - let root = new Root(filepath, emptySetGetter, base, 'postcss') - await root.generate(contents, () => {}) - } catch (e) { - console.error(e) - } - }) - - bench('tailwindcss-import', async () => { - try { - let root = new Root(filepath, emptySetGetter, base, 'tailwindcss') - await root.generate(contents, () => {}) - } catch (e) { - console.error(e) - } - }) -}) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 3d538a1eb266..d69a6e11e15f 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -2,9 +2,8 @@ import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' -import fixRelativePathsPlugin, { normalizePath } from 'internal-postcss-fix-relative-paths' +import { normalizePath } from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' -import fs from 'node:fs/promises' import path from 'path' import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' @@ -267,18 +266,6 @@ function isPotentialCssRootFile(id: string) { return isCssFile } -function isCssRootFile(content: string) { - return ( - content.includes('@tailwind') || - content.includes('@config') || - content.includes('@plugin') || - content.includes('@apply') || - content.includes('@theme') || - content.includes('@variant') || - content.includes('@utility') - ) -} - function optimizeCss( input: string, { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, @@ -359,7 +346,6 @@ export class Root { private id: string, private getSharedCandidates: () => Set, private base: string, - private atImportResolver: 'postcss' | 'tailwindcss' = 'postcss', ) {} // Generate the CSS for the root file. This can return false if the file is @@ -377,39 +363,7 @@ export class Root { clearRequireCache(Array.from(this.dependencies)) this.dependencies = new Set([idToPath(inputPath)]) - let css = content - if (this.atImportResolver === 'postcss') { - const [{ default: postcss }, { default: postcssImport }] = await Promise.all([ - import('postcss'), - import('postcss-import'), - ]) - - let postcssCompiled = await postcss([ - postcssImport({ - load: (path) => { - this.dependencies.add(path) - addWatchFile(path) - return fs.readFile(path, 'utf8') - }, - }), - fixRelativePathsPlugin(), - ]).process(content, { - from: inputPath, - to: inputPath, - }) - - css = postcssCompiled.css - } - - // This is done inside the Root#generate() method so that we can later use - // information from the Tailwind compiler to determine if the file is a - // CSS root (necessary because we will probably inline the `@import` - // resolution at some point). - // if (!isCssRootFile(css)) { - // return false - // } - - this.compiler = await compile(css, { + this.compiler = await compile(content, { base: inputBase, onDependency: (path) => { addWatchFile(path) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5854cc7a1d15..204cbb23fc8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -323,12 +323,6 @@ importers: lightningcss: specifier: 'catalog:' version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) - postcss: - specifier: ^8.4.41 - version: 8.4.41 - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) tailwindcss: specifier: workspace:^ version: link:../tailwindcss @@ -336,9 +330,6 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 internal-postcss-fix-relative-paths: specifier: workspace:^ version: link:../internal-postcss-fix-relative-paths From 87c0bedd78a26ae0d118b9845d316d83f759a4f8 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:10:06 +0200 Subject: [PATCH 19/51] Update @tailwindcss/cli --- packages/@tailwindcss-cli/package.json | 6 - .../src/commands/build/index.ts | 104 ++++-------------- packages/@tailwindcss-cli/tsup.config.ts | 1 - packages/@tailwindcss-node/src/compile.ts | 11 ++ packages/@tailwindcss-standalone/src/index.ts | 10 ++ .../@tailwindcss-standalone/src/types.d.ts | 3 + packages/@tailwindcss-vite/src/index.ts | 2 +- .../@tailwindcss-vite/src/normalize-path.ts | 47 ++++++++ packages/@tailwindcss-vite/tsup.config.ts | 1 - pnpm-lock.yaml | 16 --- 10 files changed, 92 insertions(+), 109 deletions(-) create mode 100644 packages/@tailwindcss-vite/src/normalize-path.ts diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json index e36b6e2ada6c..c8dbf8cce678 100644 --- a/packages/@tailwindcss-cli/package.json +++ b/packages/@tailwindcss-cli/package.json @@ -36,12 +36,6 @@ "lightningcss": "catalog:", "mri": "^1.2.0", "picocolors": "^1.0.1", - "postcss-import": "^16.1.0", - "postcss": "^8.4.41", "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@types/postcss-import": "^14.0.3", - "internal-postcss-fix-relative-paths": "workspace:^" } } diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 3ca7f4ad0367..daf12294cf74 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -2,13 +2,10 @@ import watcher from '@parcel/watcher' import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner, type ChangedContent } from '@tailwindcss/oxide' -import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' -import { existsSync, readFileSync } from 'node:fs' +import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' -import postcss from 'postcss' -import atImport from 'postcss-import' import type { Arg, Result } from '../../utils/args' import { Disposables } from '../../utils/disposables' import { @@ -19,7 +16,6 @@ import { println, relative, } from '../../utils/renderer' -import { resolveCssId } from '../../utils/resolve' import { drainStdin, outputFile } from './utils' const css = String.raw @@ -83,17 +79,13 @@ export async function handle(args: Result>) { let start = process.hrtime.bigint() - // Resolve the input - let [input, cssImportPaths] = await handleImports( - args['--input'] - ? args['--input'] === '-' - ? await drainStdin() - : await fs.readFile(args['--input'], 'utf-8') - : css` - @import 'tailwindcss'; - `, - args['--input'] ?? base, - ) + let input = args['--input'] + ? args['--input'] === '-' + ? await drainStdin() + : await fs.readFile(args['--input'], 'utf-8') + : css` + @import 'tailwindcss'; + ` let previous = { css: '', @@ -128,7 +120,7 @@ export async function handle(args: Result>) { let inputFile = args['--input'] && args['--input'] !== '-' ? args['--input'] : process.cwd() let inputBasePath = path.dirname(path.resolve(inputFile)) - let fullRebuildPaths: string[] = cssImportPaths.slice() + let fullRebuildPaths: string[] = [] function createCompiler(css: string) { return compile(css, { @@ -143,12 +135,7 @@ export async function handle(args: Result>) { let compiler = await createCompiler(input) let scanner = new Scanner({ detectSources: { base }, - sources: compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the config file - // where it is specified. - base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, - pattern, - })), + sources: compiler.globs, }) // Watch for changes @@ -196,17 +183,16 @@ export async function handle(args: Result>) { // Clear all watchers cleanupWatchers() - // Collect the new `input` and `cssImportPaths`. - ;[input, cssImportPaths] = await handleImports( - args['--input'] - ? await fs.readFile(args['--input'], 'utf-8') - : css` - @import 'tailwindcss'; - `, - args['--input'] ?? base, - ) + // Read the new `input`. + let input = args['--input'] + ? args['--input'] === '-' + ? await drainStdin() + : await fs.readFile(args['--input'], 'utf-8') + : css` + @import 'tailwindcss'; + ` clearRequireCache(resolvedFullRebuildPaths) - fullRebuildPaths = cssImportPaths.slice() + fullRebuildPaths = [] // Create a new compiler, given the new `input` compiler = await createCompiler(input) @@ -214,12 +200,7 @@ export async function handle(args: Result>) { // Re-scan the directory to get the new `candidates` scanner = new Scanner({ detectSources: { base }, - sources: compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the - // config file where it is specified. - base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, - pattern, - })), + sources: compiler.globs, }) // Scan the directory for candidates @@ -367,51 +348,6 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { } } -function handleImports( - input: string, - file: string, -): [css: string, paths: string[]] | Promise<[css: string, paths: string[]]> { - // TODO: Should we implement this ourselves instead of relying on PostCSS? - // - // Relevant specification: - // - CSS Import Resolve: https://csstools.github.io/css-import-resolve/ - - if (!input.includes('@import')) { - return [input, [file]] - } - - return postcss() - .use( - atImport({ - resolve(id, basedir) { - let resolved = resolveCssId(id, basedir) - if (!resolved) { - throw new Error(`Could not resolve ${id} from ${basedir}`) - } - return resolved - }, - load(id) { - // We need to synchronously read the file here because when bundled - // with bun, some of the ids might resolve to files inside the bun - // embedded files root which can only be read by `node:fs` and not - // `node:fs/promises`. - return readFileSync(id, 'utf-8') - }, - }), - ) - .use(fixRelativePathsPlugin()) - .process(input, { from: file }) - .then((result) => [ - result.css, - - // Use `result.messages` to get the imported files. This also includes the - // current file itself. - [file].concat( - result.messages.filter((msg) => msg.type === 'dependency').map((msg) => msg.file), - ), - ]) -} - function optimizeCss( input: string, { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, diff --git a/packages/@tailwindcss-cli/tsup.config.ts b/packages/@tailwindcss-cli/tsup.config.ts index 23628127060d..7d82eee2c882 100644 --- a/packages/@tailwindcss-cli/tsup.config.ts +++ b/packages/@tailwindcss-cli/tsup.config.ts @@ -5,5 +5,4 @@ export default defineConfig({ clean: true, minify: true, entry: ['src/index.ts'], - noExternal: ['internal-postcss-fix-relative-paths'], }) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index fd64e7c4bafc..cdd0d7255e4e 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -48,6 +48,17 @@ export async function compile( async loadStylesheet(id, basedir) { let resolvedPath = await resolveCssId(id, basedir) if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${basedir}'`) + + if (typeof globalThis.__tw_readFile === 'function') { + let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8') + if (file) { + return { + base: path.dirname(resolvedPath), + content: file, + } + } + } + let file = await fsPromises.readFile(resolvedPath, 'utf-8') return { base: path.dirname(resolvedPath), diff --git a/packages/@tailwindcss-standalone/src/index.ts b/packages/@tailwindcss-standalone/src/index.ts index 5dfefaf82928..ae90e2b6efc7 100644 --- a/packages/@tailwindcss-standalone/src/index.ts +++ b/packages/@tailwindcss-standalone/src/index.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' import packageJson from 'tailwindcss/package.json' @@ -42,5 +43,14 @@ globalThis.__tw_resolve = (id, baseDir) => { } } globalThis.__tw_version = packageJson.version +globalThis.__tw_readFile = async (path, encoding) => { + // When reading a file from the `$bunfs`, we need to use the synchronous + // `readFileSync` API + let isEmbeddedFileBase = path.includes('/$bunfs/root') || path.includes(':/~BUN/root') + if (!isEmbeddedFileBase) { + return + } + return fs.readFileSync(path, encoding) +} await import('../../@tailwindcss-cli/src/index.ts') diff --git a/packages/@tailwindcss-standalone/src/types.d.ts b/packages/@tailwindcss-standalone/src/types.d.ts index dfdac2715c0c..e13e4e4275d2 100644 --- a/packages/@tailwindcss-standalone/src/types.d.ts +++ b/packages/@tailwindcss-standalone/src/types.d.ts @@ -5,3 +5,6 @@ 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) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index d69a6e11e15f..c5f61e10ea92 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -2,10 +2,10 @@ import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' -import { normalizePath } from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' import path from 'path' import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' +import { normalizePath } from './normalize-path' export default function tailwindcss(): Plugin[] { let servers: ViteDevServer[] = [] diff --git a/packages/@tailwindcss-vite/src/normalize-path.ts b/packages/@tailwindcss-vite/src/normalize-path.ts new file mode 100644 index 000000000000..a8184ef2317c --- /dev/null +++ b/packages/@tailwindcss-vite/src/normalize-path.ts @@ -0,0 +1,47 @@ +// Inlined version of `normalize-path` +// Copyright (c) 2014-2018, Jon Schlinkert. +// Released under the MIT License. +function normalizePathBase(path: string, stripTrailing?: boolean) { + if (typeof path !== 'string') { + throw new TypeError('expected path to be a string') + } + + if (path === '\\' || path === '/') return '/' + + var len = path.length + if (len <= 1) return path + + // ensure that win32 namespaces has two leading slashes, so that the path is + // handled properly by the win32 version of path.parse() after being normalized + // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces + var prefix = '' + if (len > 4 && path[3] === '\\') { + var ch = path[2] + if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') { + path = path.slice(2) + prefix = '//' + } + } + + var segs = path.split(/[/\\]+/) + if (stripTrailing !== false && segs[segs.length - 1] === '') { + segs.pop() + } + return prefix + segs.join('/') +} + +export function normalizePath(originalPath: string) { + let normalized = normalizePathBase(originalPath) + + // Make sure Windows network share paths are normalized properly + // They have to begin with two slashes or they won't resolve correctly + if ( + originalPath.startsWith('\\\\') && + normalized.startsWith('/') && + !normalized.startsWith('//') + ) { + return `/${normalized}` + } + + return normalized +} diff --git a/packages/@tailwindcss-vite/tsup.config.ts b/packages/@tailwindcss-vite/tsup.config.ts index eaf99e82abae..85bf3149d3f9 100644 --- a/packages/@tailwindcss-vite/tsup.config.ts +++ b/packages/@tailwindcss-vite/tsup.config.ts @@ -6,5 +6,4 @@ export default defineConfig({ minify: true, dts: true, entry: ['src/index.ts'], - noExternal: ['internal-postcss-fix-relative-paths'], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 204cbb23fc8b..ce37a8fe3783 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,22 +156,9 @@ importers: picocolors: specifier: ^1.0.1 version: 1.0.1 - postcss: - specifier: ^8.4.41 - version: 8.4.41 - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) tailwindcss: specifier: workspace:^ version: link:../tailwindcss - devDependencies: - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 - internal-postcss-fix-relative-paths: - specifier: workspace:^ - version: link:../internal-postcss-fix-relative-paths packages/@tailwindcss-node: dependencies: @@ -330,9 +317,6 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 - internal-postcss-fix-relative-paths: - specifier: workspace:^ - version: link:../internal-postcss-fix-relative-paths vite: specifier: 'catalog:' version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) From 3d563c6cfdd9ca77b53e1616a5112782f9084da8 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:10:36 +0200 Subject: [PATCH 20/51] Fix linter issues --- .../src/fixtures/example-project/src/relative-import.css | 2 +- playgrounds/vite/src/app.scss | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css index 088dd2a05919..5dfb60b16219 100644 --- a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css @@ -1,4 +1,4 @@ /* @plugin '../plugin.js'; */ .foo { - @apply text-red-500 + @apply text-red-500; } diff --git a/playgrounds/vite/src/app.scss b/playgrounds/vite/src/app.scss index aeefee952e89..5b8b9e575d82 100644 --- a/playgrounds/vite/src/app.scss +++ b/playgrounds/vite/src/app.scss @@ -1,5 +1,5 @@ -@use "sass:list"; -@use "sass:color"; +@use 'sass:list'; +@use 'sass:color'; @import 'tailwindcss/index.css'; @tailwind utilities; @@ -448,7 +448,6 @@ } @keyframes ping { - 75%, 100% { transform: scale(2); @@ -463,7 +462,6 @@ } @keyframes bounce { - 0%, 100% { transform: translateY(-25%); From d57dcf37c6c1ec2a93034e8eae1ff49e23907d29 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:11:42 +0200 Subject: [PATCH 21/51] Remove accidentially commited file --- playgrounds/vite/src/app.scss | 493 ---------------------------------- 1 file changed, 493 deletions(-) delete mode 100644 playgrounds/vite/src/app.scss diff --git a/playgrounds/vite/src/app.scss b/playgrounds/vite/src/app.scss deleted file mode 100644 index 5b8b9e575d82..000000000000 --- a/playgrounds/vite/src/app.scss +++ /dev/null @@ -1,493 +0,0 @@ -@use 'sass:list'; -@use 'sass:color'; - -@import 'tailwindcss/index.css'; -@tailwind utilities; -@theme default { - /* Defaults */ - --default-transition-duration: 150ms; - --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - --default-font-family: var(--font-family-sans); - --default-font-feature-settings: var(--font-family-sans--font-feature-settings); - --default-font-variation-settings: var(--font-family-sans--font-variation-settings); - --default-mono-font-family: var(--font-family-mono); - --default-mono-font-feature-settings: var(--font-family-mono--font-feature-settings); - --default-mono-font-variation-settings: var(--font-family-mono--font-variation-settings); - - /* Breakpoints */ - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 64rem; - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - - /* Colors */ - --color-black: #000; - --color-white: #fff; - --color-slate-50: #f8fafc; - --color-slate-100: #f1f5f9; - --color-slate-200: #e2e8f0; - --color-slate-300: #cbd5e1; - --color-slate-400: #94a3b8; - --color-slate-500: #64748b; - --color-slate-600: #475569; - --color-slate-700: #334155; - --color-slate-800: #1e293b; - --color-slate-900: #0f172a; - --color-slate-950: #020617; - --color-gray-50: #f9fafb; - --color-gray-100: #f3f4f6; - --color-gray-200: #e5e7eb; - --color-gray-300: #d1d5db; - --color-gray-400: #9ca3af; - --color-gray-500: #6b7280; - --color-gray-600: #4b5563; - --color-gray-700: #374151; - --color-gray-800: #1f2937; - --color-gray-900: #111827; - --color-gray-950: #030712; - --color-zinc-50: #fafafa; - --color-zinc-100: #f4f4f5; - --color-zinc-200: #e4e4e7; - --color-zinc-300: #d4d4d8; - --color-zinc-400: #a1a1aa; - --color-zinc-500: #71717a; - --color-zinc-600: #52525b; - --color-zinc-700: #3f3f46; - --color-zinc-800: #27272a; - --color-zinc-900: #18181b; - --color-zinc-950: #09090b; - --color-neutral-50: #fafafa; - --color-neutral-100: #f5f5f5; - --color-neutral-200: #e5e5e5; - --color-neutral-300: #d4d4d4; - --color-neutral-400: #a3a3a3; - --color-neutral-500: #737373; - --color-neutral-600: #525252; - --color-neutral-700: #404040; - --color-neutral-800: #262626; - --color-neutral-900: #171717; - --color-neutral-950: #0a0a0a; - --color-stone-50: #fafaf9; - --color-stone-100: #f5f5f4; - --color-stone-200: #e7e5e4; - --color-stone-300: #d6d3d1; - --color-stone-400: #a8a29e; - --color-stone-500: #78716c; - --color-stone-600: #57534e; - --color-stone-700: #44403c; - --color-stone-800: #292524; - --color-stone-900: #1c1917; - --color-stone-950: #0c0a09; - --color-red-50: #fef2f2; - --color-red-100: #fee2e2; - --color-red-200: #fecaca; - --color-red-300: #fca5a5; - --color-red-400: #f87171; - --color-red-500: #ef4444; - --color-red-600: #dc2626; - --color-red-700: #b91c1c; - --color-red-800: #991b1b; - --color-red-900: #7f1d1d; - --color-red-950: #450a0a; - --color-orange-50: #fff7ed; - --color-orange-100: #ffedd5; - --color-orange-200: #fed7aa; - --color-orange-300: #fdba74; - --color-orange-400: #fb923c; - --color-orange-500: #f97316; - --color-orange-600: #ea580c; - --color-orange-700: #c2410c; - --color-orange-800: #9a3412; - --color-orange-900: #7c2d12; - --color-orange-950: #431407; - --color-amber-50: #fffbeb; - --color-amber-100: #fef3c7; - --color-amber-200: #fde68a; - --color-amber-300: #fcd34d; - --color-amber-400: #fbbf24; - --color-amber-500: #f59e0b; - --color-amber-600: #d97706; - --color-amber-700: #b45309; - --color-amber-800: #92400e; - --color-amber-900: #78350f; - --color-amber-950: #451a03; - --color-yellow-50: #fefce8; - --color-yellow-100: #fef9c3; - --color-yellow-200: #fef08a; - --color-yellow-300: #fde047; - --color-yellow-400: #facc15; - --color-yellow-500: #eab308; - --color-yellow-600: #ca8a04; - --color-yellow-700: #a16207; - --color-yellow-800: #854d0e; - --color-yellow-900: #713f12; - --color-yellow-950: #422006; - --color-lime-50: #f7fee7; - --color-lime-100: #ecfccb; - --color-lime-200: #d9f99d; - --color-lime-300: #bef264; - --color-lime-400: #a3e635; - --color-lime-500: #84cc16; - --color-lime-600: #65a30d; - --color-lime-700: #4d7c0f; - --color-lime-800: #3f6212; - --color-lime-900: #365314; - --color-lime-950: #1a2e05; - --color-green-50: #f0fdf4; - --color-green-100: #dcfce7; - --color-green-200: #bbf7d0; - --color-green-300: #86efac; - --color-green-400: #4ade80; - --color-green-500: #22c55e; - --color-green-600: #16a34a; - --color-green-700: #15803d; - --color-green-800: #166534; - --color-green-900: #14532d; - --color-green-950: #052e16; - --color-emerald-50: #ecfdf5; - --color-emerald-100: #d1fae5; - --color-emerald-200: #a7f3d0; - --color-emerald-300: #6ee7b7; - --color-emerald-400: #34d399; - --color-emerald-500: #10b981; - --color-emerald-600: #059669; - --color-emerald-700: #047857; - --color-emerald-800: #065f46; - --color-emerald-900: #064e3b; - --color-emerald-950: #022c22; - --color-teal-50: #f0fdfa; - --color-teal-100: #ccfbf1; - --color-teal-200: #99f6e4; - --color-teal-300: #5eead4; - --color-teal-400: #2dd4bf; - --color-teal-500: #14b8a6; - --color-teal-600: #0d9488; - --color-teal-700: #0f766e; - --color-teal-800: #115e59; - --color-teal-900: #134e4a; - --color-teal-950: #042f2e; - --color-cyan-50: #ecfeff; - --color-cyan-100: #cffafe; - --color-cyan-200: #a5f3fc; - --color-cyan-300: #67e8f9; - --color-cyan-400: #22d3ee; - --color-cyan-500: #06b6d4; - --color-cyan-600: #0891b2; - --color-cyan-700: #0e7490; - --color-cyan-800: #155e75; - --color-cyan-900: #164e63; - --color-cyan-950: #083344; - --color-sky-50: #f0f9ff; - --color-sky-100: #e0f2fe; - --color-sky-200: #bae6fd; - --color-sky-300: #7dd3fc; - --color-sky-400: #38bdf8; - --color-sky-500: #0ea5e9; - --color-sky-600: #0284c7; - --color-sky-700: #0369a1; - --color-sky-800: #075985; - --color-sky-900: #0c4a6e; - --color-sky-950: #082f49; - --color-blue-50: #eff6ff; - --color-blue-100: #dbeafe; - --color-blue-200: #bfdbfe; - --color-blue-300: #93c5fd; - --color-blue-400: #60a5fa; - --color-blue-500: #3b82f6; - --color-blue-600: #2563eb; - --color-blue-700: #1d4ed8; - --color-blue-800: #1e40af; - --color-blue-900: #1e3a8a; - --color-blue-950: #172554; - --color-indigo-50: #eef2ff; - --color-indigo-100: #e0e7ff; - --color-indigo-200: #c7d2fe; - --color-indigo-300: #a5b4fc; - --color-indigo-400: #818cf8; - --color-indigo-500: #6366f1; - --color-indigo-600: #4f46e5; - --color-indigo-700: #4338ca; - --color-indigo-800: #3730a3; - --color-indigo-900: #312e81; - --color-indigo-950: #1e1b4b; - --color-violet-50: #f5f3ff; - --color-violet-100: #ede9fe; - --color-violet-200: #ddd6fe; - --color-violet-300: #c4b5fd; - --color-violet-400: #a78bfa; - --color-violet-500: #8b5cf6; - --color-violet-600: #7c3aed; - --color-violet-700: #6d28d9; - --color-violet-800: #5b21b6; - --color-violet-900: #4c1d95; - --color-violet-950: #2e1065; - --color-purple-50: #faf5ff; - --color-purple-100: #f3e8ff; - --color-purple-200: #e9d5ff; - --color-purple-300: #d8b4fe; - --color-purple-400: #c084fc; - --color-purple-500: #a855f7; - --color-purple-600: #9333ea; - --color-purple-700: #7e22ce; - --color-purple-800: #6b21a8; - --color-purple-900: #581c87; - --color-purple-950: #3b0764; - --color-fuchsia-50: #fdf4ff; - --color-fuchsia-100: #fae8ff; - --color-fuchsia-200: #f5d0fe; - --color-fuchsia-300: #f0abfc; - --color-fuchsia-400: #e879f9; - --color-fuchsia-500: #d946ef; - --color-fuchsia-600: #c026d3; - --color-fuchsia-700: #a21caf; - --color-fuchsia-800: #86198f; - --color-fuchsia-900: #701a75; - --color-fuchsia-950: #4a044e; - --color-pink-50: #fdf2f8; - --color-pink-100: #fce7f3; - --color-pink-200: #fbcfe8; - --color-pink-300: #f9a8d4; - --color-pink-400: #f472b6; - --color-pink-500: #ec4899; - --color-pink-600: #db2777; - --color-pink-700: #be185d; - --color-pink-800: #9d174d; - --color-pink-900: #831843; - --color-pink-950: #500724; - --color-rose-50: #fff1f2; - --color-rose-100: #ffe4e6; - --color-rose-200: #fecdd3; - --color-rose-300: #fda4af; - --color-rose-400: #fb7185; - --color-rose-500: #f43f5e; - --color-rose-600: #e11d48; - --color-rose-700: #be123c; - --color-rose-800: #9f1239; - --color-rose-900: #881337; - --color-rose-950: #4c0519; - - /* Animations */ - --animate-spin: spin 1s linear infinite; - --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; - --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - --animate-bounce: bounce 1s infinite; - - /* Blurs */ - --blur: 8px; - --blur-sm: 4px; - --blur-md: 12px; - --blur-lg: 16px; - --blur-xl: 24px; - --blur-2xl: 40px; - --blur-3xl: 64px; - - /* Radii */ - --radius: 0.25rem; - --radius-sm: 0.125rem; - --radius-md: 0.375rem; - --radius-lg: 0.5rem; - --radius-xl: 0.75rem; - --radius-2xl: 1rem; - --radius-3xl: 1.5rem; - --radius-4xl: 2rem; - - /* Shadows */ - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-xs: 0 1px rgb(0 0 0 / 0.05); - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25); - --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); - - /* Inset shadows */ - --inset-shadow-xs: inset 0 1px rgb(0 0 0 / 0.05); - --inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05); - --inset-shadow: inset 0 2px 4px rgb(0 0 0 / 0.05); - - /* Drop shadows */ - --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06); - --drop-shadow-sm: 0 1px 1px rgb(0 0 0 / 0.05); - --drop-shadow-md: 0 4px 3px rgb(0 0 0 / 0.07), 0 2px 2px rgb(0 0 0 / 0.06); - --drop-shadow-lg: 0 10px 8px rgb(0 0 0 / 0.04), 0 4px 3px rgb(0 0 0 / 0.1); - --drop-shadow-xl: 0 20px 13px rgb(0 0 0 / 0.03), 0 8px 5px rgb(0 0 0 / 0.08); - --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15); - --drop-shadow-none: 0 0 #0000; - - /* Spacing */ - --spacing-px: 1px; - --spacing-0: 0px; - --spacing-0_5: 0.125rem; - --spacing-1: 0.25rem; - --spacing-1_5: 0.375rem; - --spacing-2: 0.5rem; - --spacing-2_5: 0.625rem; - --spacing-3: 0.75rem; - --spacing-3_5: 0.875rem; - --spacing-4: 1rem; - --spacing-5: 1.25rem; - --spacing-6: 1.5rem; - --spacing-7: 1.75rem; - --spacing-8: 2rem; - --spacing-9: 2.25rem; - --spacing-10: 2.5rem; - --spacing-11: 2.75rem; - --spacing-12: 3rem; - --spacing-14: 3.5rem; - --spacing-16: 4rem; - --spacing-20: 5rem; - --spacing-24: 6rem; - --spacing-28: 7rem; - --spacing-32: 8rem; - --spacing-36: 9rem; - --spacing-40: 10rem; - --spacing-44: 11rem; - --spacing-48: 12rem; - --spacing-52: 13rem; - --spacing-56: 14rem; - --spacing-60: 15rem; - --spacing-64: 16rem; - --spacing-72: 18rem; - --spacing-80: 20rem; - --spacing-96: 24rem; - - /* Widths */ - --width-3xs: 16rem; - --width-2xs: 18rem; - --width-xs: 20rem; - --width-sm: 24rem; - --width-md: 28rem; - --width-lg: 32rem; - --width-xl: 36rem; - --width-2xl: 42rem; - --width-3xl: 48rem; - --width-4xl: 56rem; - --width-5xl: 64rem; - --width-6xl: 72rem; - --width-7xl: 80rem; - --width-prose: 65ch; - - /* Fonts */ - --font-family-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-family-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; - --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', - 'Courier New', monospace; - - /* Type scale */ - --font-size-xs: 0.75rem; - --font-size-xs--line-height: 1rem; - --font-size-sm: 0.875rem; - --font-size-sm--line-height: 1.25rem; - --font-size-base: 1rem; - --font-size-base--line-height: 1.5rem; - --font-size-lg: 1.125rem; - --font-size-lg--line-height: 1.75rem; - --font-size-xl: 1.25rem; - --font-size-xl--line-height: 1.75rem; - --font-size-2xl: 1.5rem; - --font-size-2xl--line-height: 2rem; - --font-size-3xl: 1.875rem; - --font-size-3xl--line-height: 2.25rem; - --font-size-4xl: 2.25rem; - --font-size-4xl--line-height: 2.5rem; - --font-size-5xl: 3rem; - --font-size-5xl--line-height: 1; - --font-size-6xl: 3.75rem; - --font-size-6xl--line-height: 1; - --font-size-7xl: 4.5rem; - --font-size-7xl--line-height: 1; - --font-size-8xl: 6rem; - --font-size-8xl--line-height: 1; - --font-size-9xl: 8rem; - --font-size-9xl--line-height: 1; - - /* Letter spacing */ - --letter-spacing-tighter: -0.05em; - --letter-spacing-tight: -0.025em; - --letter-spacing-normal: 0em; - --letter-spacing-wide: 0.025em; - --letter-spacing-wider: 0.05em; - --letter-spacing-widest: 0.1em; - - /* Line-height */ - --line-height-none: 1; - --line-height-tight: 1.25; - --line-height-snug: 1.375; - --line-height-normal: 1.5; - --line-height-relaxed: 1.625; - --line-height-loose: 2; - --line-height-3: 0.75rem; - --line-height-4: 1rem; - --line-height-5: 1.25rem; - --line-height-6: 1.5rem; - --line-height-7: 1.75rem; - --line-height-8: 2rem; - --line-height-9: 2.25rem; - --line-height-10: 2.5rem; - - /* 3D perspectives */ - --perspective-dramatic: 100px; - --perspective-near: 300px; - --perspective-normal: 500px; - --perspective-midrange: 800px; - --perspective-distant: 1200px; - - /* Transition timing functions */ - --transition-timing-function-linear: linear; - --transition-timing-function-in: cubic-bezier(0.4, 0, 1, 1); - --transition-timing-function-out: cubic-bezier(0, 0, 0.2, 1); - --transition-timing-function-in-out: cubic-bezier(0.4, 0, 0.2, 1); - - @keyframes spin { - to { - transform: rotate(360deg); - } - } - - @keyframes ping { - 75%, - 100% { - transform: scale(2); - opacity: 0; - } - } - - @keyframes pulse { - 50% { - opacity: 0.5; - } - } - - @keyframes bounce { - 0%, - 100% { - transform: translateY(-25%); - animation-timing-function: cubic-bezier(0.8, 0, 1, 1); - } - - 50% { - transform: none; - animation-timing-function: cubic-bezier(0, 0, 0.2, 1); - } - } -} -@plugin "./plugin.js"; - -$font-stack: Helvetica, Arial; -$primary-color: #333; - -body { - $font-stack: list.append($font-stack, sans-serif); - font: $font-stack; -} - -a { - color: $primary-color; - - &:hover { - color: color.scale($primary-color, $lightness: 20%); - } -} From d8f5b0a735d485b74962c7406dd5e1cef2524d48 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:14:51 +0200 Subject: [PATCH 22/51] Use async resolver APIs to avoid blocking core --- packages/@tailwindcss-node/src/compile.ts | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index cdd0d7255e4e..69066f9f9e19 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -14,7 +14,7 @@ export async function compile( return await _compile(css, base, { async loadModule(id, base) { if (id[0] !== '.') { - let resolvedPath = resolveJsId(id, base) + let resolvedPath = await resolveJsId(id, base) if (!resolvedPath) { throw new Error(`Could not resolve '${id}' from '${base}'`) } @@ -26,7 +26,7 @@ export async function compile( } } - let resolvedPath = resolveJsId(id, base) + let resolvedPath = await resolveJsId(id, base) if (!resolvedPath) { throw new Error(`Could not resolve '${id}' from '${base}'`) } @@ -91,21 +91,31 @@ const cssResolver = EnhancedResolve.ResolverFactory.createResolver({ mainFields: ['style'], conditionNames: ['style'], }) -function resolveCssId(id: string, base: string) { +function resolveCssId(id: string, base: string): Promise { if (typeof globalThis.__tw_resolve === 'function') { let resolved = globalThis.__tw_resolve(id, base) if (resolved) { - return resolved + return Promise.resolve(resolved) } } - return cssResolver.resolveSync({}, base, id) + return new Promise((resolve, reject) => + cssResolver.resolve({}, base, id, {}, (err, result) => { + if (err) return reject(err) + resolve(result) + }), + ) } const jsResolver = EnhancedResolve.ResolverFactory.createResolver({ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), useSyncFileSystemCalls: true, }) -function resolveJsId(id: string, base: string) { - return jsResolver.resolveSync({}, base, id) +function resolveJsId(id: string, base: string): Promise { + return new Promise((resolve, reject) => + jsResolver.resolve({}, base, id, {}, (err, result) => { + if (err) return reject(err) + resolve(result) + }), + ) } From 71df1a72c3185a4fa6d779b6301044f5618ddcfa Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:19:29 +0200 Subject: [PATCH 23/51] Remove @tailwindcss/internal-postcss-fix-relative-paths --- packages/@tailwindcss-postcss/package.json | 5 ++-- packages/@tailwindcss-postcss/src/index.ts | 2 +- .../fixtures/example-project/src/index.css | 0 .../fixtures/example-project/src/invalid.css | 0 .../fixtures/external-import/src/index.css | 0 .../fixtures/external-import/src/invalid.css | 0 .../external-import/src/plugins-in-root.css | 0 .../src/plugins-in-sibling.css | 0 .../postcss-fix-relative-paths}/index.test.ts | 0 .../src/postcss-fix-relative-paths}/index.ts | 0 .../normalize-path.ts | 0 packages/@tailwindcss-postcss/tsup.config.ts | 2 -- .../package.json | 27 ------------------- .../tsconfig.json | 3 --- pnpm-lock.yaml | 24 +++++------------ 15 files changed, 10 insertions(+), 53 deletions(-) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/example-project/src/index.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/example-project/src/invalid.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/index.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/invalid.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/plugins-in-root.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/plugins-in-sibling.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/index.test.ts (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/index.ts (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/normalize-path.ts (100%) delete mode 100644 packages/internal-postcss-fix-relative-paths/package.json delete mode 100644 packages/internal-postcss-fix-relative-paths/tsconfig.json diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index 6c167243d9d6..11da052a9e60 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -38,7 +38,8 @@ "devDependencies": { "@types/node": "catalog:", "postcss": "^8.4.41", - "internal-example-plugin": "workspace:*", - "internal-postcss-fix-relative-paths": "workspace:^" + "postcss-import": "^16.1.0", + "@types/postcss-import": "14.0.3", + "internal-example-plugin": "workspace:*" } } diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 32db243895b5..881445a8016e 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -2,10 +2,10 @@ import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import fs from 'fs' -import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' import path from 'path' import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' +import fixRelativePathsPlugin from './postcss-fix-relative-paths' /** * A Map that can generate default values for keys that don't exist. diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/invalid.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/invalid.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/index.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/index.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/invalid.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/invalid.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-root.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-root.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-sibling.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-sibling.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css diff --git a/packages/internal-postcss-fix-relative-paths/src/index.test.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/index.test.ts rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts diff --git a/packages/internal-postcss-fix-relative-paths/src/index.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/index.ts rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts diff --git a/packages/internal-postcss-fix-relative-paths/src/normalize-path.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/normalize-path.ts similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/normalize-path.ts rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/normalize-path.ts diff --git a/packages/@tailwindcss-postcss/tsup.config.ts b/packages/@tailwindcss-postcss/tsup.config.ts index 76e4fc03b0a0..684c072ac854 100644 --- a/packages/@tailwindcss-postcss/tsup.config.ts +++ b/packages/@tailwindcss-postcss/tsup.config.ts @@ -7,7 +7,6 @@ export default defineConfig([ cjsInterop: true, dts: true, entry: ['src/index.ts'], - noExternal: ['internal-postcss-fix-relative-paths'], }, { format: ['cjs'], @@ -15,6 +14,5 @@ export default defineConfig([ cjsInterop: true, dts: true, entry: ['src/index.cts'], - noExternal: ['internal-postcss-fix-relative-paths'], }, ]) diff --git a/packages/internal-postcss-fix-relative-paths/package.json b/packages/internal-postcss-fix-relative-paths/package.json deleted file mode 100644 index 893f7466d1d3..000000000000 --- a/packages/internal-postcss-fix-relative-paths/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "internal-postcss-fix-relative-paths", - "version": "0.0.0", - "private": true, - "scripts": { - "lint": "tsc --noEmit", - "build": "tsup-node ./src/index.ts --format cjs,esm --dts --cjsInterop --splitting --minify --clean", - "dev": "pnpm run build -- --watch" - }, - "files": [ - "dist/" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "dependencies": {}, - "devDependencies": { - "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3", - "postcss": "8.4.41", - "postcss-import": "^16.1.0" - } -} diff --git a/packages/internal-postcss-fix-relative-paths/tsconfig.json b/packages/internal-postcss-fix-relative-paths/tsconfig.json deleted file mode 100644 index 6ae022f65bf0..000000000000 --- a/packages/internal-postcss-fix-relative-paths/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../tsconfig.base.json", -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce37a8fe3783..eeaae1c5eacf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,15 +191,18 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 + '@types/postcss-import': + specifier: 14.0.3 + version: 14.0.3 internal-example-plugin: specifier: workspace:* version: link:../internal-example-plugin - internal-postcss-fix-relative-paths: - specifier: workspace:^ - version: link:../internal-postcss-fix-relative-paths postcss: specifier: ^8.4.41 version: 8.4.41 + postcss-import: + specifier: ^16.1.0 + version: 16.1.0(postcss@8.4.41) packages/@tailwindcss-standalone: dependencies: @@ -323,21 +326,6 @@ importers: packages/internal-example-plugin: {} - packages/internal-postcss-fix-relative-paths: - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 20.14.13 - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 - postcss: - specifier: 8.4.41 - version: 8.4.41 - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) - packages/tailwindcss: devDependencies: '@tailwindcss/oxide': From cff95a71c81017a5f5f43ff9f410a489fec59001 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:21:51 +0200 Subject: [PATCH 24/51] Fix type error in plugin-api tests --- packages/tailwindcss/src/compat/plugin-api.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 8ebfebb6d25f..4fd183e472e3 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -3,16 +3,12 @@ import { compile as coreCompile } from '..' import plugin from '../plugin' import { optimizeCss } from '../test-utils/run' import defaultTheme from './default-theme' -import type { CssInJs, PluginAPI, PluginWithConfig } from './plugin-api' +import type { CssInJs, Plugin, PluginAPI } from './plugin-api' const css = String.raw // TODO: Expand the API changes into the tests below -function compile( - css: string, - base: string, - { loadPlugin }: { loadPlugin: () => Promise }, -) { +function compile(css: string, base: string, { loadPlugin }: { loadPlugin: () => Promise }) { return coreCompile(css, base, { async loadModule(id, base) { let plugin = await loadPlugin() From edb1d47da3706a53152c83846d80888b618ba21a Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 15:56:26 +0200 Subject: [PATCH 25/51] Fix TypeScript issues --- packages/tailwindcss/src/at-import.test.ts | 6 +- .../src/compat/apply-config-to-theme.test.ts | 1 + packages/tailwindcss/src/index.bench.ts | 9 +- packages/tailwindcss/src/utilities.test.ts | 193 ++++++++++-------- packages/tailwindcss/tests/ui.spec.ts | 29 +-- 5 files changed, 138 insertions(+), 100 deletions(-) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 7754b2239791..46c245315130 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,4 +1,5 @@ import { expect, test, vi } from 'vitest' +import type { Plugin } from './compat/plugin-api' import { compile, type Config } from './index' import plugin from './plugin' import { optimizeCss } from './test-utils/run' @@ -8,7 +9,10 @@ let css = String.raw async function run( css: string, loadStylesheet: (id: string, base: string) => Promise<{ content: string; base: string }>, - loadModule: (id: string, base: string) => Promise<{ module: unknown; base: string }> = () => + loadModule: ( + id: string, + base: string, + ) => Promise<{ module: Config | Plugin; base: string }> = () => Promise.reject(new Error('Unexpected module')), candidates: string[] = [], ) { diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index af511a25e32f..aad2753b021d 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -35,6 +35,7 @@ test('Config values can be merged into the theme', () => { }, }, }, + base: '/root', }, ]) applyConfigToTheme(design, resolvedUserConfig) diff --git a/packages/tailwindcss/src/index.bench.ts b/packages/tailwindcss/src/index.bench.ts index dcffb522e3f2..1dd1b27f71bf 100644 --- a/packages/tailwindcss/src/index.bench.ts +++ b/packages/tailwindcss/src/index.bench.ts @@ -10,9 +10,12 @@ bench('compile', async () => { let scanner = new Scanner({ detectSources: { base: root } }) let candidates = scanner.scan() - let { build } = await compile(css` - @tailwind utilities; - `) + let { build } = await compile( + css` + @tailwind utilities; + `, + root, + ) build(candidates) }) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 20dbe123d22b..a8c1a549a721 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -15373,20 +15373,23 @@ test('@container', async () => { describe('custom utilities', () => { test('custom static utility', async () => { - let { build } = await compile(css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile( + css` + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; - } + @theme reference { + --breakpoint-lg: 1024px; + } - @utility text-trim { - text-box-trim: both; - text-box-edge: cap alphabetic; - } - `) + @utility text-trim { + text-box-trim: both; + text-box-edge: cap alphabetic; + } + `, + '/root', + ) let compiled = build(['text-trim', 'lg:text-trim']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15407,20 +15410,23 @@ describe('custom utilities', () => { }) test('Multiple static utilities are merged', async () => { - let { build } = await compile(css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile( + css` + @layer utilities { + @tailwind utilities; + } - @utility really-round { - --custom-prop: hi; - border-radius: 50rem; - } + @utility really-round { + --custom-prop: hi; + border-radius: 50rem; + } - @utility really-round { - border-radius: 30rem; - } - `) + @utility really-round { + border-radius: 30rem; + } + `, + '/root', + ) let compiled = build(['really-round']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15434,19 +15440,22 @@ describe('custom utilities', () => { }) test('custom utilities support some special characters', async () => { - let { build } = await compile(css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile( + css` + @layer utilities { + @tailwind utilities; + } - @utility push-1/2 { - right: 50%; - } + @utility push-1/2 { + right: 50%; + } - @utility push-50% { - right: 50%; - } - `) + @utility push-50% { + right: 50%; + } + `, + '/root', + ) let compiled = build(['push-1/2', 'push-50%']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15459,22 +15468,25 @@ describe('custom utilities', () => { }) test('can override specific versions of a functional utility with a static utility', async () => { - let { build } = await compile(css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile( + css` + @layer utilities { + @tailwind utilities; + } - @theme reference { - --font-size-sm: 0.875rem; - --font-size-sm--line-height: 1.25rem; - } + @theme reference { + --font-size-sm: 0.875rem; + --font-size-sm--line-height: 1.25rem; + } - @utility text-sm { - font-size: var(--font-size-sm, 0.8755rem); - line-height: var(--font-size-sm--line-height, 1.255rem); - text-rendering: optimizeLegibility; - } - `) + @utility text-sm { + font-size: var(--font-size-sm, 0.8755rem); + line-height: var(--font-size-sm--line-height, 1.255rem); + text-rendering: optimizeLegibility; + } + `, + '/root', + ) let compiled = build(['text-sm']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15491,19 +15503,22 @@ describe('custom utilities', () => { }) test('can override the default value of a functional utility', async () => { - let { build } = await compile(css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile( + css` + @layer utilities { + @tailwind utilities; + } - @theme reference { - --radius-xl: 16px; - } + @theme reference { + --radius-xl: 16px; + } - @utility rounded { - border-radius: 50rem; - } - `) + @utility rounded { + border-radius: 50rem; + } + `, + '/root', + ) let compiled = build(['rounded', 'rounded-xl', 'rounded-[33px]']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15524,15 +15539,18 @@ describe('custom utilities', () => { }) test('custom utilities are sorted by used properties', async () => { - let { build } = await compile(css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile( + css` + @layer utilities { + @tailwind utilities; + } - @utility push-left { - right: 100%; - } - `) + @utility push-left { + right: 100%; + } + `, + '/root', + ) let compiled = build(['top-[100px]', 'push-left', 'right-[100px]', 'bottom-[100px]']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15558,27 +15576,36 @@ describe('custom utilities', () => { test('custom utilities must use a valid name definitions', async () => { await expect(() => - compile(css` - @utility push-* { - right: 100%; - } - `), + compile( + css` + @utility push-* { + right: 100%; + } + `, + '/root', + ), ).rejects.toThrowError(/should be alphanumeric/) await expect(() => - compile(css` - @utility ~push { - right: 100%; - } - `), + compile( + css` + @utility ~push { + right: 100%; + } + `, + '/root', + ), ).rejects.toThrowError(/should be alphanumeric/) await expect(() => - compile(css` - @utility @push { - right: 100%; - } - `), + compile( + css` + @utility @push { + right: 100%; + } + `, + '/root', + ), ).rejects.toThrowError(/should be alphanumeric/) }) diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index b7580d2f310a..53575064e2b5 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -632,19 +632,22 @@ const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css') const defaultTheme = fs.readFileSync(path.resolve(__dirname, '..', 'theme.css'), 'utf-8') async function render(page: Page, content: string, extraCss: string = '') { - let { build } = await compile(css` - @layer theme, base, components, utilities; - @layer theme { - ${defaultTheme} - } - @layer base { - ${preflight} - } - @layer utilities { - @tailwind utilities; - } - ${extraCss} - `) + let { build } = await compile( + css` + @layer theme, base, components, utilities; + @layer theme { + ${defaultTheme} + } + @layer base { + ${preflight} + } + @layer utilities { + @tailwind utilities; + } + ${extraCss} + `, + '', + ) // We noticed that some of the tests depending on the `hover:` variant were // flaky. After some investigation, we found that injected elements had the From a11cd80ed425a1a25559c80a2e0057934ae3dbb1 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 16:06:14 +0200 Subject: [PATCH 26/51] Cleanup @import resolver --- packages/tailwindcss/src/at-import.ts | 39 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 8ee181d15b76..6c6850fe78da 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -11,50 +11,59 @@ export async function substituteAtImports( ) { let promises: Map> = new Map() + // Step 1: Find @import rules and start loading them in parallel walk(ast, (node) => { - // Find @import rules and start resolving them if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { let { uri } = parseImportParams(ValueParser.parse(node.selector.slice(8))) // Skip importing data URIs if (uri.startsWith('data:')) return - promises.set(key(uri, base), resolveAtImport(uri, base, loadStylesheet)) + let key = createKey(uri, base) + if (promises.has(key)) return + promises.set(key, atImport(uri, base, loadStylesheet)) } }) - let entries = [...promises.entries()] - let resolvers = entries.map(async ([id, promise]) => [id, await promise] as const) - let resolved = await Promise.all(resolvers) - let unwrapped = new Map(resolved) + if (promises.size === 0) return + let resolved = new Map( + await Promise.all( + Array.from(promises.entries()).map(async ([id, promise]) => [id, await promise] as const), + ), + ) + + // Step 2: Replace all @import rules with their resolved stylesheets walk(ast, (node, { replaceWith }) => { - // Replace all @import rules if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { let { uri, layer, media, supports } = parseImportParams( ValueParser.parse(node.selector.slice(8)), ) - let imported = unwrapped.get(key(uri, base)) + let key = createKey(uri, base) + let imported = resolved.get(key) if (imported) { replaceWith(buildImportNodes(imported.ast, imported.base, layer, media, supports)) + + // The resolved Stylesheets already have their transitive @imports + // resolved, so we can skip walking them. return WalkAction.Skip } } }) } -async function resolveAtImport( +async function atImport( id: string, - base: string, + parentBase: string, loadStylesheet: LoadStylesheet, ): Promise<{ ast: AstNode[]; base: string }> { - const { content, base: nestedBase } = await loadStylesheet(id, base) + const { content, base } = await loadStylesheet(id, parentBase) let ast = CSS.parse(content) - await substituteAtImports(ast, nestedBase, loadStylesheet) - return { ast, base: nestedBase } + await substituteAtImports(ast, base, loadStylesheet) + return { ast: [context({ base }, ast)], base } } -function key(id: string, basedir: string): string { +function createKey(id: string, basedir: string): string { return `${id}:${basedir}` } @@ -139,5 +148,5 @@ function buildImportNodes( root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] } - return [context({ base }, root)] + return root } From 4d60a61c664f46ee771a124b87961f5eb0a5da2c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 19 Sep 2024 16:27:56 +0200 Subject: [PATCH 27/51] Fix context node issue --- packages/tailwindcss/src/at-import.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 6c6850fe78da..786a34e12875 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -60,7 +60,7 @@ async function atImport( const { content, base } = await loadStylesheet(id, parentBase) let ast = CSS.parse(content) await substituteAtImports(ast, base, loadStylesheet) - return { ast: [context({ base }, ast)], base } + return { ast, base } } function createKey(id: string, basedir: string): string { @@ -148,5 +148,5 @@ function buildImportNodes( root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] } - return root + return [context({ base }, root)] } From 8cc914383bb477d4694f3cd3c1dd3b76d05889c6 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:05:27 +0200 Subject: [PATCH 28/51] Avoid second walk when resolving `@import` --- packages/tailwindcss/src/at-import.ts | 54 ++++++++++----------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 786a34e12875..9dba822cffee 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -9,47 +9,35 @@ export async function substituteAtImports( base: string, loadStylesheet: LoadStylesheet, ) { - let promises: Map> = new Map() + let promises: Promise[] = [] - // Step 1: Find @import rules and start loading them in parallel - walk(ast, (node) => { + walk(ast, (node, { replaceWith }) => { if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { - let { uri } = parseImportParams(ValueParser.parse(node.selector.slice(8))) + let { uri, layer, media, supports } = parseImportParams( + ValueParser.parse(node.selector.slice(8)), + ) // Skip importing data URIs if (uri.startsWith('data:')) return - let key = createKey(uri, base) - if (promises.has(key)) return - promises.set(key, atImport(uri, base, loadStylesheet)) - } - }) - - if (promises.size === 0) return + let contextNode = context({}, []) - let resolved = new Map( - await Promise.all( - Array.from(promises.entries()).map(async ([id, promise]) => [id, await promise] as const), - ), - ) - - // Step 2: Replace all @import rules with their resolved stylesheets - walk(ast, (node, { replaceWith }) => { - if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { - let { uri, layer, media, supports } = parseImportParams( - ValueParser.parse(node.selector.slice(8)), + promises.push( + (async () => { + let imported = await atImport(uri, base, loadStylesheet) + contextNode.nodes = buildImportNodes(imported.ast, imported.base, layer, media, supports) + contextNode.context.base = imported.base + })(), ) - let key = createKey(uri, base) - let imported = resolved.get(key) - if (imported) { - replaceWith(buildImportNodes(imported.ast, imported.base, layer, media, supports)) - - // The resolved Stylesheets already have their transitive @imports - // resolved, so we can skip walking them. - return WalkAction.Skip - } + + replaceWith(contextNode) + // The resolved Stylesheets already have their transitive @imports + // resolved, so we can skip walking them. + return WalkAction.Skip } }) + + await Promise.all(promises) } async function atImport( @@ -63,10 +51,6 @@ async function atImport( return { ast, base } } -function createKey(id: string, basedir: string): string { - return `${id}:${basedir}` -} - // c.f. https://github.com/postcss/postcss-import/blob/master/lib/parse-statements.js function parseImportParams(params: ValueParser.ValueAstNode[]) { let uri From 2529aa97af3057556a04d3c44cfc4fa04f813176 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:06:07 +0200 Subject: [PATCH 29/51] Don't check for `@import` before doing the `@import` walk --- packages/tailwindcss/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 1e5b718ac366..d6e4d2ee3ced 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -61,9 +61,7 @@ async function parseCss( ) { let ast = [context({ base }, CSS.parse(css))] as AstNode[] - if (css.includes('@import')) { - await substituteAtImports(ast, base, loadStylesheet) - } + await substituteAtImports(ast, base, loadStylesheet) // Find all `@theme` declarations let theme = new Theme() From 224ea668fc202f9523ce1b34ef9a8663f75ecddd Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:06:29 +0200 Subject: [PATCH 30/51] Remove leftover export --- packages/@tailwindcss-vite/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index c5f61e10ea92..189cdf36590a 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -315,7 +315,7 @@ class DefaultMap extends Map { } } -export class Root { +class Root { // Content is only used in serve mode where we need to capture the initial // contents of the root file so that we can restore it during the // `renderStart` hook. From 812482312a6248fec45531dfe5b60b55f675267c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:11:13 +0200 Subject: [PATCH 31/51] Make context nodes transparent in walks --- packages/tailwindcss/src/ast.ts | 11 +++++++++-- packages/tailwindcss/src/compat/apply-compat-hooks.ts | 4 ++-- packages/tailwindcss/src/index.ts | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 4ac91b23dcc9..461ddeef85df 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,4 +1,5 @@ export type Rule = { + replaceWith(contextNode: Context): unknown kind: 'rule' selector: string nodes: AstNode[] @@ -83,6 +84,14 @@ export function walk( for (let i = 0; i < ast.length; i++) { let node = ast[i] + // We want context nodes to be transparent in walks. This means that + // whenever we encounter one, we immediately walk through its children and + // furthermore we also don't update the parent. + if (node.kind === 'context') { + walk(node.nodes, visit, parent, { ...context, ...node.context }) + continue + } + let status = visit(node, { parent, @@ -104,8 +113,6 @@ export function walk( if (node.kind === 'rule') { walk(node.nodes, visit, node, context) - } else if (node.kind === 'context') { - walk(node.nodes, visit, node, { ...context, ...node.context }) } } } diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 7eeb52900174..ea035694c7f2 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -32,7 +32,7 @@ export async function applyCompatibilityHooks({ // Collect paths from `@plugin` at-rules if (node.selector === '@plugin' || node.selector.startsWith('@plugin ')) { - if (parent !== null && parent.kind !== 'context') { + if (parent !== null) { throw new Error('`@plugin` cannot be nested.') } @@ -99,7 +99,7 @@ export async function applyCompatibilityHooks({ throw new Error('`@config` cannot have a body.') } - if (parent !== null && parent.kind !== 'context') { + if (parent !== null) { throw new Error('`@config` cannot be nested.') } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index d6e4d2ee3ced..a6a147bf59d5 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -106,7 +106,7 @@ async function parseCss( throw new Error('`@source` cannot have a body.') } - if (parent !== null && parent.kind !== 'context') { + if (parent !== null) { throw new Error('`@source` cannot be nested.') } @@ -125,7 +125,7 @@ async function parseCss( // Register custom variants from `@variant` at-rules if (node.selector.startsWith('@variant ')) { - if (parent !== null && parent.kind !== 'context') { + if (parent !== null) { throw new Error('`@variant` cannot be nested.') } From fba81c251dc3a20d43e08e543b72c0be6193ecce Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:13:06 +0200 Subject: [PATCH 32/51] Remove leftover context node in buildImportNodes --- packages/tailwindcss/src/at-import.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 9dba822cffee..88bbaffe4a89 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -132,5 +132,5 @@ function buildImportNodes( root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] } - return [context({ base }, root)] + return root } From a7d98adebef88615711ac5e602d489d1f40fb5d0 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:16:18 +0200 Subject: [PATCH 33/51] Add \`resourceHint\` to loadModule callback --- packages/tailwindcss/src/at-import.test.ts | 1 + packages/tailwindcss/src/compat/apply-compat-hooks.ts | 10 +++++++--- packages/tailwindcss/src/index.ts | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 46c245315130..eb1d0aca51ec 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -12,6 +12,7 @@ async function run( loadModule: ( id: string, base: string, + resourceHint: 'plugin' | 'config', ) => Promise<{ module: Config | Plugin; base: string }> = () => Promise.reject(new Error('Unexpected module')), candidates: string[] = [], diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index ea035694c7f2..9e85faf9675d 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -21,7 +21,11 @@ export async function applyCompatibilityHooks({ }: { designSystem: DesignSystem ast: AstNode[] - loadModule: (path: string, base: string) => Promise<{ module: any; base: string }> + loadModule: ( + path: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: any; base: string }> globs: { origin?: string; pattern: string }[] }) { let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = [] @@ -146,7 +150,7 @@ export async function applyCompatibilityHooks({ let [configs, pluginDetails] = await Promise.all([ Promise.all( configPaths.map(async ({ id, base }) => { - let loaded = await loadModule(id, base) + let loaded = await loadModule(id, base, 'config') return { path: id, base: loaded.base, @@ -156,7 +160,7 @@ export async function applyCompatibilityHooks({ ), Promise.all( pluginPaths.map(async ([{ id, base }, pluginOptions]) => { - let loaded = await loadModule(id, base) + let loaded = await loadModule(id, base, 'plugin') return { path: id, base: loaded.base, diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index a6a147bf59d5..ae069ced25ee 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -26,7 +26,11 @@ export type Config = UserConfig const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { - loadModule?: (id: string, base: string) => Promise<{ module: Plugin | Config; base: string }> + loadModule?: ( + id: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: Plugin | Config; base: string }> loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> } From 31ff3eb5c3a4392d1af01424057e75e9b1d13a4a Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 11:57:26 +0200 Subject: [PATCH 34/51] Revert screwup --- packages/tailwindcss/src/ast.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 461ddeef85df..8707bcff7d96 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -1,5 +1,4 @@ export type Rule = { - replaceWith(contextNode: Context): unknown kind: 'rule' selector: string nodes: AstNode[] From 8ffad46784f47e1ed6ea9511798158ee92325fb5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 13:05:54 +0200 Subject: [PATCH 35/51] Handle recursion --- packages/@tailwindcss-node/src/compile.ts | 20 +++++++++------ packages/tailwindcss/src/at-import.test.ts | 29 +++++++++++++++++++--- packages/tailwindcss/src/at-import.ts | 27 ++++++++++---------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 69066f9f9e19..dec23a7e4228 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -99,21 +99,27 @@ function resolveCssId(id: string, base: string): Promise - cssResolver.resolve({}, base, id, {}, (err, result) => { - if (err) return reject(err) - resolve(result) - }), - ) + let dotResolved = runResolver(cssResolver, `./${id}`, base) + if (!dotResolved) return runResolver(cssResolver, id, base) + return dotResolved } const jsResolver = EnhancedResolve.ResolverFactory.createResolver({ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), useSyncFileSystemCalls: true, }) + function resolveJsId(id: string, base: string): Promise { + return runResolver(jsResolver, id, base) +} + +function runResolver( + resolver: EnhancedResolve.Resolver, + id: string, + base: string, +): Promise { return new Promise((resolve, reject) => - jsResolver.resolve({}, base, id, {}, (err, result) => { + resolver.resolve({}, base, id, {}, (err, result) => { if (err) return reject(err) resolve(result) }), diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index eb1d0aca51ec..40efe8bf8dfa 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -261,10 +261,10 @@ test('updates the base when loading modules inside nested files', async () => { ).trim(), ).toBe('') - expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo') - expect(loadModule).toHaveBeenNthCalledWith(2, './root-config.js', '/root') - expect(loadModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo') - expect(loadModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root') + expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo', 'config') + expect(loadModule).toHaveBeenNthCalledWith(2, './root-config.js', '/root', 'config') + expect(loadModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo', 'plugin') + expect(loadModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root', 'plugin') }) test('emits the right base for @source directives inside nested files', async () => { @@ -346,3 +346,24 @@ test('emits the right base for @source found inside JS configs and plugins from { pattern: './root-config/*.html', base: '/root-config' }, ]) }) + +test('it crashes when inside a cycle', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @import url('foo.css'); + `, + base: '/root', + }) + + expect( + run( + css` + @import url('foo.css'); + `, + loadStylesheet, + ), + ).rejects.toMatchInlineSnapshot( + `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, + ) +}) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 88bbaffe4a89..f3c8af5def48 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -8,6 +8,7 @@ export async function substituteAtImports( ast: AstNode[], base: string, loadStylesheet: LoadStylesheet, + recurseCount = 0, ) { let promises: Promise[] = [] @@ -24,9 +25,18 @@ export async function substituteAtImports( promises.push( (async () => { - let imported = await atImport(uri, base, loadStylesheet) - contextNode.nodes = buildImportNodes(imported.ast, imported.base, layer, media, supports) - contextNode.context.base = imported.base + if (recurseCount > 50) { + throw new Error( + `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, + ) + } + + const loaded = await loadStylesheet(uri, base) + let ast = CSS.parse(loaded.content) + await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) + + contextNode.nodes = buildImportNodes(ast, loaded.base, layer, media, supports) + contextNode.context.base = loaded.base })(), ) @@ -40,17 +50,6 @@ export async function substituteAtImports( await Promise.all(promises) } -async function atImport( - id: string, - parentBase: string, - loadStylesheet: LoadStylesheet, -): Promise<{ ast: AstNode[]; base: string }> { - const { content, base } = await loadStylesheet(id, parentBase) - let ast = CSS.parse(content) - await substituteAtImports(ast, base, loadStylesheet) - return { ast, base } -} - // c.f. https://github.com/postcss/postcss-import/blob/master/lib/parse-statements.js function parseImportParams(params: ValueParser.ValueAstNode[]) { let uri From b8fbe5600e890575e7a83270345398ffbc8ada03 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 14:11:27 +0200 Subject: [PATCH 36/51] Improve spec compliance --- packages/@tailwindcss-node/src/compile.ts | 12 ++- packages/tailwindcss/src/at-import.test.ts | 69 ++++++++++++++++- packages/tailwindcss/src/at-import.ts | 88 +++++++++++++--------- 3 files changed, 127 insertions(+), 42 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index dec23a7e4228..a343aa7f432f 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -91,7 +91,7 @@ const cssResolver = EnhancedResolve.ResolverFactory.createResolver({ mainFields: ['style'], conditionNames: ['style'], }) -function resolveCssId(id: string, base: string): Promise { +async function resolveCssId(id: string, base: string): Promise { if (typeof globalThis.__tw_resolve === 'function') { let resolved = globalThis.__tw_resolve(id, base) if (resolved) { @@ -99,9 +99,13 @@ function resolveCssId(id: string, base: string): Promise { +let resolver = async (id: string) => { + if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id) return { content: exampleCSS, base: '/root', @@ -101,7 +102,52 @@ let resolver = async () => { } // Examples from https://developer.mozilla.org/en-US/docs/Web/CSS/@import -test.each([ +test.only.each([ + // url extraction + [ + css` + @import url('example.css'); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], + [ + css` + @import url('./example.css'); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], + [ + css` + @import url(example.css); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], + [ + css` + @import url(./example.css); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], + + // handles case-insensitive `@import` directive + [ + // prettier-ignore + css` + @ImPoRt url('example.css'); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], + // @media [ css` @@ -204,6 +250,25 @@ test.each([ } `), ], + + // unknown syntax is ignored + [ + css` + @import url(example.css) does-not-exist(foo); + `, + optimizeCss(css` + @import url(example.css) does-not-exist(foo); + `), + ], + // prettier-ignore + [ + css` + @import url('example.css' url-mod); + `, + optimizeCss(css` + @import url('example.css' url-mod); + `), + ], ])('resolves %s', async (input, output) => { await expect(run(input, resolver)).resolves.toBe(output) }) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index f3c8af5def48..f3e00e3430c7 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -13,37 +13,46 @@ export async function substituteAtImports( let promises: Promise[] = [] walk(ast, (node, { replaceWith }) => { - if (node.kind === 'rule' && node.selector[0] === '@' && node.selector.startsWith('@import ')) { - let { uri, layer, media, supports } = parseImportParams( - ValueParser.parse(node.selector.slice(8)), - ) - - // Skip importing data URIs - if (uri.startsWith('data:')) return - - let contextNode = context({}, []) - - promises.push( - (async () => { - if (recurseCount > 50) { - throw new Error( - `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, - ) - } - - const loaded = await loadStylesheet(uri, base) - let ast = CSS.parse(loaded.content) - await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) - - contextNode.nodes = buildImportNodes(ast, loaded.base, layer, media, supports) - contextNode.context.base = loaded.base - })(), - ) - - replaceWith(contextNode) - // The resolved Stylesheets already have their transitive @imports - // resolved, so we can skip walking them. - return WalkAction.Skip + if ( + node.kind === 'rule' && + node.selector[0] === '@' && + node.selector.toLowerCase().startsWith('@import ') + ) { + try { + let { uri, layer, media, supports } = parseImportParams( + ValueParser.parse(node.selector.slice(8)), + ) + + // Skip importing data URIs + if (uri.startsWith('data:')) return + + let contextNode = context({}, []) + + promises.push( + (async () => { + if (recurseCount > 50) { + throw new Error( + `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, + ) + } + + const loaded = await loadStylesheet(uri, base) + let ast = CSS.parse(loaded.content) + await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) + + contextNode.nodes = buildImportNodes(ast, layer, media, supports) + contextNode.context.base = loaded.base + })(), + ) + + replaceWith(contextNode) + // The resolved Stylesheets already have their transitive @imports + // resolved, so we can skip walking them. + return WalkAction.Skip + } catch (e: any) { + // When an error occurs while parsing the `@import` statement, we skip + // the import. + } } }) @@ -72,11 +81,19 @@ function parseImportParams(params: ValueParser.ValueAstNode[]) { if (node.kind === 'function' && /^url$/i.test(node.value)) { if (uri) throw new Error("Multiple url's") - if (!node.nodes?.[0]?.value) throw new Error('Unable to find uri') - if (node.nodes[0].value[0] !== '"' && node.nodes[0].value[0] !== "'") - throw new Error('Unable to find uri') - uri = node.nodes[0].value.slice(1, -1) + if (!node.nodes?.[0]?.value) throw new Error('Unable to find uri') + if (node.nodes.length > 1) throw new Error('Unable to find uri') + + let uriCandidate = node.nodes[0].value + if ( + (uriCandidate.at(0) === '"' && uriCandidate.at(-1) === '"') || + (uriCandidate.at(0) === "'" && uriCandidate.at(-1) === "'") + ) { + uri = node.nodes[0].value.slice(1, -1) + } else { + uri = node.nodes[0].value + } continue } @@ -112,7 +129,6 @@ function parseImportParams(params: ValueParser.ValueAstNode[]) { function buildImportNodes( importedAst: AstNode[], - base: string, layer: string | null, media: string | null, supports: string | null, From 026c30c408d0b62543ccb03d6887e9aae8ec9337 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 14:24:32 +0200 Subject: [PATCH 37/51] Add a test case for imports starting with `/`. --- packages/tailwindcss/src/at-import.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index a894da4fbfc5..fccf3ad0d635 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -120,6 +120,14 @@ test.only.each([ ${exampleCSS} `), ], + [ + css` + @import url('/example.css'); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], [ css` @import url(example.css); @@ -136,6 +144,14 @@ test.only.each([ ${exampleCSS} `), ], + [ + css` + @import url(/example.css); + `, + optimizeCss(css` + ${exampleCSS} + `), + ], // handles case-insensitive `@import` directive [ From 19ab40d48e577341d4ee2fc272688dff334b6edd Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 14:28:56 +0200 Subject: [PATCH 38/51] Move noramlizePath into node lib --- packages/@tailwindcss-node/src/index.cts | 1 + packages/@tailwindcss-node/src/index.ts | 1 + .../src}/normalize-path.ts | 0 .../src/postcss-fix-relative-paths/index.ts | 4 +- packages/@tailwindcss-vite/src/index.ts | 3 +- .../@tailwindcss-vite/src/normalize-path.ts | 47 ------------------- 6 files changed, 4 insertions(+), 52 deletions(-) rename packages/{@tailwindcss-postcss/src/postcss-fix-relative-paths => @tailwindcss-node/src}/normalize-path.ts (100%) delete mode 100644 packages/@tailwindcss-vite/src/normalize-path.ts diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts index a143865efb84..ee0de7ff5503 100644 --- a/packages/@tailwindcss-node/src/index.cts +++ b/packages/@tailwindcss-node/src/index.cts @@ -1,6 +1,7 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' export * from './compile' +export * from './normalize-path' // In Bun, ESM modules will also populate `require.cache`, so the module hook is // not necessary. diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index 85b292ed022f..f42c4ff4e40d 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -1,6 +1,7 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' export * from './compile' +export * from './normalize-path' // In Bun, ESM modules will also populate `require.cache`, so the module hook is // not necessary. diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/normalize-path.ts b/packages/@tailwindcss-node/src/normalize-path.ts similarity index 100% rename from packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/normalize-path.ts rename to packages/@tailwindcss-node/src/normalize-path.ts diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts index 7a0f92da714a..2b88014a3e85 100644 --- a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts @@ -1,12 +1,10 @@ +import { normalizePath } from '@tailwindcss/node' import path from 'node:path' import type { AtRule, Plugin } from 'postcss' -import { normalizePath } from './normalize-path' const SINGLE_QUOTE = "'" const DOUBLE_QUOTE = '"' -export { normalizePath } - export default function fixRelativePathsPlugin(): Plugin { // Retain a list of touched at-rules to avoid infinite loops let touched: WeakSet = new WeakSet() diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 189cdf36590a..53f2aecffb6b 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -1,11 +1,10 @@ -import { compile } from '@tailwindcss/node' +import { compile, normalizePath } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import { Features, transform } from 'lightningcss' import path from 'path' import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' -import { normalizePath } from './normalize-path' export default function tailwindcss(): Plugin[] { let servers: ViteDevServer[] = [] diff --git a/packages/@tailwindcss-vite/src/normalize-path.ts b/packages/@tailwindcss-vite/src/normalize-path.ts deleted file mode 100644 index a8184ef2317c..000000000000 --- a/packages/@tailwindcss-vite/src/normalize-path.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Inlined version of `normalize-path` -// Copyright (c) 2014-2018, Jon Schlinkert. -// Released under the MIT License. -function normalizePathBase(path: string, stripTrailing?: boolean) { - if (typeof path !== 'string') { - throw new TypeError('expected path to be a string') - } - - if (path === '\\' || path === '/') return '/' - - var len = path.length - if (len <= 1) return path - - // ensure that win32 namespaces has two leading slashes, so that the path is - // handled properly by the win32 version of path.parse() after being normalized - // https://msdn.microsoft.com/library/windows/desktop/aa365247(v=vs.85).aspx#namespaces - var prefix = '' - if (len > 4 && path[3] === '\\') { - var ch = path[2] - if ((ch === '?' || ch === '.') && path.slice(0, 2) === '\\\\') { - path = path.slice(2) - prefix = '//' - } - } - - var segs = path.split(/[/\\]+/) - if (stripTrailing !== false && segs[segs.length - 1] === '') { - segs.pop() - } - return prefix + segs.join('/') -} - -export function normalizePath(originalPath: string) { - let normalized = normalizePathBase(originalPath) - - // Make sure Windows network share paths are normalized properly - // They have to begin with two slashes or they won't resolve correctly - if ( - originalPath.startsWith('\\\\') && - normalized.startsWith('/') && - !normalized.startsWith('//') - ) { - return `/${normalized}` - } - - return normalized -} From c7335870010bae0edcfd54ba275163f2403f5568 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 14:30:26 +0200 Subject: [PATCH 39/51] revert change in postcss fixture --- .../example-project/src/relative-import.css | 5 +-- .../@tailwindcss-postcss/src/index.test.ts | 34 ++++--------------- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css index 5dfb60b16219..48a30ab4dca7 100644 --- a/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css +++ b/packages/@tailwindcss-postcss/src/fixtures/example-project/src/relative-import.css @@ -1,4 +1 @@ -/* @plugin '../plugin.js'; */ -.foo { - @apply text-red-500; -} +@plugin '../plugin.js'; diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index a7c55f1dbcb5..ab1c25dd415d 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -169,7 +169,6 @@ describe('plugins', () => { let result = await processor.process( css` - @import 'tailwindcss/theme' theme(reference); @import 'tailwindcss/utilities'; @import '../example-project/src/relative-import.css'; `, @@ -177,39 +176,18 @@ describe('plugins', () => { ) expect(result.css.trim()).toMatchInlineSnapshot(` - ".text-2xl { - font-size: var(--font-size-2xl, 1.5rem); - line-height: var(--tw-leading, var(--font-size-2xl--line-height, 2rem)); - } - - .text-black\\/50 { - color: color-mix(in srgb, var(--color-black, #000) 50%, transparent); - } - - .underline { + ".underline { text-decoration-line: underline; } - @media (width >= 96rem) { - .\\32 xl\\:font-bold { - --tw-font-weight: 700; - font-weight: 700; - } - } - - @apply text-red-500; - - @supports (-moz-orient: inline) { - @layer base { - *, :before, :after, ::backdrop { - --tw-font-weight: initial; - } + @media (inverted-colors: inverted) { + .inverted\\:flex { + display: flex; } } - @property --tw-font-weight { - syntax: "*"; - inherits: false + .hocus\\:underline:focus, .hocus\\:underline:hover { + text-decoration-line: underline; }" `) }) From 03a3bbcbbb858d63445ecba047ff3de777074e2d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 14:37:14 +0200 Subject: [PATCH 40/51] Move `base` into compiler options --- packages/@tailwindcss-node/src/compile.ts | 3 +- packages/tailwindcss/src/at-import.test.ts | 8 +- .../tailwindcss/src/compat/config.test.ts | 54 +-- .../tailwindcss/src/compat/plugin-api.test.ts | 96 ++--- .../src/compat/screens-config.test.ts | 14 +- .../tailwindcss/src/css-functions.test.ts | 3 - packages/tailwindcss/src/index.bench.ts | 9 +- packages/tailwindcss/src/index.test.ts | 331 ++++++++---------- packages/tailwindcss/src/index.ts | 13 +- packages/tailwindcss/src/plugin.test.ts | 4 +- packages/tailwindcss/src/test-utils/run.ts | 4 +- packages/tailwindcss/src/utilities.test.ts | 193 +++++----- packages/tailwindcss/tests/ui.spec.ts | 29 +- 13 files changed, 317 insertions(+), 444 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index a343aa7f432f..cfa5af17d527 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -11,7 +11,8 @@ export async function compile( css: string, { base, onDependency }: { base: string; onDependency: (path: string) => void }, ) { - return await _compile(css, base, { + return await _compile(css, { + base, async loadModule(id, base) { if (id[0] !== '.') { let resolvedPath = await resolveJsId(id, base) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index fccf3ad0d635..7ed0b6caef08 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -17,7 +17,7 @@ async function run( Promise.reject(new Error('Unexpected module')), candidates: string[] = [], ) { - let compiler = await compile(css, '/root', { loadStylesheet, loadModule }) + let compiler = await compile(css, { base: '/root', loadStylesheet, loadModule }) return optimizeCss(compiler.build(candidates)) } @@ -362,8 +362,7 @@ test('emits the right base for @source directives inside nested files', async () @import './foo/bar.css'; @source './root/**/*.css'; `, - '/root', - { loadStylesheet }, + { base: '/root', loadStylesheet }, ) expect(compiler.globs).toEqual([ @@ -411,8 +410,7 @@ test('emits the right base for @source found inside JS configs and plugins from @config './root-config.js'; @plugin './root-plugin.js'; `, - '/root', - { loadStylesheet, loadModule }, + { base: '/root', loadStylesheet, loadModule }, ) expect(compiler.globs).toEqual([ diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index c808ac682318..2b8819a68d83 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -11,7 +11,7 @@ test('Config files can add content', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }), }) @@ -24,7 +24,7 @@ test('Config files can change dark mode (media)', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }), }) @@ -44,7 +44,7 @@ test('Config files can change dark mode (selector)', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }), }) @@ -64,7 +64,7 @@ test('Config files can change dark mode (variant)', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { darkMode: ['variant', '&:where(:not(.light))'] }, base: '/root', @@ -87,7 +87,7 @@ test('Config files can add plugins', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { plugins: [ @@ -118,7 +118,7 @@ test('Plugins loaded from config files can contribute to the config', async () = @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { plugins: [ @@ -147,7 +147,7 @@ test('Config file presets can contribute to the config', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { presets: [ @@ -176,7 +176,7 @@ test('Config files can affect the theme', async () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -220,7 +220,7 @@ test('Variants in CSS overwrite variants from plugins', async () => { @variant light (&:is(.my-light)); ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { darkMode: ['variant', '&:is(.dark)'], @@ -270,7 +270,7 @@ describe('theme callbacks', () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -381,7 +381,7 @@ describe('theme overrides order', () => { @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -427,7 +427,7 @@ describe('theme overrides order', () => { @plugin "./plugin.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async (id) => { if (id.includes('config.js')) { return { @@ -555,7 +555,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -594,7 +594,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -634,7 +634,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -674,7 +674,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -721,7 +721,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -761,7 +761,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -800,7 +800,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -834,7 +834,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -873,7 +873,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -913,7 +913,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -953,7 +953,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -1000,7 +1000,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -1040,7 +1040,7 @@ describe('default font family compatibility', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -1070,7 +1070,7 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s @config "./config.js"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -1169,7 +1169,7 @@ test('merges css breakpoints with js config screens', async () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 4fd183e472e3..f73cde3ee2bc 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -8,8 +8,8 @@ import type { CssInJs, Plugin, PluginAPI } from './plugin-api' const css = String.raw // TODO: Expand the API changes into the tests below -function compile(css: string, base: string, { loadPlugin }: { loadPlugin: () => Promise }) { - return coreCompile(css, base, { +function compile(css: string, { loadPlugin }: { loadPlugin: () => Promise }) { + return coreCompile(css, { async loadModule(id, base) { let plugin = await loadPlugin() return { module: plugin, base } @@ -24,7 +24,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ addBase, theme }) { @@ -87,7 +87,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -132,7 +132,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -176,7 +176,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -227,7 +227,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin(function ({ addUtilities, theme }) { addUtilities({ @@ -267,7 +267,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -318,7 +318,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -362,7 +362,7 @@ describe('theme', async () => { } ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin(function ({ matchUtilities, theme }) { matchUtilities( @@ -417,7 +417,7 @@ describe('theme', async () => { } ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -468,7 +468,7 @@ describe('theme', async () => { } ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadPlugin: async () => { return plugin( function ({ matchUtilities, theme }) { @@ -514,7 +514,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, '/root', { + await compile(input, { loadPlugin: async () => { return plugin( function ({ theme }) { @@ -546,7 +546,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let { build } = await compile(input, '/root', { + let { build } = await compile(input, { loadPlugin: async () => { return plugin(function ({ matchUtilities, theme }) { function utility(name: string, themeKey: string) { @@ -790,7 +790,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, '/root', { + await compile(input, { loadPlugin: async () => { return plugin( ({ theme }) => { @@ -829,7 +829,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, '/root', { + await compile(input, { loadPlugin: async () => { return plugin(({ theme }) => { fn(theme('transitionTimingFunction.DEFAULT')) @@ -857,7 +857,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, '/root', { + await compile(input, { loadPlugin: async () => { return plugin(({ theme }) => { fn(theme('color.red.100')) @@ -882,7 +882,7 @@ describe('theme', async () => { let fn = vi.fn() - await compile(input, '/root', { + await compile(input, { loadPlugin: async () => { return plugin(({ theme }) => { fn(theme('i.do.not.exist')) @@ -905,7 +905,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let { build } = await compile(input, '/root', { + let { build } = await compile(input, { loadPlugin: async () => { return plugin(({ addUtilities, matchUtilities }) => { addUtilities({ @@ -957,7 +957,7 @@ describe('theme', async () => { @plugin "my-plugin"; ` - let { build } = await compile(input, '/root', { + let { build } = await compile(input, { loadPlugin: async () => { return plugin(function ({ matchUtilities }) { function utility(name: string, themeKey: string) { @@ -1176,7 +1176,6 @@ describe('addVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1208,7 +1207,6 @@ describe('addVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1241,7 +1239,6 @@ describe('addVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1276,7 +1273,6 @@ describe('addVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1325,7 +1321,6 @@ describe('addVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1368,7 +1363,6 @@ describe('addVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ addVariant }: PluginAPI) => { @@ -1408,7 +1402,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1440,7 +1433,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1476,7 +1468,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1519,7 +1510,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1556,7 +1546,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1608,7 +1597,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1638,7 +1626,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1688,7 +1675,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1741,7 +1727,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1813,7 +1798,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1867,7 +1851,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -1937,7 +1920,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2007,7 +1989,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2085,7 +2066,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2117,7 +2097,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2139,7 +2118,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2169,7 +2147,6 @@ describe('matchVariant', () => { @tailwind utilities; } `, - '/root', { loadPlugin: async () => { return ({ matchVariant }: PluginAPI) => { @@ -2205,7 +2182,6 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2244,7 +2220,6 @@ describe('addUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2272,7 +2247,6 @@ describe('addUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2309,7 +2283,6 @@ describe('addUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2341,7 +2314,6 @@ describe('addUtilities()', () => { @tailwind utilities; } `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2380,7 +2352,6 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2434,7 +2405,6 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2461,7 +2431,6 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2502,7 +2471,6 @@ describe('addUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/base', { async loadPlugin() { return ({ addUtilities }: PluginAPI) => { @@ -2548,7 +2516,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2624,7 +2592,6 @@ describe('matchUtilities()', () => { @plugin "my-plugin"; @tailwind utilities; `, - '/base', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2670,7 +2637,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2737,7 +2704,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2808,7 +2775,7 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2860,7 +2827,7 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2895,7 +2862,7 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -2936,7 +2903,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3056,7 +3023,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3130,7 +3097,7 @@ describe('matchUtilities()', () => { --opacity-my-opacity: 0.5; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3186,7 +3153,6 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/base', { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3252,7 +3218,7 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, - '/root', + { async loadPlugin() { return ({ matchUtilities }: PluginAPI) => { @@ -3277,7 +3243,6 @@ describe('addComponents()', () => { @plugin "my-plugin"; @tailwind utilities; `, - '/base', { async loadPlugin() { return ({ addComponents }: PluginAPI) => { @@ -3343,7 +3308,6 @@ describe('prefix()', () => { css` @plugin "my-plugin"; `, - '/base', { async loadPlugin() { return ({ prefix }: PluginAPI) => { diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index e82ea012ffeb..5f75504ad966 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -19,7 +19,7 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -102,7 +102,7 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -200,7 +200,7 @@ test('JS config `screens` only setup, even if those match the default-theme expo @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -279,7 +279,7 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -385,7 +385,7 @@ test('JS config with `theme: { extends }` should not include the `default-config @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -463,7 +463,7 @@ describe('complex screen configs', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { @@ -550,7 +550,7 @@ describe('complex screen configs', () => { @tailwind utilities; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: { theme: { diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index a5c149ad4b3a..521f50009852 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -335,7 +335,6 @@ describe('theme function', () => { font-family: theme(fontFamily.sans); } `, - '/root', { loadModule: async () => ({ module: {}, base: '/root' }), }, @@ -795,7 +794,6 @@ describe('in plugins', () => { @tailwind utilities; } `, - '/root', { async loadModule() { return { @@ -854,7 +852,6 @@ describe('in JS config files', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: { diff --git a/packages/tailwindcss/src/index.bench.ts b/packages/tailwindcss/src/index.bench.ts index 1dd1b27f71bf..dcffb522e3f2 100644 --- a/packages/tailwindcss/src/index.bench.ts +++ b/packages/tailwindcss/src/index.bench.ts @@ -10,12 +10,9 @@ bench('compile', async () => { let scanner = new Scanner({ detectSources: { base: root } }) let candidates = scanner.scan() - let { build } = await compile( - css` - @tailwind utilities; - `, - root, - ) + let { build } = await compile(css` + @tailwind utilities; + `) build(candidates) }) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 5180b6250dba..3693bbb0e878 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1428,7 +1428,6 @@ describe('Parsing themes values from CSS', () => { @plugin "my-plugin"; @tailwind utilities; `, - '/root', { loadModule: async () => { return { @@ -1475,7 +1474,6 @@ describe('Parsing themes values from CSS', () => { @config "./my-config.js"; @tailwind utilities; `, - '/root', { loadModule: async () => { return { @@ -1518,7 +1516,6 @@ describe('plugins', () => { css` @plugin; `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1536,7 +1533,6 @@ describe('plugins', () => { css` @plugin ''; `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1556,7 +1552,6 @@ describe('plugins', () => { @plugin "my-plugin"; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1578,7 +1573,6 @@ describe('plugins', () => { color: red; } `, - '/root', { loadModule: async () => ({ module: plugin.withOptions((options) => { @@ -1631,7 +1625,6 @@ describe('plugins', () => { is-arr-mixed: null, true, false, 1234567, 1.35, foo, 'bar', 'true'; } `, - '/root', { loadModule: async () => ({ module: plugin.withOptions((options) => { @@ -1672,7 +1665,6 @@ describe('plugins', () => { } } `, - '/root', { loadModule: async () => ({ module: plugin.withOptions((options) => { @@ -1711,7 +1703,6 @@ describe('plugins', () => { color: red; } `, - '/root', { loadModule: async () => ({ module: plugin(({ addUtilities }) => { @@ -1738,7 +1729,6 @@ describe('plugins', () => { --color: [ 'red', 'green', 'blue']; } `, - '/root', { loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, @@ -1760,7 +1750,6 @@ describe('plugins', () => { }; } `, - '/root', { loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, @@ -1786,7 +1775,6 @@ describe('plugins', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1819,7 +1807,6 @@ describe('plugins', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1853,7 +1840,6 @@ describe('plugins', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1889,7 +1875,6 @@ describe('plugins', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1939,7 +1924,6 @@ describe('plugins', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -1978,7 +1962,6 @@ describe('plugins', () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addVariant }: PluginAPI) => { @@ -2020,7 +2003,7 @@ describe('@source', () => { css` @source "./foo/bar/*.ts"; `, - '/root', + { base: '/root' }, ) expect(globs).toEqual([{ pattern: './foo/bar/*.ts', base: '/root' }]) @@ -2032,7 +2015,7 @@ describe('@source', () => { @source "./foo/**/*.ts"; @source "./php/secr3t/smarty.php"; `, - '/root', + { base: '/root' }, ) expect(globs).toEqual([ @@ -2086,16 +2069,13 @@ describe('@variant', () => { describe('body-less syntax', () => { test('selector variant', async () => { - let { build } = await compile( - css` - @variant hocus (&:hover, &:focus); + let { build } = await compile(css` + @variant hocus (&:hover, &:focus); - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['hocus:underline', 'group-hocus:flex']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2112,16 +2092,13 @@ describe('@variant', () => { }) test('at-rule variant', async () => { - let { build } = await compile( - css` - @variant any-hover (@media (any-hover: hover)); + let { build } = await compile(css` + @variant any-hover (@media (any-hover: hover)); - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['any-hover:hover:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2138,20 +2115,17 @@ describe('@variant', () => { describe('body with @slot syntax', () => { test('selector with @slot', async () => { - let { build } = await compile( - css` - @variant selected { - &[data-selected] { - @slot; - } + let { build } = await compile(css` + @variant selected { + &[data-selected] { + @slot; } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['selected:underline', 'group-selected:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2168,21 +2142,18 @@ describe('@variant', () => { }) test('grouped selectors with @slot', async () => { - let { build } = await compile( - css` - @variant hocus { - &:hover, - &:focus { - @slot; - } + let { build } = await compile(css` + @variant hocus { + &:hover, + &:focus { + @slot; } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2199,24 +2170,21 @@ describe('@variant', () => { }) test('multiple selectors with @slot', async () => { - let { build } = await compile( - css` - @variant hocus { - &:hover { - @slot; - } - - &:focus { - @slot; - } + let { build } = await compile(css` + @variant hocus { + &:hover { + @slot; } - @layer utilities { - @tailwind utilities; + &:focus { + @slot; } - `, - '/root', - ) + } + + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2233,23 +2201,20 @@ describe('@variant', () => { }) test('nested selector with @slot', async () => { - let { build } = await compile( - css` - @variant custom-before { - & { - --has-before: 1; - &::before { - @slot; - } + let { build } = await compile(css` + @variant custom-before { + & { + --has-before: 1; + &::before { + @slot; } } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['custom-before:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2266,26 +2231,23 @@ describe('@variant', () => { }) test('grouped nested selectors with @slot', async () => { - let { build } = await compile( - css` - @variant custom-before { - & { - --has-before: 1; - &::before { - &:hover, - &:focus { - @slot; - } + let { build } = await compile(css` + @variant custom-before { + & { + --has-before: 1; + &::before { + &:hover, + &:focus { + @slot; } } } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['custom-before:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2302,26 +2264,23 @@ describe('@variant', () => { }) test('nested multiple selectors with @slot', async () => { - let { build } = await compile( - css` - @variant hocus { - &:hover { - @media (hover: hover) { - @slot; - } - } - - &:focus { + let { build } = await compile(css` + @variant hocus { + &:hover { + @media (hover: hover) { @slot; } } - @layer utilities { - @tailwind utilities; + &:focus { + @slot; } - `, - '/root', - ) + } + + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2350,22 +2309,19 @@ describe('@variant', () => { }) test('selector nested under at-rule with @slot', async () => { - let { build } = await compile( - css` - @variant hocus { - @media (hover: hover) { - &:hover { - @slot; - } + let { build } = await compile(css` + @variant hocus { + @media (hover: hover) { + &:hover { + @slot; } } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['hocus:underline', 'group-hocus:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2386,20 +2342,17 @@ describe('@variant', () => { }) test('at-rule with @slot', async () => { - let { build } = await compile( - css` - @variant any-hover { - @media (any-hover: hover) { - @slot; - } + let { build } = await compile(css` + @variant any-hover { + @media (any-hover: hover) { + @slot; } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['any-hover:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2414,24 +2367,21 @@ describe('@variant', () => { }) test('multiple at-rules with @slot', async () => { - let { build } = await compile( - css` - @variant desktop { - @media (any-hover: hover) { - @slot; - } - - @media (pointer: fine) { - @slot; - } + let { build } = await compile(css` + @variant desktop { + @media (any-hover: hover) { + @slot; } - @layer utilities { - @tailwind utilities; + @media (pointer: fine) { + @slot; } - `, - '/root', - ) + } + + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['desktop:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2452,26 +2402,23 @@ describe('@variant', () => { }) test('nested at-rules with @slot', async () => { - let { build } = await compile( - css` - @variant custom-variant { - @media (orientation: landscape) { - @media screen { - @slot; - } + let { build } = await compile(css` + @variant custom-variant { + @media (orientation: landscape) { + @media screen { + @slot; + } - @media print { - display: none; - } + @media print { + display: none; } } + } - @layer utilities { - @tailwind utilities; - } - `, - '/root', - ) + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['custom-variant:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2494,23 +2441,20 @@ describe('@variant', () => { }) test('at-rule and selector with @slot', async () => { - let { build } = await compile( - css` - @variant custom-dark { - @media (prefers-color-scheme: dark) { - @slot; - } - &:is(.dark *) { - @slot; - } + let { build } = await compile(css` + @variant custom-dark { + @media (prefers-color-scheme: dark) { + @slot; } - - @layer utilities { - @tailwind utilities; + &:is(.dark *) { + @slot; } - `, - '/root', - ) + } + + @layer utilities { + @tailwind utilities; + } + `) let compiled = build(['custom-dark:underline']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -2596,7 +2540,6 @@ test('addBase', async () => { @tailwind utilities; } `, - '/root', { loadModule: async () => ({ module: ({ addBase }: PluginAPI) => { diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index ae069ced25ee..cdaeed908e0e 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -26,6 +26,7 @@ export type Config = UserConfig const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { + base?: string loadModule?: ( id: string, base: string, @@ -60,8 +61,11 @@ function parseThemeOptions(selector: string) { async function parseCss( css: string, - base: string, - { loadModule = throwOnLoadModule, loadStylesheet = throwOnLoadStylesheet }: CompileOptions = {}, + { + base = '', + loadModule = throwOnLoadModule, + loadStylesheet = throwOnLoadStylesheet, + }: CompileOptions = {}, ) { let ast = [context({ base }, CSS.parse(css))] as AstNode[] @@ -332,13 +336,12 @@ async function parseCss( export async function compile( css: string, - base: string, opts: CompileOptions = {}, ): Promise<{ globs: { base: string; pattern: string }[] build(candidates: string[]): string }> { - let { designSystem, ast, globs } = await parseCss(css, base, opts) + let { designSystem, ast, globs } = await parseCss(css, opts) let tailwindUtilitiesNode: Rule | null = null @@ -416,7 +419,7 @@ export async function compile( } export async function __unstable__loadDesignSystem(css: string, opts: CompileOptions = {}) { - let result = await parseCss(css, '/* @TODO */', opts) + let result = await parseCss(css, opts) return result.designSystem } diff --git a/packages/tailwindcss/src/plugin.test.ts b/packages/tailwindcss/src/plugin.test.ts index d9a966d3333d..9af3a55fbb58 100644 --- a/packages/tailwindcss/src/plugin.test.ts +++ b/packages/tailwindcss/src/plugin.test.ts @@ -9,7 +9,7 @@ test('plugin', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: plugin(function ({ addBase }) { addBase({ @@ -37,7 +37,7 @@ test('plugin.withOptions', async () => { @plugin "my-plugin"; ` - let compiler = await compile(input, '/root', { + let compiler = await compile(input, { loadModule: async () => ({ module: plugin.withOptions(function (opts = { foo: '1px' }) { return function ({ addBase }) { diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts index fb81a5b2e94e..d5fd54c03fbd 100644 --- a/packages/tailwindcss/src/test-utils/run.ts +++ b/packages/tailwindcss/src/test-utils/run.ts @@ -2,12 +2,12 @@ import { Features, transform } from 'lightningcss' import { compile } from '..' export async function compileCss(css: string, candidates: string[] = []) { - let { build } = await compile(css, '/root') + let { build } = await compile(css) return optimizeCss(build(candidates)).trim() } export async function run(candidates: string[]) { - let { build } = await compile('@tailwind utilities;', '/root') + let { build } = await compile('@tailwind utilities;') return optimizeCss(build(candidates)).trim() } diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index a8c1a549a721..20dbe123d22b 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -15373,23 +15373,20 @@ test('@container', async () => { describe('custom utilities', () => { test('custom static utility', async () => { - let { build } = await compile( - css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } - @theme reference { - --breakpoint-lg: 1024px; - } + @theme reference { + --breakpoint-lg: 1024px; + } - @utility text-trim { - text-box-trim: both; - text-box-edge: cap alphabetic; - } - `, - '/root', - ) + @utility text-trim { + text-box-trim: both; + text-box-edge: cap alphabetic; + } + `) let compiled = build(['text-trim', 'lg:text-trim']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15410,23 +15407,20 @@ describe('custom utilities', () => { }) test('Multiple static utilities are merged', async () => { - let { build } = await compile( - css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } - @utility really-round { - --custom-prop: hi; - border-radius: 50rem; - } + @utility really-round { + --custom-prop: hi; + border-radius: 50rem; + } - @utility really-round { - border-radius: 30rem; - } - `, - '/root', - ) + @utility really-round { + border-radius: 30rem; + } + `) let compiled = build(['really-round']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15440,22 +15434,19 @@ describe('custom utilities', () => { }) test('custom utilities support some special characters', async () => { - let { build } = await compile( - css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } - @utility push-1/2 { - right: 50%; - } + @utility push-1/2 { + right: 50%; + } - @utility push-50% { - right: 50%; - } - `, - '/root', - ) + @utility push-50% { + right: 50%; + } + `) let compiled = build(['push-1/2', 'push-50%']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15468,25 +15459,22 @@ describe('custom utilities', () => { }) test('can override specific versions of a functional utility with a static utility', async () => { - let { build } = await compile( - css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } - @theme reference { - --font-size-sm: 0.875rem; - --font-size-sm--line-height: 1.25rem; - } + @theme reference { + --font-size-sm: 0.875rem; + --font-size-sm--line-height: 1.25rem; + } - @utility text-sm { - font-size: var(--font-size-sm, 0.8755rem); - line-height: var(--font-size-sm--line-height, 1.255rem); - text-rendering: optimizeLegibility; - } - `, - '/root', - ) + @utility text-sm { + font-size: var(--font-size-sm, 0.8755rem); + line-height: var(--font-size-sm--line-height, 1.255rem); + text-rendering: optimizeLegibility; + } + `) let compiled = build(['text-sm']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15503,22 +15491,19 @@ describe('custom utilities', () => { }) test('can override the default value of a functional utility', async () => { - let { build } = await compile( - css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } - @theme reference { - --radius-xl: 16px; - } + @theme reference { + --radius-xl: 16px; + } - @utility rounded { - border-radius: 50rem; - } - `, - '/root', - ) + @utility rounded { + border-radius: 50rem; + } + `) let compiled = build(['rounded', 'rounded-xl', 'rounded-[33px]']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15539,18 +15524,15 @@ describe('custom utilities', () => { }) test('custom utilities are sorted by used properties', async () => { - let { build } = await compile( - css` - @layer utilities { - @tailwind utilities; - } + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } - @utility push-left { - right: 100%; - } - `, - '/root', - ) + @utility push-left { + right: 100%; + } + `) let compiled = build(['top-[100px]', 'push-left', 'right-[100px]', 'bottom-[100px]']) expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` @@ -15576,36 +15558,27 @@ describe('custom utilities', () => { test('custom utilities must use a valid name definitions', async () => { await expect(() => - compile( - css` - @utility push-* { - right: 100%; - } - `, - '/root', - ), + compile(css` + @utility push-* { + right: 100%; + } + `), ).rejects.toThrowError(/should be alphanumeric/) await expect(() => - compile( - css` - @utility ~push { - right: 100%; - } - `, - '/root', - ), + compile(css` + @utility ~push { + right: 100%; + } + `), ).rejects.toThrowError(/should be alphanumeric/) await expect(() => - compile( - css` - @utility @push { - right: 100%; - } - `, - '/root', - ), + compile(css` + @utility @push { + right: 100%; + } + `), ).rejects.toThrowError(/should be alphanumeric/) }) diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index 53575064e2b5..b7580d2f310a 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -632,22 +632,19 @@ const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css') const defaultTheme = fs.readFileSync(path.resolve(__dirname, '..', 'theme.css'), 'utf-8') async function render(page: Page, content: string, extraCss: string = '') { - let { build } = await compile( - css` - @layer theme, base, components, utilities; - @layer theme { - ${defaultTheme} - } - @layer base { - ${preflight} - } - @layer utilities { - @tailwind utilities; - } - ${extraCss} - `, - '', - ) + let { build } = await compile(css` + @layer theme, base, components, utilities; + @layer theme { + ${defaultTheme} + } + @layer base { + ${preflight} + } + @layer utilities { + @tailwind utilities; + } + ${extraCss} + `) // We noticed that some of the tests depending on the `hover:` variant were // flaky. After some investigation, we found that injected elements had the From 2d541e97a046f8397aa925bdb85282f46088e854 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 20 Sep 2024 09:13:23 -0400 Subject: [PATCH 41/51] Update all plugin-api tests to use `loadModule` --- .../tailwindcss/src/compat/plugin-api.test.ts | 1872 +++++++++-------- 1 file changed, 1029 insertions(+), 843 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index f73cde3ee2bc..7d732ca1ccf1 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1,22 +1,12 @@ import { describe, expect, test, vi } from 'vitest' -import { compile as coreCompile } from '..' +import { compile } from '..' import plugin from '../plugin' import { optimizeCss } from '../test-utils/run' import defaultTheme from './default-theme' -import type { CssInJs, Plugin, PluginAPI } from './plugin-api' +import type { CssInJs, PluginAPI } from './plugin-api' const css = String.raw -// TODO: Expand the API changes into the tests below -function compile(css: string, { loadPlugin }: { loadPlugin: () => Promise }) { - return coreCompile(css, { - async loadModule(id, base) { - let plugin = await loadPlugin() - return { module: plugin, base } - }, - }) -} - describe('theme', async () => { test('plugin theme can contain objects', async () => { let input = css` @@ -25,37 +15,40 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ addBase, theme }) { - addBase({ - '@keyframes enter': theme('keyframes.enter'), - '@keyframes exit': theme('keyframes.exit'), - }) - }, - { - theme: { - extend: { - keyframes: { - enter: { - from: { - opacity: 'var(--tw-enter-opacity, 1)', - transform: - 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ addBase, theme }) { + addBase({ + '@keyframes enter': theme('keyframes.enter'), + '@keyframes exit': theme('keyframes.exit'), + }) + }, + { + theme: { + extend: { + keyframes: { + enter: { + from: { + opacity: 'var(--tw-enter-opacity, 1)', + transform: + 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', + }, }, - }, - exit: { - to: { - opacity: 'var(--tw-exit-opacity, 1)', - transform: - 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', + exit: { + to: { + opacity: 'var(--tw-exit-opacity, 1)', + transform: + 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', + }, }, }, }, }, }, - }, - ) + ), + } }, }) @@ -88,28 +81,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { - values: theme('colors'), - }, - ) - }, - { - theme: { - extend: { - colors: { - 'russet-700': '#7a4724', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { + values: theme('colors'), + }, + ) + }, + { + theme: { + extend: { + colors: { + 'russet-700': '#7a4724', + }, }, }, }, - }, - ) + ), + } }, }) @@ -133,30 +129,33 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-duration': value }), - }, - { - values: theme('animationDuration'), - }, - ) - }, - { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => { - return { - ...theme('transitionDuration'), - } + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), + }, + { + values: theme('animationDuration'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => { + return { + ...theme('transitionDuration'), + } + }, }, }, }, - }, - ) + ), + } }, }) @@ -177,32 +176,35 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-duration': value }), - }, - { - values: theme('animationDuration'), - }, - ) - }, - { - theme: { - extend: { - transitionDuration: { - slow: '800ms', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), + }, + { + values: theme('animationDuration'), }, + ) + }, + { + theme: { + extend: { + transitionDuration: { + slow: '800ms', + }, - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, }, }, - }, - ) + ), + } }, }) @@ -228,20 +230,23 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.percentage': { - color: theme('colors.red.500 / 50%'), - }, - '.fraction': { - color: theme('colors.red.500 / 0.5'), - }, - '.variable': { - color: theme('colors.red.500 / var(--opacity)'), - }, - }) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.percentage': { + color: theme('colors.red.500 / 50%'), + }, + '.fraction': { + color: theme('colors.red.500 / 0.5'), + }, + '.variable': { + color: theme('colors.red.500 / var(--opacity)'), + }, + }) + }), + } }, }) @@ -268,36 +273,39 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-delay': (value) => ({ 'animation-delay': value }), - }, - { - values: theme('animationDelay'), - }, - ) - }, - { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-delay': (value) => ({ 'animation-delay': value }), + }, + { + values: theme('animationDelay'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), - animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('animationDuration'), - }), + animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('animationDuration'), + }), - transitionDuration: { - slow: '800ms', + transitionDuration: { + slow: '800ms', + }, }, }, }, - }, - ) + ), + } }, }) @@ -319,28 +327,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-delay': value }), - }, - { - values: theme('transitionDuration'), - }, - ) - }, - { - theme: { - extend: { - transitionDuration: { - DEFAULT: '1500ms', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-delay': value }), + }, + { + values: theme('transitionDuration'), + }, + ) + }, + { + theme: { + extend: { + transitionDuration: { + DEFAULT: '1500ms', + }, }, }, }, - }, - ) + ), + } }, }) @@ -363,29 +374,32 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ animation: value }), - }, - { - values: theme('animation'), - }, - ) + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ animation: value }), + }, + { + values: theme('animation'), + }, + ) - matchUtilities( - { - animation2: (value) => ({ animation: value }), - }, - { - values: { - DEFAULT: theme('animation.DEFAULT'), - twist: theme('animation.spin'), + matchUtilities( + { + animation2: (value) => ({ animation: value }), }, - }, - ) - }) + { + values: { + DEFAULT: theme('animation.DEFAULT'), + twist: theme('animation.spin'), + }, + }, + ) + }), + } }, }) @@ -418,28 +432,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ '--animation': value }), - }, - { - values: theme('animation'), - }, - ) - }, - { - theme: { - extend: { - animation: { - bounce: 'bounce 1s linear infinite', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ '--animation': value }), + }, + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + bounce: 'bounce 1s linear infinite', + }, }, }, }, - }, - ) + ), + } }, }) @@ -469,28 +486,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ '--animation': value }), - }, - { - values: theme('animation'), - }, - ) - }, - { - theme: { - extend: { - animation: { - DEFAULT: 'twist 1s linear infinite', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ '--animation': value }), + }, + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + DEFAULT: 'twist 1s linear infinite', + }, }, }, }, - }, - ) + ), + } }, }) @@ -515,21 +535,24 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ theme }) { - fn(theme('animation.simple')) - }, - { - theme: { - extend: { - animation: { - simple: 'simple 1s linear', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ theme }) { + fn(theme('animation.simple')) + }, + { + theme: { + extend: { + animation: { + simple: 'simple 1s linear', + }, }, }, }, - }, - ) + ), + } }, }) @@ -547,58 +570,61 @@ describe('theme', async () => { ` let { build } = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ matchUtilities, theme }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - { values: theme(themeKey) }, - ) - } + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities, theme }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + { values: theme(themeKey) }, + ) + } - utility('my-aspect', 'aspectRatio') - utility('my-backdrop-brightness', 'backdropBrightness') - utility('my-backdrop-contrast', 'backdropContrast') - utility('my-backdrop-grayscale', 'backdropGrayscale') - utility('my-backdrop-hue-rotate', 'backdropHueRotate') - utility('my-backdrop-invert', 'backdropInvert') - utility('my-backdrop-opacity', 'backdropOpacity') - utility('my-backdrop-saturate', 'backdropSaturate') - utility('my-backdrop-sepia', 'backdropSepia') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-divide-width', 'divideWidth') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }) + utility('my-aspect', 'aspectRatio') + utility('my-backdrop-brightness', 'backdropBrightness') + utility('my-backdrop-contrast', 'backdropContrast') + utility('my-backdrop-grayscale', 'backdropGrayscale') + utility('my-backdrop-hue-rotate', 'backdropHueRotate') + utility('my-backdrop-invert', 'backdropInvert') + utility('my-backdrop-opacity', 'backdropOpacity') + utility('my-backdrop-saturate', 'backdropSaturate') + utility('my-backdrop-sepia', 'backdropSepia') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-divide-width', 'divideWidth') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } }, }) @@ -791,23 +817,26 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin( - ({ theme }) => { - // The compatibility config specifies that `accentColor` spreads in `colors` - fn(theme('accentColor.primary')) - - // This should even work for theme keys specified in plugin configs - fn(theme('myAccentColor.secondary')) - }, - { - theme: { - extend: { - myAccentColor: ({ theme }) => theme('accentColor'), + loadModule: async (id, base) => { + return { + base, + module: plugin( + ({ theme }) => { + // The compatibility config specifies that `accentColor` spreads in `colors` + fn(theme('accentColor.primary')) + + // This should even work for theme keys specified in plugin configs + fn(theme('myAccentColor.secondary')) + }, + { + theme: { + extend: { + myAccentColor: ({ theme }) => theme('accentColor'), + }, }, }, - }, - ) + ), + } }, }) @@ -830,12 +859,15 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin(({ theme }) => { - fn(theme('transitionTimingFunction.DEFAULT')) - fn(theme('transitionTimingFunction.in')) - fn(theme('transitionTimingFunction.out')) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ theme }) => { + fn(theme('transitionTimingFunction.DEFAULT')) + fn(theme('transitionTimingFunction.in')) + fn(theme('transitionTimingFunction.out')) + }), + } }, }) @@ -858,12 +890,15 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin(({ theme }) => { - fn(theme('color.red.100')) - fn(theme('colors.red.200')) - fn(theme('backgroundColor.red.300')) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ theme }) => { + fn(theme('color.red.100')) + fn(theme('colors.red.200')) + fn(theme('backgroundColor.red.300')) + }), + } }, }) @@ -883,13 +918,16 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin(({ theme }) => { - fn(theme('i.do.not.exist')) - fn(theme('color')) - fn(theme('color', 'magenta')) - fn(theme('colors')) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ theme }) => { + fn(theme('i.do.not.exist')) + fn(theme('color')) + fn(theme('color', 'magenta')) + fn(theme('colors')) + }), + } }, }) @@ -906,34 +944,37 @@ describe('theme', async () => { ` let { build } = await compile(input, { - loadPlugin: async () => { - return plugin(({ addUtilities, matchUtilities }) => { - addUtilities({ - '.foo-bar': { - color: 'red', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ addUtilities, matchUtilities }) => { + addUtilities({ + '.foo-bar': { + color: 'red', + }, + }) - matchUtilities( - { - foo: (value) => ({ - '--my-prop': value, - }), - }, - { - values: { - bar: 'bar-valuer', - baz: 'bar-valuer', + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), }, - }, - ) + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) - addUtilities({ - '.foo-bar': { - backgroundColor: 'red', - }, - }) - }) + addUtilities({ + '.foo-bar': { + backgroundColor: 'red', + }, + }) + }), + } }, }) @@ -958,62 +999,65 @@ describe('theme', async () => { ` let { build } = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ matchUtilities }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - // @ts-ignore - { values: defaultTheme[themeKey] }, - ) - } + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + // @ts-ignore + { values: defaultTheme[themeKey] }, + ) + } - utility('my-aspect', 'aspectRatio') - // The following keys deliberately doesn't work as these are exported - // as functions from the compat config. - // - // utility('my-backdrop-brightness', 'backdropBrightness') - // utility('my-backdrop-contrast', 'backdropContrast') - // utility('my-backdrop-grayscale', 'backdropGrayscale') - // utility('my-backdrop-hue-rotate', 'backdropHueRotate') - // utility('my-backdrop-invert', 'backdropInvert') - // utility('my-backdrop-opacity', 'backdropOpacity') - // utility('my-backdrop-saturate', 'backdropSaturate') - // utility('my-backdrop-sepia', 'backdropSepia') - // utility('my-divide-width', 'divideWidth') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }) + utility('my-aspect', 'aspectRatio') + // The following keys deliberately doesn't work as these are exported + // as functions from the compat config. + // + // utility('my-backdrop-brightness', 'backdropBrightness') + // utility('my-backdrop-contrast', 'backdropContrast') + // utility('my-backdrop-grayscale', 'backdropGrayscale') + // utility('my-backdrop-hue-rotate', 'backdropHueRotate') + // utility('my-backdrop-invert', 'backdropInvert') + // utility('my-backdrop-opacity', 'backdropOpacity') + // utility('my-backdrop-saturate', 'backdropSaturate') + // utility('my-backdrop-sepia', 'backdropSepia') + // utility('my-divide-width', 'divideWidth') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } }, }) @@ -1177,9 +1221,12 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', '&:hover, &:focus') + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', '&:hover, &:focus') + }, } }, }, @@ -1208,9 +1255,12 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', ['&:hover', '&:focus']) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', ['&:hover', '&:focus']) + }, } }, }, @@ -1240,12 +1290,15 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&:hover': '@slot', - '&:focus': '@slot', - }) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&:hover': '@slot', + '&:focus': '@slot', + }) + }, } }, }, @@ -1274,14 +1327,17 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '@media (hover: hover)': { - '&:hover': '@slot', - }, - '&:focus': '@slot', - }) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '@media (hover: hover)': { + '&:hover': '@slot', + }, + '&:focus': '@slot', + }) + }, } }, }, @@ -1322,12 +1378,15 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant( - 'potato', - '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', - ) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant( + 'potato', + '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', + ) + }, } }, }, @@ -1364,15 +1423,18 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&': { - '--custom-property': '@slot', - '&:hover': '@slot', - '&:focus': '@slot', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&': { + '--custom-property': '@slot', + '&:hover': '@slot', + '&:focus': '@slot', + }, + }) + }, } }, }, @@ -1403,9 +1465,12 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `.potato-${flavor} &`) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `.potato-${flavor} &`) + }, } }, }, @@ -1434,9 +1499,12 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + }, } }, }, @@ -1469,12 +1537,16 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant( - 'potato', - (flavor) => `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, - ) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant( + 'potato', + (flavor) => + `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, + ) + }, } }, }, @@ -1511,14 +1583,17 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('tooltip', (side) => `&${side}`, { - values: { - bottom: '[data-location="bottom"]', - top: '[data-location="top"]', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('tooltip', (side) => `&${side}`, { + values: { + bottom: '[data-location="bottom"]', + top: '[data-location="top"]', + }, + }) + }, } }, }, @@ -1547,16 +1622,19 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('alphabet', (side) => `&${side}`, { - values: { - d: '[data-order="1"]', - a: '[data-order="2"]', - c: '[data-order="3"]', - b: '[data-order="4"]', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('alphabet', (side) => `&${side}`, { + values: { + d: '[data-order="1"]', + a: '[data-order="2"]', + c: '[data-order="3"]', + b: '[data-order="4"]', + }, + }) + }, } }, }, @@ -1598,11 +1676,14 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('test', (selector) => - selector.split(',').map((selector) => `&.${selector} > *`), - ) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('test', (selector) => + selector.split(',').map((selector) => `&.${selector} > *`), + ) + }, } }, }, @@ -1627,13 +1708,16 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } }, }, @@ -1676,16 +1760,19 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - values: { - example: '600px', - }, - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + values: { + example: '600px', + }, + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } }, }, @@ -1728,19 +1815,22 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1799,19 +1889,22 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1852,18 +1945,21 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1921,18 +2017,21 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1990,26 +2089,29 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - let lookup = ['100px', '200px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(a.value) - lookup.indexOf(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - let lookup = ['300px', '400px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(z.value) - lookup.indexOf(a.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + let lookup = ['100px', '200px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(a.value) - lookup.indexOf(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + let lookup = ['300px', '400px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(z.value) - lookup.indexOf(a.value) + }, + }) + }, } }, }, @@ -2067,13 +2169,16 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`, { - values: { - DEFAULT: '.bar', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`, { + values: { + DEFAULT: '.bar', + }, + }) + }, } }, }, @@ -2098,9 +2203,12 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`) + }, } }, }, @@ -2119,11 +2227,14 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { - values: { DEFAULT: null }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { + values: { DEFAULT: null }, + }) + }, } }, }, @@ -2148,11 +2259,14 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { - values: { DEFAULT: undefined }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { + values: { DEFAULT: undefined }, + }) + }, } }, }, @@ -2183,14 +2297,17 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, } }, }, @@ -2221,13 +2338,19 @@ describe('addUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': [{ 'text-box-trim': 'both' }, { 'text-box-edge': 'cap alphabetic' }], - }, - ]) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': [ + { 'text-box-trim': 'both' }, + { 'text-box-edge': 'cap alphabetic' }, + ], + }, + ]) + }, } }, }, @@ -2248,22 +2371,25 @@ describe('addUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, }, - }, - { - '.text-trim-2': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', + { + '.text-trim-2': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, }, - }, - ]) + ]) + }, } }, }, @@ -2284,15 +2410,18 @@ describe('addUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.outlined': { - outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.outlined': { + outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], + }, }, - }, - ]) + ]) + }, } }, }, @@ -2315,15 +2444,18 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - WebkitAppearance: 'none', - textBoxTrim: 'both', - textBoxEdge: 'cap alphabetic', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + WebkitAppearance: 'none', + textBoxTrim: 'both', + textBoxEdge: 'cap alphabetic', + }, + }) + }, } }, }, @@ -2353,13 +2485,16 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - '@apply flex dark:underline': {}, - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + '@apply flex dark:underline': {}, + }, + }) + }, } }, }, @@ -2406,14 +2541,17 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim > *': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim > *': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, } }, }, @@ -2432,14 +2570,17 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-textarea': { - appearance: 'none', - 'background-color': '#fff', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-textarea': { + appearance: 'none', + 'background-color': '#fff', + }, + }) + }, } }, }, @@ -2472,13 +2613,16 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { - 'background-color': 'red', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { + 'background-color': 'red', + }, + }) + }, } }, }, @@ -2518,19 +2662,22 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value) => ({ 'border-block-width': value }), - }, - { - values: { - DEFAULT: '1px', - '2': '2px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'border-block': (value) => ({ 'border-block-width': value }), }, - }, - ) + { + values: { + DEFAULT: '1px', + '2': '2px', + }, + }, + ) + }, } }, }, @@ -2593,23 +2740,26 @@ describe('matchUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'all-but-order-bottom-left-radius': (value) => - [ - { 'border-top-left-radius': value }, - { 'border-top-right-radius': value }, - { 'border-bottom-right-radius': value }, - ] as CssInJs[], - }, - { - values: { - DEFAULT: '1px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'all-but-order-bottom-left-radius': (value) => + [ + { 'border-top-left-radius': value }, + { 'border-top-right-radius': value }, + { 'border-bottom-right-radius': value }, + ] as CssInJs[], }, - }, - ) + { + values: { + DEFAULT: '1px', + }, + }, + ) + }, } }, }, @@ -2639,24 +2789,27 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - values: { - DEFAULT: '1px', - '2': '2px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'border-block': (value, { modifier }) => ({ + '--my-modifier': modifier ?? 'none', + 'border-block-width': value, + }), }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, - modifiers: 'any', - }, - ) + modifiers: 'any', + }, + ) + }, } }, }, @@ -2706,26 +2859,29 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - values: { - DEFAULT: '1px', - '2': '2px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'border-block': (value, { modifier }) => ({ + '--my-modifier': modifier ?? 'none', + 'border-block-width': value, + }), }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, - modifiers: { - foo: 'foo', + modifiers: { + foo: 'foo', + }, }, - }, - ) + ) + }, } }, }, @@ -2777,21 +2933,24 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { type: ['color', 'any'] }, - ) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { type: ['color', 'any'] }, + ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'] }, - ) + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, + { type: ['length'] }, + ) + }, } }, }, @@ -2829,21 +2988,24 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ '--scrollbar-angle': value }), - }, - { type: ['angle', 'any'] }, - ) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ '--scrollbar-angle': value }), + }, + { type: ['angle', 'any'] }, + ) - matchUtilities( - { - scrollbar: (value) => ({ '--scrollbar-width': value }), - }, - { type: ['length'] }, - ) + matchUtilities( + { + scrollbar: (value) => ({ '--scrollbar-width': value }), + }, + { type: ['length'] }, + ) + }, } }, }, @@ -2864,21 +3026,24 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { type: ['color', 'any'], modifiers: { foo: 'foo' } }, - ) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { type: ['color', 'any'], modifiers: { foo: 'foo' } }, + ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'], modifiers: { bar: 'bar' } }, - ) + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, + { type: ['length'], modifiers: { bar: 'bar' } }, + ) + }, } }, }, @@ -2905,31 +3070,34 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { - type: ['color', 'any'], - values: { - black: 'black', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), }, - }, - ) + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, + ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { - type: ['length'], - values: { - 2: '2px', + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), }, - }, - ) + { + type: ['length'], + values: { + 2: '2px', + }, + }, + ) + }, } }, }, @@ -3025,19 +3193,22 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { - type: ['color', 'any'], - values: { - black: 'black', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), }, - }, - ) + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, + ) + }, } }, }, @@ -3099,23 +3270,26 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value, { modifier }) => ({ - '--modifier': modifier ?? 'none', - 'scrollbar-width': value, - }), - }, - { - type: ['any'], - values: {}, - modifiers: { - foo: 'foo', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value, { modifier }) => ({ + '--modifier': modifier ?? 'none', + 'scrollbar-width': value, + }), }, - }, - ) + { + type: ['any'], + values: {}, + modifiers: { + foo: 'foo', + }, + }, + ) + }, } }, }, @@ -3154,21 +3328,24 @@ describe('matchUtilities()', () => { } `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - foo: (value) => ({ - '--foo': value, - [`@apply flex`]: {}, - }), - }, - { - values: { - bar: 'bar', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + foo: (value) => ({ + '--foo': value, + [`@apply flex`]: {}, + }), }, - }, - ) + { + values: { + bar: 'bar', + }, + }, + ) + }, } }, }, @@ -3220,14 +3397,17 @@ describe('matchUtilities()', () => { `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities({ - '.text-trim > *': () => ({ - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }), - }) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities({ + '.text-trim > *': () => ({ + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }), + }) + }, } }, }, @@ -3244,29 +3424,32 @@ describe('addComponents()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addComponents }: PluginAPI) => { - addComponents({ - '.btn': { - padding: '.5rem 1rem', - borderRadius: '.25rem', - fontWeight: '600', - }, - '.btn-blue': { - backgroundColor: '#3490dc', - color: '#fff', - '&:hover': { - backgroundColor: '#2779bd', + async loadModule(id, base) { + return { + base, + module: ({ addComponents }: PluginAPI) => { + addComponents({ + '.btn': { + padding: '.5rem 1rem', + borderRadius: '.25rem', + fontWeight: '600', }, - }, - '.btn-red': { - backgroundColor: '#e3342f', - color: '#fff', - '&:hover': { - backgroundColor: '#cc1f1a', + '.btn-blue': { + backgroundColor: '#3490dc', + color: '#fff', + '&:hover': { + backgroundColor: '#2779bd', + }, }, - }, - }) + '.btn-red': { + backgroundColor: '#e3342f', + color: '#fff', + '&:hover': { + backgroundColor: '#cc1f1a', + }, + }, + }) + }, } }, }, @@ -3309,9 +3492,12 @@ describe('prefix()', () => { @plugin "my-plugin"; `, { - async loadPlugin() { - return ({ prefix }: PluginAPI) => { - fn(prefix('btn')) + async loadModule(id, base) { + return { + base, + module: ({ prefix }: PluginAPI) => { + fn(prefix('btn')) + }, } }, }, From b59ccc9c8ecc116f95c6e5a88a314ec349e55d52 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 15:17:42 +0200 Subject: [PATCH 42/51] Remove .only --- packages/tailwindcss/src/at-import.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 7ed0b6caef08..97ca16f74e59 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -102,7 +102,7 @@ let resolver = async (id: string) => { } // Examples from https://developer.mozilla.org/en-US/docs/Web/CSS/@import -test.only.each([ +test.each([ // url extraction [ css` From 0dd3a38700d8ce522b28f5b879a7e3e21d261e02 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 20 Sep 2024 15:18:46 +0200 Subject: [PATCH 43/51] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcda15312b0..b8c3e59ed84e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Disallow negative bare values in core utilities and variants ([#14453](https://github.com/tailwindlabs/tailwindcss/pull/14453)) - Preserve explicit shadow color when overriding shadow size ([#14458](https://github.com/tailwindlabs/tailwindcss/pull/14458)) - Preserve explicit transition duration and timing function when overriding transition property ([#14490](https://github.com/tailwindlabs/tailwindcss/pull/14490)) +- Change the implementation for `@import` resolution to speed up initial builds ([#14446](https://github.com/tailwindlabs/tailwindcss/pull/14446)) ## [4.0.0-alpha.24] - 2024-09-11 From b06d194f2a8e2610b4a691dd2e949bbfa17b9b7d Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 11:23:36 +0200 Subject: [PATCH 44/51] Add comment regarding recursion --- packages/tailwindcss/src/at-import.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index f3e00e3430c7..d93e03031a6c 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -30,7 +30,10 @@ export async function substituteAtImports( promises.push( (async () => { - if (recurseCount > 50) { + // Since we do not have fully resolved paths in core, we can't reliably detect circular + // imports. Instead, we try to limit the recursion depth to a number that is too large + // to be reached in practice. + if (recurseCount > 100) { throw new Error( `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, ) From 69f4ffcaba9f41cb764028fca8fc449c83f735fc Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 11:25:07 +0200 Subject: [PATCH 45/51] Gate out http/https resources --- packages/tailwindcss/src/at-import.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index d93e03031a6c..45adfa938b71 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -23,8 +23,9 @@ export async function substituteAtImports( ValueParser.parse(node.selector.slice(8)), ) - // Skip importing data URIs + // Skip importing data or remote URIs if (uri.startsWith('data:')) return + if (uri.startsWith('http://') || uri.startsWith('https://')) return let contextNode = context({}, []) From ecdf002899680bd48fc896a0e1b281962bae3912 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 11:31:21 +0200 Subject: [PATCH 46/51] Make `@import url(...)` pass-through --- packages/tailwindcss/src/at-import.test.ts | 71 ++++++++++++++-------- packages/tailwindcss/src/at-import.ts | 21 ++----- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 97ca16f74e59..74ccb9bb5e9f 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -103,10 +103,10 @@ let resolver = async (id: string) => { // Examples from https://developer.mozilla.org/en-US/docs/Web/CSS/@import test.each([ - // url extraction + // path extraction [ css` - @import url('example.css'); + @import 'example.css'; `, optimizeCss(css` ${exampleCSS} @@ -114,7 +114,7 @@ test.each([ ], [ css` - @import url('./example.css'); + @import './example.css'; `, optimizeCss(css` ${exampleCSS} @@ -122,18 +122,44 @@ test.each([ ], [ css` - @import url('/example.css'); + @import '/example.css'; `, optimizeCss(css` ${exampleCSS} `), ], + + // url() imports are ignored + [ + css` + @import url('example.css'); + `, + optimizeCss(css` + @import url('example.css'); + `), + ], + [ + css` + @import url('./example.css'); + `, + optimizeCss(css` + @import url('./example.css'); + `), + ], + [ + css` + @import url('/example.css'); + `, + optimizeCss(css` + @import url('/example.css'); + `), + ], [ css` @import url(example.css); `, optimizeCss(css` - ${exampleCSS} + @import url(example.css); `), ], [ @@ -141,7 +167,7 @@ test.each([ @import url(./example.css); `, optimizeCss(css` - ${exampleCSS} + @import url(./example.css); `), ], [ @@ -149,7 +175,7 @@ test.each([ @import url(/example.css); `, optimizeCss(css` - ${exampleCSS} + @import url(/example.css); `), ], @@ -157,7 +183,7 @@ test.each([ [ // prettier-ignore css` - @ImPoRt url('example.css'); + @ImPoRt 'example.css'; `, optimizeCss(css` ${exampleCSS} @@ -167,7 +193,7 @@ test.each([ // @media [ css` - @import url('example.css') print; + @import 'example.css' print; `, optimizeCss(css` @media print { @@ -177,7 +203,7 @@ test.each([ ], [ css` - @import url('example.css') print, screen; + @import 'example.css' print, screen; `, optimizeCss(css` @media print, screen { @@ -197,7 +223,7 @@ test.each([ ], [ css` - @import url('example.css') screen and (orientation: landscape); + @import 'example.css' screen and (orientation: landscape); `, optimizeCss(css` @media screen and (orientation: landscape) { @@ -209,7 +235,7 @@ test.each([ // @supports [ css` - @import url('example.css') supports(display: grid) screen and (max-width: 400px); + @import 'example.css' supports(display: grid) screen and (max-width: 400px); `, optimizeCss(css` @supports (display: grid) { @@ -221,7 +247,7 @@ test.each([ ], [ css` - @import url('example.css') supports((not (display: grid)) and (display: flex)) screen and + @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and (max-width: 400px); `, optimizeCss(css` @@ -235,7 +261,7 @@ test.each([ [ // prettier-ignore css` - @import url('example.css') + @import 'example.css' supports((selector(h2 > p)) and (font-tech(color-COLRv1))); `, optimizeCss(css` @@ -270,19 +296,10 @@ test.each([ // unknown syntax is ignored [ css` - @import url(example.css) does-not-exist(foo); - `, - optimizeCss(css` - @import url(example.css) does-not-exist(foo); - `), - ], - // prettier-ignore - [ - css` - @import url('example.css' url-mod); + @import 'example.css' does-not-exist(foo); `, optimizeCss(css` - @import url('example.css' url-mod); + @import 'example.css' does-not-exist(foo); `), ], ])('resolves %s', async (input, output) => { @@ -430,7 +447,7 @@ test('it crashes when inside a cycle', async () => { let loadStylesheet = () => Promise.resolve({ content: css` - @import url('foo.css'); + @import 'foo.css'; `, base: '/root', }) @@ -438,7 +455,7 @@ test('it crashes when inside a cycle', async () => { expect( run( css` - @import url('foo.css'); + @import 'foo.css'; `, loadStylesheet, ), diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 45adfa938b71..bc51aa10335c 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -63,7 +63,10 @@ export async function substituteAtImports( await Promise.all(promises) } -// c.f. https://github.com/postcss/postcss-import/blob/master/lib/parse-statements.js +// Modified and inlined version of `parse-statements` from +// `postcss-import` +// Copyright (c) 2014 Maxime Thirouin, Jason Campbell & Kevin MÃ¥rtensson +// Released under the MIT License. function parseImportParams(params: ValueParser.ValueAstNode[]) { let uri let layer: string | null = null @@ -84,21 +87,7 @@ function parseImportParams(params: ValueParser.ValueAstNode[]) { } if (node.kind === 'function' && /^url$/i.test(node.value)) { - if (uri) throw new Error("Multiple url's") - - if (!node.nodes?.[0]?.value) throw new Error('Unable to find uri') - if (node.nodes.length > 1) throw new Error('Unable to find uri') - - let uriCandidate = node.nodes[0].value - if ( - (uriCandidate.at(0) === '"' && uriCandidate.at(-1) === '"') || - (uriCandidate.at(0) === "'" && uriCandidate.at(-1) === "'") - ) { - uri = node.nodes[0].value.slice(1, -1) - } else { - uri = node.nodes[0].value - } - continue + throw new Error('url functions are not supported') } if (!uri) throw new Error('Unable to find uri') From 0763a2547d4b4b194cc16de3d093688eaaebfb66 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 12:19:11 +0200 Subject: [PATCH 47/51] Clean up tests --- packages/tailwindcss/package.json | 3 +- packages/tailwindcss/src/at-import.test.ts | 559 ++++++++++++--------- pnpm-lock.yaml | 3 + 3 files changed, 338 insertions(+), 227 deletions(-) diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 7173e7201608..10e45bdb3aa1 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -89,6 +89,7 @@ "devDependencies": { "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", - "lightningcss": "catalog:" + "lightningcss": "catalog:", + "dedent": "1.5.3" } } diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 74ccb9bb5e9f..bc42ff252550 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -8,21 +8,29 @@ let css = String.raw async function run( css: string, - loadStylesheet: (id: string, base: string) => Promise<{ content: string; base: string }>, - loadModule: ( - id: string, - base: string, - resourceHint: 'plugin' | 'config', - ) => Promise<{ module: Config | Plugin; base: string }> = () => - Promise.reject(new Error('Unexpected module')), - candidates: string[] = [], + { + loadStylesheet = () => Promise.reject(new Error('Unexpected stylesheet')), + loadModule = () => Promise.reject(new Error('Unexpected module')), + candidates = [], + optimize = true, + }: { + loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> + loadModule?: ( + id: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: Config | Plugin; base: string }> + candidates?: string[] + optimize?: boolean + }, ) { let compiler = await compile(css, { base: '/root', loadStylesheet, loadModule }) - return optimizeCss(compiler.build(candidates)) + let result = compiler.build(candidates) + return optimize ? optimizeCss(result) : result } test('can resolve relative @imports', async () => { - let resolver = async (id: string, base: string) => { + let loadStylesheet = async (id: string, base: string) => { expect(base).toBe('/root') expect(id).toBe('./foo/bar.css') return { @@ -40,7 +48,7 @@ test('can resolve relative @imports', async () => { css` @import './foo/bar.css'; `, - resolver, + { loadStylesheet }, ), ).resolves.toMatchInlineSnapshot(` ".foo { @@ -51,7 +59,7 @@ test('can resolve relative @imports', async () => { }) test('can recursively resolve relative @imports', async () => { - let resolver = async (id: string, base: string) => { + let loadStylesheet = async (id: string, base: string) => { if (base === '/root' && id === './foo/bar.css') { return { content: css` @@ -78,7 +86,7 @@ test('can recursively resolve relative @imports', async () => { css` @import './foo/bar.css'; `, - resolver, + { loadStylesheet }, ), ).resolves.toMatchInlineSnapshot(` ".baz { @@ -93,7 +101,7 @@ let exampleCSS = css` color: red; } ` -let resolver = async (id: string) => { +let loadStylesheet = async (id: string) => { if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id) return { content: exampleCSS, @@ -101,209 +109,311 @@ let resolver = async (id: string) => { } } -// Examples from https://developer.mozilla.org/en-US/docs/Web/CSS/@import -test.each([ - // path extraction - [ - css` - @import 'example.css'; - `, - optimizeCss(css` - ${exampleCSS} - `), - ], - [ - css` - @import './example.css'; - `, - optimizeCss(css` - ${exampleCSS} - `), - ], - [ - css` - @import '/example.css'; - `, - optimizeCss(css` - ${exampleCSS} - `), - ], +test('extracts path from @import nodes', async () => { + await expect( + run( + css` + @import 'example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) - // url() imports are ignored - [ - css` - @import url('example.css'); - `, - optimizeCss(css` - @import url('example.css'); - `), - ], - [ - css` - @import url('./example.css'); - `, - optimizeCss(css` - @import url('./example.css'); - `), - ], - [ - css` - @import url('/example.css'); - `, - optimizeCss(css` - @import url('/example.css'); - `), - ], - [ - css` - @import url(example.css); - `, - optimizeCss(css` - @import url(example.css); - `), - ], - [ - css` - @import url(./example.css); - `, - optimizeCss(css` - @import url(./example.css); - `), - ], - [ - css` - @import url(/example.css); - `, - optimizeCss(css` - @import url(/example.css); - `), - ], - - // handles case-insensitive `@import` directive - [ - // prettier-ignore - css` - @ImPoRt 'example.css'; - `, - optimizeCss(css` - ${exampleCSS} - `), - ], + await expect( + run( + css` + @import './example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) - // @media - [ - css` - @import 'example.css' print; - `, - optimizeCss(css` - @media print { - ${exampleCSS} + await expect( + run( + css` + @import '/example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) +}) + +test('url() imports are passed-through', async () => { + await expect( + run( + css` + @import url('example.css'); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url('example.css'); + " + `) + + await expect( + run( + css` + @import url('./example.css'); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url('./example.css'); + " + `) + + await expect( + run( + css` + @import url('/example.css'); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url('/example.css'); + " + `) + + await expect( + run( + css` + @import url(example.css); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url(example.css); + " + `) + + await expect( + run( + css` + @import url(./example.css); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url(./example.css); + " + `) + + await expect( + run( + css` + @import url(/example.css); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url(/example.css); + " + `) +}) + +test('handles case-insensitive @import directive', async () => { + await expect( + run( + css` + @import 'example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) +}) + +test('@media', async () => { + await expect( + run( + css` + @import 'example.css' print; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media print { + a { + color: red; } - `), - ], - [ - css` - @import 'example.css' print, screen; - `, - optimizeCss(css` - @media print, screen { - ${exampleCSS} + } + " + `) + + await expect( + run( + css` + @import 'example.css' print, screen; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media print, screen { + a { + color: red; } - `), - ], - [ - css` - @import 'example.css' screen; - `, - optimizeCss(css` - @media screen { - ${exampleCSS} + } + " + `) + + await expect( + run( + css` + @import 'example.css' screen and (orientation: landscape); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media screen and (orientation: landscape) { + a { + color: red; } - `), - ], - [ - css` - @import 'example.css' screen and (orientation: landscape); - `, - optimizeCss(css` - @media screen and (orientation: landscape) { - ${exampleCSS} + } + " + `) + + await expect( + run( + css` + @import 'example.css' foo(bar); + `, + { loadStylesheet, optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media foo(bar) { + a { + color: red; } - `), - ], + } + " + `) +}) - // @supports - [ - css` - @import 'example.css' supports(display: grid) screen and (max-width: 400px); - `, - optimizeCss(css` - @supports (display: grid) { - @media screen and (max-width: 400px) { - ${exampleCSS} - } +test('@supports', async () => { + await expect( + run( + css` + @import 'example.css' supports(display: grid); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { + a { + color: red; } - `), - ], - [ - css` - @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and - (max-width: 400px); - `, - optimizeCss(css` - @supports (not (display: grid)) and (display: flex) { - @media screen and (max-width: 400px) { - ${exampleCSS} + } + " + `) + + await expect( + run( + css` + @import 'example.css' supports(display: grid) screen and (max-width: 400px); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { + @media screen and (width <= 400px) { + a { + color: red; } } - `), - ], - [ - // prettier-ignore - css` - @import 'example.css' - supports((selector(h2 > p)) and (font-tech(color-COLRv1))); - `, - optimizeCss(css` - @supports (selector(h2 > p)) and (font-tech(color-COLRv1)) { - ${exampleCSS} + } + " + `) + + await expect( + run( + css` + @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and + (max-width: 400px); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports (not (display: grid)) and (display: flex) { + @media screen and (width <= 400px) { + a { + color: red; + } } - `), - ], + } + " + `) - // @layer - [ - css` - @import 'example.css' layer(utilities); - `, - optimizeCss(css` - @layer utilities { - ${exampleCSS} + await expect( + run( + // prettier-ignore + css` + @import 'example.css' + supports((selector(h2 > p)) and (font-tech(color-COLRv1))); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports selector(h2 > p) and font-tech(color-COLRv1) { + a { + color: red; } - `), - ], - [ - css` - @import 'example.css' layer(); - `, - optimizeCss(css` - @layer { - ${exampleCSS} + } + " + `) +}) + +test('@layer', async () => { + await expect( + run( + css` + @import 'example.css' layer(utilities); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@layer utilities { + a { + color: red; } - `), - ], + } + " + `) - // unknown syntax is ignored - [ - css` - @import 'example.css' does-not-exist(foo); - `, - optimizeCss(css` - @import 'example.css' does-not-exist(foo); - `), - ], -])('resolves %s', async (input, output) => { - await expect(run(input, resolver)).resolves.toBe(output) + await expect( + run( + css` + @import 'example.css' layer(); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@layer { + a { + color: red; + } + } + " + `) }) test('supports theme(reference) imports', async () => { @@ -313,25 +423,25 @@ test('supports theme(reference) imports', async () => { @tailwind utilities; @import 'example.css' theme(reference); `, - () => - Promise.resolve({ - content: css` - @theme { - --color-red-500: red; - } - `, - base: '', - }), - () => Promise.reject(new Error('Unexpected module')), - ['text-red-500'], + { + loadStylesheet: () => + Promise.resolve({ + content: css` + @theme { + --color-red-500: red; + } + `, + base: '', + }), + candidates: ['text-red-500'], + }, ), - ).resolves.toBe( - optimizeCss(css` - .text-red-500 { - color: var(--color-red-500, red); - } - `), - ) + ).resolves.toMatchInlineSnapshot(` + ".text-red-500 { + color: var(--color-red-500, red); + } + " + `) }) test('updates the base when loading modules inside nested files', async () => { @@ -353,8 +463,7 @@ test('updates the base when loading modules inside nested files', async () => { @config './root-config.js'; @plugin './root-plugin.js'; `, - loadStylesheet, - loadModule, + { loadStylesheet, loadModule }, ) ).trim(), ).toBe('') @@ -457,9 +566,7 @@ test('it crashes when inside a cycle', async () => { css` @import 'foo.css'; `, - loadStylesheet, + { loadStylesheet }, ), - ).rejects.toMatchInlineSnapshot( - `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, - ) + ).rejects.toMatchInlineSnapshot(`[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eeaae1c5eacf..7a1f10b82655 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -334,6 +334,9 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 + dedent: + specifier: 1.5.3 + version: 1.5.3 lightningcss: specifier: 'catalog:' version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) From e6ee1dc3e9d776a29d561cdf595f3f45eff49402 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 13:22:48 +0200 Subject: [PATCH 48/51] Change context type --- packages/tailwindcss/src/ast.ts | 8 ++++---- packages/tailwindcss/src/compat/apply-compat-hooks.ts | 4 ++-- packages/tailwindcss/src/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 8707bcff7d96..afc1888c2659 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -18,7 +18,7 @@ export type Comment = { export type Context = { kind: 'context' - context: Record + context: Record nodes: AstNode[] } @@ -48,7 +48,7 @@ export function comment(value: string): Comment { } } -export function context(context: Record, nodes: AstNode[]): Context { +export function context(context: Record, nodes: AstNode[]): Context { return { kind: 'context', context, @@ -74,11 +74,11 @@ export function walk( utils: { parent: AstNode | null replaceWith(newNode: AstNode | AstNode[]): void - context: Record + context: Record }, ) => void | WalkAction, parent: AstNode | null = null, - context: Record = {}, + context: Record = {}, ) { for (let i = 0; i < ast.length; i++) { let node = ast[i] diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 9e85faf9675d..ca211688b95b 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -89,7 +89,7 @@ export async function applyCompatibilityHooks({ } pluginPaths.push([ - { id: pluginPath, base: (context as any).base }, + { id: pluginPath, base: context.base }, Object.keys(options).length > 0 ? options : null, ]) @@ -107,7 +107,7 @@ export async function applyCompatibilityHooks({ throw new Error('`@config` cannot be nested.') } - configPaths.push({ id: node.selector.slice(9, -1), base: (context as any).base }) + configPaths.push({ id: node.selector.slice(9, -1), base: context.base }) replaceWith([]) return } diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index cdaeed908e0e..f19c47d6bc59 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -126,7 +126,7 @@ async function parseCss( ) { throw new Error('`@source` paths must be quoted.') } - globs.push({ base: context.base as string, pattern: path.slice(1, -1) }) + globs.push({ base: context.base, pattern: path.slice(1, -1) }) replaceWith([]) return } From 2174a5a1ecaf87896baa921376bb467c4229aec9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 13:25:43 +0200 Subject: [PATCH 49/51] Remove `//` --- packages/tailwindcss/src/at-import.test.ts | 6 +++--- packages/tailwindcss/src/compat/apply-compat-hooks.ts | 6 ++++-- packages/tailwindcss/src/index.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index bc42ff252550..bbd5e6863f26 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -492,7 +492,6 @@ test('emits the right base for @source directives inside nested files', async () ) expect(compiler.globs).toEqual([ - // { pattern: './nested/**/*.css', base: '/root/foo' }, { pattern: './root/**/*.css', base: '/root' }, ]) @@ -540,7 +539,6 @@ test('emits the right base for @source found inside JS configs and plugins from ) expect(compiler.globs).toEqual([ - // { pattern: './nested-plugin/*.html', base: '/root/foo-plugin' }, { pattern: './root-plugin/*.html', base: '/root-plugin' }, @@ -568,5 +566,7 @@ test('it crashes when inside a cycle', async () => { `, { loadStylesheet }, ), - ).rejects.toMatchInlineSnapshot(`[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`) + ).rejects.toMatchInlineSnapshot( + `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, + ) }) diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index ca211688b95b..4290c136ab5e 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -15,11 +15,13 @@ import { registerThemeVariantOverrides } from './theme-variants' export async function applyCompatibilityHooks({ designSystem, + base, ast, loadModule, globs, }: { designSystem: DesignSystem + base: string ast: AstNode[] loadModule: ( path: string, @@ -186,9 +188,9 @@ export async function applyCompatibilityHooks({ let userConfig = [...pluginConfigs, ...configs] let resolvedConfig = resolveConfig(designSystem, [ - { config: createCompatConfig(designSystem.theme), base: '' }, + { config: createCompatConfig(designSystem.theme), base }, ...userConfig, - { config: { plugins: [darkModePlugin] }, base: '' }, + { config: { plugins: [darkModePlugin] }, base }, ]) let resolvedUserConfig = resolveConfig(designSystem, userConfig) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index f19c47d6bc59..6ded20cd4dc0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -256,7 +256,7 @@ async function parseCss( // 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 // compatible without polluting core. - await applyCompatibilityHooks({ designSystem, ast, loadModule, globs }) + await applyCompatibilityHooks({ designSystem, base, ast, loadModule, globs }) for (let customVariant of customVariants) { customVariant(designSystem) From 8afbcdc9aaa2f2b467aff9559dc92cdb890a78d5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 13:31:15 +0200 Subject: [PATCH 50/51] Avoid two-pass resolution for some cases --- packages/@tailwindcss-node/src/compile.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index cfa5af17d527..02332d578262 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' import fs from 'node:fs' import fsPromises from 'node:fs/promises' -import path, { dirname } from 'node:path' +import path, { dirname, extname } from 'node:path' import { pathToFileURL } from 'node:url' import { compile as _compile } from 'tailwindcss' import { getModuleDependencies } from './get-module-dependencies' @@ -100,13 +100,22 @@ async function resolveCssId(id: string, base: string): Promise Date: Mon, 23 Sep 2024 15:46:05 +0200 Subject: [PATCH 51/51] Don't use regex for case-insensitive string comparisons --- packages/tailwindcss/src/at-import.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index bc51aa10335c..a06000826b05 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -86,13 +86,16 @@ function parseImportParams(params: ValueParser.ValueAstNode[]) { continue } - if (node.kind === 'function' && /^url$/i.test(node.value)) { + if (node.kind === 'function' && node.value.toLowerCase() === 'url') { throw new Error('url functions are not supported') } if (!uri) throw new Error('Unable to find uri') - if ((node.kind === 'word' || node.kind === 'function') && /^layer$/i.test(node.value)) { + if ( + (node.kind === 'word' || node.kind === 'function') && + node.value.toLowerCase() === 'layer' + ) { if (layer) throw new Error('Multiple layers') if (supports) throw new Error('layers must be defined before support conditions') @@ -105,7 +108,7 @@ function parseImportParams(params: ValueParser.ValueAstNode[]) { continue } - if (node.kind === 'function' && /^supports$/i.test(node.value)) { + if (node.kind === 'function' && node.value.toLowerCase() === 'supports') { if (supports) throw new Error('Multiple support conditions') supports = ValueParser.toCss(node.nodes) continue