Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vite: Use Vite resolvers for CSS and JS files #15173

Merged
merged 3 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Ensure the Vite plugin resolves CSS and JS files according to the configured resolver conditions ([#15173])(https://github.com/tailwindlabs/tailwindcss/pull/15173)
- _Upgrade (experimental)_: Migrate prefixes for `.group` and `.peer` classes ([#15208](https://github.com/tailwindlabs/tailwindcss/pull/15208))

### Fixed
Expand Down
143 changes: 143 additions & 0 deletions integrations/vite/resolvers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, expect } from 'vitest'
import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils'

for (let transformer of ['postcss', 'lightningcss']) {
describe(transformer, () => {
test(
`resolves aliases in production build`,
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'

export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
alias: {
'#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
'#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
},
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline custom-underline">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import '#css-alias';
@plugin '#js-alias';
`,
'src/alias.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
`,
'src/plugin.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
}
`,
},
},
async ({ fs, exec }) => {
await exec('pnpm vite build')

let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]

await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
},
)

test(
`resolves aliases in dev mode`,
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'

export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
alias: {
'#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
'#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
},
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline custom-underline">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import '#css-alias';
@plugin '#js-alias';
`,
'src/alias.css': css`
@import 'tailwindcss/theme' theme(reference);
@import 'tailwindcss/utilities';
`,
'src/plugin.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
}
`,
},
},
async ({ root, spawn, getFreePort, fs }) => {
let port = await getFreePort()
await spawn(`pnpm vite dev --port ${port}`)

await retryAssertion(async () => {
let styles = await fetchStyles(port, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`custom-underline`)
})
},
)
})
}
59 changes: 50 additions & 9 deletions packages/@tailwindcss-node/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,33 @@ import {
import { getModuleDependencies } from './get-module-dependencies'
import { rewriteUrls } from './urls'

export type Resolver = (id: string, base: string) => Promise<string | false | undefined>

export async function compile(
css: string,
{
base,
onDependency,
shouldRewriteUrls,

customCssResolver,
customJsResolver,
}: {
base: string
onDependency: (path: string) => void
shouldRewriteUrls?: boolean

customCssResolver?: Resolver
customJsResolver?: Resolver
},
) {
let compiler = await _compile(css, {
base,
async loadModule(id, base) {
return loadModule(id, base, onDependency)
return loadModule(id, base, onDependency, customJsResolver)
},
async loadStylesheet(id, base) {
let sheet = await loadStylesheet(id, base, onDependency)
let sheet = await loadStylesheet(id, base, onDependency, customCssResolver)

if (shouldRewriteUrls) {
sheet.content = await rewriteUrls({
Expand Down Expand Up @@ -80,9 +88,14 @@ export async function __unstable__loadDesignSystem(css: string, { base }: { base
})
}

export async function loadModule(id: string, base: string, onDependency: (path: string) => void) {
export async function loadModule(
id: string,
base: string,
onDependency: (path: string) => void,
customJsResolver?: Resolver,
) {
if (id[0] !== '.') {
let resolvedPath = await resolveJsId(id, base)
let resolvedPath = await resolveJsId(id, base, customJsResolver)
if (!resolvedPath) {
throw new Error(`Could not resolve '${id}' from '${base}'`)
}
Expand All @@ -94,7 +107,7 @@ export async function loadModule(id: string, base: string, onDependency: (path:
}
}

let resolvedPath = await resolveJsId(id, base)
let resolvedPath = await resolveJsId(id, base, customJsResolver)
if (!resolvedPath) {
throw new Error(`Could not resolve '${id}' from '${base}'`)
}
Expand All @@ -113,8 +126,13 @@ export async function loadModule(id: string, base: string, onDependency: (path:
}
}

async function loadStylesheet(id: string, base: string, onDependency: (path: string) => void) {
let resolvedPath = await resolveCssId(id, base)
async function loadStylesheet(
id: string,
base: string,
onDependency: (path: string) => void,
cssResolver?: Resolver,
) {
let resolvedPath = await resolveCssId(id, base, cssResolver)
if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${base}'`)

onDependency(resolvedPath)
Expand Down Expand Up @@ -163,14 +181,25 @@ const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
mainFields: ['style'],
conditionNames: ['style'],
})
async function resolveCssId(id: string, base: string): Promise<string | false | undefined> {
async function resolveCssId(
id: string,
base: string,
customCssResolver?: Resolver,
): Promise<string | false | undefined> {
if (typeof globalThis.__tw_resolve === 'function') {
let resolved = globalThis.__tw_resolve(id, base)
if (resolved) {
return Promise.resolve(resolved)
}
}

if (customCssResolver) {
let customResolution = await customCssResolver(id, base)
if (customResolution) {
return customResolution
}
}

return runResolver(cssResolver, id, base)
}

Expand All @@ -188,13 +217,25 @@ const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
conditionNames: ['node', 'require'],
})

function resolveJsId(id: string, base: string): Promise<string | false | undefined> {
async function resolveJsId(
id: string,
base: string,
customJsResolver?: Resolver,
): Promise<string | false | undefined> {
if (typeof globalThis.__tw_resolve === 'function') {
let resolved = globalThis.__tw_resolve(id, base)
if (resolved) {
return Promise.resolve(resolved)
}
}

if (customJsResolver) {
let customResolution = await customJsResolver(id, base)
if (customResolution) {
return customResolution
}
}

return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
}

Expand Down
34 changes: 31 additions & 3 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,31 @@ export default function tailwindcss(): Plugin[] {
let moduleGraphCandidates = new DefaultMap<string, Set<string>>(() => new Set<string>())
let moduleGraphScanner = new Scanner({})

let roots: DefaultMap<string, Root> = new DefaultMap(
(id) => new Root(id, () => moduleGraphCandidates, config!.base),
)
let roots: DefaultMap<string, Root> = new DefaultMap((id) => {
let cssResolver = config!.createResolver({
...config!.resolve,
extensions: ['.css'],
mainFields: ['style'],
conditions: ['style', 'development|production'],
tryIndex: false,
preferRelative: true,
})
function customCssResolver(id: string, base: string) {
return cssResolver(id, base, false, isSSR)
}

let jsResolver = config!.createResolver(config!.resolve)
function customJsResolver(id: string, base: string) {
return jsResolver(id, base, true, isSSR)
}
return new Root(
id,
() => moduleGraphCandidates,
config!.base,
customCssResolver,
customJsResolver,
)
})

function scanFile(id: string, content: string, extension: string, isSSR: boolean) {
let updated = false
Expand Down Expand Up @@ -423,6 +445,9 @@ class Root {
private id: string,
private getSharedCandidates: () => Map<string, Set<string>>,
private base: string,

private customCssResolver: (id: string, base: string) => Promise<string | false | undefined>,
private customJsResolver: (id: string, base: string) => Promise<string | false | undefined>,
) {}

// Generate the CSS for the root file. This can return false if the file is
Expand All @@ -448,6 +473,9 @@ class Root {
addWatchFile(path)
this.dependencies.add(path)
},

customCssResolver: this.customCssResolver,
customJsResolver: this.customJsResolver,
})
env.DEBUG && console.timeEnd('[@tailwindcss/vite] Setup compiler')

Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.