Simplify Vite plugin and handle all CSS transforms in pre-transform hook
This commit is contained in:
parent
5789eb789d
commit
f0f7418785
7 changed files with 469 additions and 314 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import path from 'node:path'
|
||||
import { expect } from 'vitest'
|
||||
import { describe, expect } from 'vitest'
|
||||
import {
|
||||
candidate,
|
||||
css,
|
||||
|
|
@ -12,206 +12,342 @@ import {
|
|||
ts,
|
||||
yaml,
|
||||
} from '../utils'
|
||||
;['postcss', 'lightningcss'].forEach((transformer) => {
|
||||
describe.concurrent(transformer, () => {
|
||||
test(
|
||||
`production build`,
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.3.5"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
test(
|
||||
'production build',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline m-2">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.3.5"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/theme' theme(reference);
|
||||
@import 'tailwindcss/utilities';
|
||||
@source '../../project-b/src/**/*.js';
|
||||
`,
|
||||
'project-b/src/index.js': js`
|
||||
const className = "content-['project-b/src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, fs, exec }) => {
|
||||
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
let files = await fs.glob('project-a/dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [
|
||||
candidate`underline`,
|
||||
candidate`m-2`,
|
||||
candidate`content-['project-b/src/index.js']`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
`dev mode`,
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.3.5"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/about.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="font-bold">Tailwind Labs</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/theme' theme(reference);
|
||||
@import 'tailwindcss/utilities';
|
||||
@source '../../project-b/src/**/*.js';
|
||||
`,
|
||||
'project-b/src/index.js': js`
|
||||
const className = "content-['project-b/src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, getFreePort, fs }) => {
|
||||
let port = await getFreePort()
|
||||
await spawn(`pnpm vite dev --port ${port}`, {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline m-2">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/theme' theme(reference);
|
||||
@import 'tailwindcss/utilities';
|
||||
@source '../../project-b/src/**/*.js';
|
||||
`,
|
||||
'project-b/src/index.js': js`
|
||||
const className = "content-['project-b/src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, fs, exec }) => {
|
||||
await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
|
||||
|
||||
let files = await fs.glob('project-a/dist/**/*.css')
|
||||
expect(files).toHaveLength(1)
|
||||
let [filename] = files[0]
|
||||
|
||||
await fs.expectFileToContain(filename, [
|
||||
candidate`underline`,
|
||||
candidate`m-2`,
|
||||
candidate`content-['project-b/src/index.js']`,
|
||||
])
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'dev mode',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`{}`,
|
||||
'pnpm-workspace.yaml': yaml`
|
||||
#
|
||||
packages:
|
||||
- project-a
|
||||
`,
|
||||
'project-a/package.json': json`
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.3.5"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'project-a/vite.config.ts': ts`
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: { cssMinify: false },
|
||||
plugins: [tailwindcss()],
|
||||
// Candidates are resolved lazily, so the first visit of index.html
|
||||
// will only have candidates from this file.
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port, '/index.html')
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).not.toContain(candidate`font-bold`)
|
||||
})
|
||||
`,
|
||||
'project-a/index.html': html`
|
||||
|
||||
// Going to about.html will extend the candidate list to include
|
||||
// candidates from about.html.
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port, '/about.html')
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
})
|
||||
|
||||
// Updates are additive and cause new candidates to be added.
|
||||
await fs.write(
|
||||
'project-a/index.html',
|
||||
html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline">Hello, world!</div>
|
||||
</body>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline m-2">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/about.html': html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="font-bold ">Tailwind Labs</div>
|
||||
</body>
|
||||
`,
|
||||
'project-a/src/index.css': css`
|
||||
@import 'tailwindcss/theme' theme(reference);
|
||||
@import 'tailwindcss/utilities';
|
||||
@source '../../project-b/src/**/*.js';
|
||||
`,
|
||||
'project-b/src/index.js': js`
|
||||
const className = "content-['project-b/src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ root, spawn, getFreePort, fs }) => {
|
||||
let port = await getFreePort()
|
||||
await spawn(`pnpm vite dev --port ${port}`, {
|
||||
cwd: path.join(root, 'project-a'),
|
||||
})
|
||||
)
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
expect(css).toContain(candidate`m-2`)
|
||||
})
|
||||
|
||||
// Candidates are resolved lazily, so the first visit of index.html
|
||||
// will only have candidates from this file.
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port, '/index.html')
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).not.toContain(candidate`font-bold`)
|
||||
})
|
||||
// Manually added `@source`s are watched and trigger a rebuild
|
||||
await fs.write(
|
||||
'project-b/src/index.js',
|
||||
js`
|
||||
const className = "[.changed_&]:content-['project-b/src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
)
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
expect(css).toContain(candidate`m-2`)
|
||||
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
|
||||
})
|
||||
|
||||
// Going to about.html will extend the candidate list to include
|
||||
// candidates from about.html.
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port, '/about.html')
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
})
|
||||
// After updates to the CSS file, all previous candidates should still be in
|
||||
// the generated CSS
|
||||
await fs.write(
|
||||
'project-a/src/index.css',
|
||||
css`
|
||||
${await fs.read('project-a/src/index.css')}
|
||||
|
||||
// Updates are additive and cause new candidates to be added.
|
||||
await fs.write(
|
||||
'project-a/index.html',
|
||||
html`
|
||||
<head>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="underline m-2">Hello, world!</div>
|
||||
</body>
|
||||
`,
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
)
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port)
|
||||
expect(css).toContain(candidate`red`)
|
||||
expect(css).toContain(candidate`m-2`)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
})
|
||||
},
|
||||
)
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
expect(css).toContain(candidate`m-2`)
|
||||
})
|
||||
|
||||
// Manually added `@source`s are watched and trigger a rebuild
|
||||
await fs.write(
|
||||
'project-b/src/index.js',
|
||||
js`
|
||||
const className = "[.changed_&]:content-['project-b/src/index.js']"
|
||||
module.exports = { className }
|
||||
`,
|
||||
)
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
expect(css).toContain(candidate`m-2`)
|
||||
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
|
||||
})
|
||||
// test(
|
||||
// 'watch mode',
|
||||
// {
|
||||
// fs: {
|
||||
// 'package.json': json`{}`,
|
||||
// 'pnpm-workspace.yaml': yaml`
|
||||
// #
|
||||
// packages:
|
||||
// - project-a
|
||||
// `,
|
||||
// 'project-a/package.json': json`
|
||||
// {
|
||||
// "type": "module",
|
||||
// "dependencies": {
|
||||
// "@tailwindcss/vite": "workspace:^",
|
||||
// "tailwindcss": "workspace:^"
|
||||
// },
|
||||
// "devDependencies": {
|
||||
// "vite": "^5.3.5"
|
||||
// }
|
||||
// }
|
||||
// `,
|
||||
// 'project-a/vite.config.ts': ts`
|
||||
// import tailwindcss from '@tailwindcss/vite'
|
||||
// import { defineConfig } from 'vite'
|
||||
|
||||
// After updates to the CSS file, all previous candidates should still be in
|
||||
// the generated CSS
|
||||
await fs.write(
|
||||
'project-a/src/index.css',
|
||||
css`
|
||||
${await fs.read('project-a/src/index.css')}
|
||||
// export default defineConfig({
|
||||
// build: { cssMinify: false },
|
||||
// plugins: [tailwindcss()],
|
||||
// })
|
||||
// `,
|
||||
// 'project-a/index.html': html`
|
||||
// <head>
|
||||
// <link rel="stylesheet" href="./src/index.css" />
|
||||
// </head>
|
||||
// <body>
|
||||
// <div class="underline">Hello, world!</div>
|
||||
// </body>
|
||||
// `,
|
||||
// 'project-a/src/index.css': css`
|
||||
// @import 'tailwindcss/theme' theme(reference);
|
||||
// @import 'tailwindcss/utilities';
|
||||
// @source '../../project-b/src/**/*.js';
|
||||
// `,
|
||||
// 'project-b/src/index.js': js`
|
||||
// const className = "content-['project-b/src/index.js']"
|
||||
// module.exports = { className }
|
||||
// `,
|
||||
// },
|
||||
// },
|
||||
// async ({ root, spawn, fs }) => {
|
||||
// await spawn(`pnpm vite build --watch`, {
|
||||
// cwd: path.join(root, 'project-a'),
|
||||
// })
|
||||
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
)
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(port)
|
||||
expect(css).toContain(candidate`red`)
|
||||
expect(css).toContain(candidate`m-2`)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
|
||||
expect(css).toContain(candidate`font-bold`)
|
||||
})
|
||||
},
|
||||
)
|
||||
// let filename = ''
|
||||
// await retryAssertion(async () => {
|
||||
// let files = await fs.glob('project-a/dist/**/*.css')
|
||||
// expect(files).toHaveLength(1)
|
||||
// filename = files[0][0]
|
||||
// })
|
||||
|
||||
// await fs.expectFileToContain(filename, [candidate`underline`])
|
||||
|
||||
// await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// // Updates are additive and cause new candidates to be added.
|
||||
// await fs.write(
|
||||
// 'project-a/index.html',
|
||||
// html`
|
||||
// <head>
|
||||
// <link rel="stylesheet" href="./src/index.css" />
|
||||
// </head>
|
||||
// <body>
|
||||
// <div class="underline m-2">Hello, world!</div>
|
||||
// </body>
|
||||
// `,
|
||||
// )
|
||||
// await retryAssertion(async () => {
|
||||
// let files = await fs.glob('project-a/dist/**/*.css')
|
||||
// let filenames = files.map(([filename]) => filename)
|
||||
// // Expect a rebuild to have a different .css hash
|
||||
// expect(filenames).not.toContain(filename)
|
||||
// expect(filenames).toHaveLength(1)
|
||||
// filename = filenames[0]
|
||||
// })
|
||||
// await fs.expectFileToContain(filename, [candidate`underline`, candidate`m-2`])
|
||||
|
||||
// // Manually added `@source`s are watched and trigger a rebuild
|
||||
// await fs.write(
|
||||
// 'project-b/src/index.js',
|
||||
// js`
|
||||
// const className = "[.changed_&]:content-['project-b/src/index.js']"
|
||||
// module.exports = { className }
|
||||
// `,
|
||||
// )
|
||||
// await fs.expectFileToContain(filename, [
|
||||
// candidate`underline`,
|
||||
// candidate`m-2`,
|
||||
// candidate`[.changed_&]:content-['project-b/src/index.js']`,
|
||||
// ])
|
||||
|
||||
// // After updates to the CSS file, all previous candidates should still be in
|
||||
// // the generated CSS
|
||||
// await fs.write(
|
||||
// 'project-a/src/index.css',
|
||||
// css`
|
||||
// ${await fs.read('project-a/src/index.css')}
|
||||
|
||||
// .red {
|
||||
// color: red;
|
||||
// }
|
||||
// `,
|
||||
// )
|
||||
// await retryAssertion(async () => {
|
||||
// let files = await fs.glob('project-a/dist/**/*.css')
|
||||
// let filenames = files.map(([filename]) => filename)
|
||||
// // Expect a rebuild to have a different .css hash
|
||||
// expect(filenames).not.toContain(filename)
|
||||
// expect(filenames).toHaveLength(1)
|
||||
// filename = filenames[0]
|
||||
// })
|
||||
// await fs.expectFileToContain(filename, [
|
||||
// candidate`red`,
|
||||
// candidate`underline`,
|
||||
// candidate`m-2`,
|
||||
// candidate`[.changed_&]:content-['project-b/src/index.js']`,
|
||||
// ])
|
||||
// },
|
||||
// )
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -31,13 +31,15 @@
|
|||
"@tailwindcss/node": "workspace:^",
|
||||
"@tailwindcss/oxide": "workspace:^",
|
||||
"lightningcss": "catalog:",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-import": "^16.1.0",
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"internal-postcss-fix-relative-paths": "workspace:^"
|
||||
"@types/postcss-import": "^14.0.3",
|
||||
"internal-postcss-fix-relative-paths": "workspace:^",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
|||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import fixRelativePathsPlugin, { normalizePath } from 'internal-postcss-fix-relative-paths'
|
||||
import { Features, transform } from 'lightningcss'
|
||||
import fs from 'node:fs/promises'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import path from 'path'
|
||||
import postcssrc from 'postcss-load-config'
|
||||
import postcss from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
import { compile } from 'tailwindcss'
|
||||
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
|
||||
|
||||
|
|
@ -16,23 +18,19 @@ export default function tailwindcss(): Plugin[] {
|
|||
let scanner: Scanner | null = null
|
||||
let changedContent: { content: string; extension: string }[] = []
|
||||
let candidates = new Set<string>()
|
||||
let fullRebuildPaths: string[] = []
|
||||
let fullRebuildPaths = new Set<string>()
|
||||
let rebuildStrategy: 'full' | 'incremental' = 'full'
|
||||
let compiler: null | Awaited<ReturnType<typeof compile>> = null
|
||||
|
||||
// In serve mode this is treated as a set — the content doesn't matter.
|
||||
// In build mode, we store file contents to use them in renderChunk.
|
||||
let cssModules: Record<
|
||||
string,
|
||||
{
|
||||
content: string
|
||||
handled: boolean
|
||||
}
|
||||
> = {}
|
||||
let cssModules: Record<string, { content: string; handled: boolean }> = {}
|
||||
let isSSR = false
|
||||
let minify = false
|
||||
let cssPlugins: readonly Plugin[] = []
|
||||
|
||||
// Trigger update to all CSS modules
|
||||
function updateCssModules(isSSR: boolean) {
|
||||
function queueRebuild(isSSR: boolean) {
|
||||
// If we're building then we don't need to update anything
|
||||
if (!server) return
|
||||
|
||||
|
|
@ -82,56 +80,85 @@ export default function tailwindcss(): Plugin[] {
|
|||
return updated
|
||||
}
|
||||
|
||||
function onFileChange(src: string) {
|
||||
if (fullRebuildPaths.has(idToPath(src))) {
|
||||
rebuildStrategy = 'full'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function generateCss(css: string, inputPath: string, addWatchFile: (file: string) => void) {
|
||||
await import('@tailwindcss/node/esm-cache-hook')
|
||||
|
||||
let inputBasePath = path.dirname(path.resolve(inputPath))
|
||||
clearRequireCache(fullRebuildPaths)
|
||||
fullRebuildPaths = []
|
||||
let { build, globs } = await compile(css, {
|
||||
loadPlugin: async (pluginPath) => {
|
||||
if (pluginPath[0] !== '.') {
|
||||
return import(pluginPath).then((m) => m.default ?? m)
|
||||
}
|
||||
|
||||
let resolvedPath = path.resolve(inputBasePath, pluginPath)
|
||||
let [module, moduleDependencies] = await Promise.all([
|
||||
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
|
||||
getModuleDependencies(resolvedPath),
|
||||
])
|
||||
if (compiler === null || rebuildStrategy === 'full') {
|
||||
rebuildStrategy = 'incremental'
|
||||
clearRequireCache(Array.from(fullRebuildPaths))
|
||||
fullRebuildPaths.clear()
|
||||
fullRebuildPaths.add(idToPath(inputPath))
|
||||
|
||||
addWatchFile(resolvedPath)
|
||||
fullRebuildPaths.push(resolvedPath)
|
||||
for (let file of moduleDependencies) {
|
||||
addWatchFile(file)
|
||||
fullRebuildPaths.push(file)
|
||||
}
|
||||
return module.default ?? module
|
||||
},
|
||||
// Resolve `@import`s
|
||||
let postcssCompiled = await postcss([
|
||||
postcssImport({
|
||||
load(path) {
|
||||
fullRebuildPaths.add(path)
|
||||
addWatchFile(path)
|
||||
return fs.readFile(path, 'utf8')
|
||||
},
|
||||
}),
|
||||
fixRelativePathsPlugin(),
|
||||
]).process(css, {
|
||||
from: inputPath,
|
||||
to: inputPath,
|
||||
})
|
||||
css = postcssCompiled.css
|
||||
|
||||
loadConfig: async (configPath) => {
|
||||
if (configPath[0] !== '.') {
|
||||
return import(configPath).then((m) => m.default ?? m)
|
||||
}
|
||||
compiler = await compile(css, {
|
||||
loadPlugin: async (pluginPath) => {
|
||||
if (pluginPath[0] !== '.') {
|
||||
return import(pluginPath).then((m) => m.default ?? m)
|
||||
}
|
||||
|
||||
let resolvedPath = path.resolve(inputBasePath, configPath)
|
||||
let [module, moduleDependencies] = await Promise.all([
|
||||
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
|
||||
getModuleDependencies(resolvedPath),
|
||||
])
|
||||
let resolvedPath = path.resolve(inputBasePath, pluginPath)
|
||||
let [module, moduleDependencies] = await Promise.all([
|
||||
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
|
||||
getModuleDependencies(resolvedPath),
|
||||
])
|
||||
|
||||
addWatchFile(resolvedPath)
|
||||
fullRebuildPaths.push(resolvedPath)
|
||||
for (let file of moduleDependencies) {
|
||||
addWatchFile(file)
|
||||
fullRebuildPaths.push(file)
|
||||
}
|
||||
return module.default ?? module
|
||||
},
|
||||
})
|
||||
addWatchFile(resolvedPath)
|
||||
fullRebuildPaths.add(resolvedPath)
|
||||
for (let file of moduleDependencies) {
|
||||
addWatchFile(file)
|
||||
fullRebuildPaths.add(file)
|
||||
}
|
||||
return module.default ?? module
|
||||
},
|
||||
|
||||
loadConfig: async (configPath) => {
|
||||
if (configPath[0] !== '.') {
|
||||
return import(configPath).then((m) => m.default ?? m)
|
||||
}
|
||||
|
||||
let resolvedPath = path.resolve(inputBasePath, configPath)
|
||||
let [module, moduleDependencies] = await Promise.all([
|
||||
import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
|
||||
getModuleDependencies(resolvedPath),
|
||||
])
|
||||
|
||||
addWatchFile(resolvedPath)
|
||||
fullRebuildPaths.add(resolvedPath)
|
||||
for (let file of moduleDependencies) {
|
||||
addWatchFile(file)
|
||||
fullRebuildPaths.add(file)
|
||||
}
|
||||
return module.default ?? module
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
scanner = new Scanner({
|
||||
sources: globs.map((pattern) => ({
|
||||
sources: compiler.globs.map((pattern) => ({
|
||||
base: inputBasePath, // Globs are relative to the input.css file
|
||||
pattern,
|
||||
})),
|
||||
|
|
@ -170,7 +197,7 @@ export default function tailwindcss(): Plugin[] {
|
|||
addWatchFile(path.posix.join(relative, glob.pattern))
|
||||
}
|
||||
|
||||
return build(Array.from(candidates))
|
||||
return compiler.build(Array.from(candidates))
|
||||
}
|
||||
|
||||
async function generateOptimizedCss(
|
||||
|
|
@ -239,106 +266,57 @@ export default function tailwindcss(): Plugin[] {
|
|||
...(config.command === 'build' ? ['vite:css-post'] : []),
|
||||
]
|
||||
|
||||
console.log('plugins')
|
||||
console.log(config.plugins)
|
||||
cssPlugins = config.plugins.filter((plugin) => {
|
||||
return allowedPlugins.includes(plugin.name)
|
||||
})
|
||||
},
|
||||
|
||||
// Append the postcss-fix-relative-paths plugin
|
||||
async config(config) {
|
||||
let postcssConfig = config.css?.postcss
|
||||
|
||||
if (typeof postcssConfig === 'string') {
|
||||
// We expand string configs to their PostCSS config object similar to
|
||||
// how Vite does it.
|
||||
// See: https://github.com/vitejs/vite/blob/440783953a55c6c63cd09ec8d13728dc4693073d/packages/vite/src/node/plugins/css.ts#L1580
|
||||
let searchPath = typeof postcssConfig === 'string' ? postcssConfig : config.root
|
||||
let parsedConfig = await postcssrc({}, searchPath).catch((e: Error) => {
|
||||
if (!e.message.includes('No PostCSS Config found')) {
|
||||
if (e instanceof Error) {
|
||||
let { name, message, stack } = e
|
||||
e.name = 'Failed to load PostCSS config'
|
||||
e.message = `Failed to load PostCSS config (searchPath: ${searchPath}): [${name}] ${message}\n${stack}`
|
||||
e.stack = '' // add stack to message to retain stack
|
||||
throw e
|
||||
} else {
|
||||
throw new Error(`Failed to load PostCSS config: ${e}`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
if (parsedConfig !== null) {
|
||||
postcssConfig = {
|
||||
options: parsedConfig.options,
|
||||
plugins: parsedConfig.plugins,
|
||||
} as any
|
||||
} else {
|
||||
postcssConfig = {}
|
||||
}
|
||||
config.css = { postcss: postcssConfig }
|
||||
}
|
||||
|
||||
// postcssConfig is no longer a string after the above. This test is to
|
||||
// avoid TypeScript errors below.
|
||||
if (typeof postcssConfig === 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!postcssConfig || !postcssConfig?.plugins) {
|
||||
config.css = config.css || {}
|
||||
config.css.postcss = postcssConfig || {}
|
||||
config.css.postcss.plugins = [fixRelativePathsPlugin() as any]
|
||||
} else {
|
||||
postcssConfig.plugins.push(fixRelativePathsPlugin() as any)
|
||||
}
|
||||
},
|
||||
|
||||
// Scan index.html for candidates
|
||||
transformIndexHtml(html) {
|
||||
let updated = scan(html, 'html')
|
||||
transformIndexHtml(html, context) {
|
||||
onFileChange(context.filename)
|
||||
|
||||
// In serve mode, if the generated CSS contains a URL that causes the
|
||||
// browser to load a page (e.g. an URL to a missing image), triggering a
|
||||
// CSS update will cause an infinite loop. We only trigger if the
|
||||
// candidates have been updated.
|
||||
let updated = scan(html, 'html')
|
||||
if (updated) {
|
||||
updateCssModules(isSSR)
|
||||
queueRebuild(isSSR)
|
||||
}
|
||||
},
|
||||
|
||||
// Scan all non-CSS files for candidates
|
||||
transform(src, id, options) {
|
||||
onFileChange(id)
|
||||
|
||||
if (id.includes('/.vite/')) return
|
||||
let extension = getExtension(id)
|
||||
if (extension === '' || extension === 'css') return
|
||||
|
||||
scan(src, extension)
|
||||
updateCssModules(options?.ssr ?? false)
|
||||
let updated = scan(src, extension)
|
||||
if (updated) {
|
||||
queueRebuild(options?.ssr ?? false)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
* The plugins that generate CSS must run after 'enforce: pre' so @imports
|
||||
* are expanded in transform.
|
||||
*/
|
||||
|
||||
{
|
||||
// Step 2 (serve mode): Generate CSS
|
||||
name: '@tailwindcss/vite:generate:serve',
|
||||
apply: 'serve',
|
||||
enforce: 'pre',
|
||||
|
||||
async transform(src, id, options) {
|
||||
if (!isTailwindCssFile(id, src)) return
|
||||
onFileChange(id)
|
||||
|
||||
// In serve mode, we treat cssModules as a set, ignoring the value.
|
||||
cssModules[id] = { content: '', handled: true }
|
||||
|
||||
if (!options?.ssr) {
|
||||
// Wait until all other files have been processed, so we can extract
|
||||
// all candidates before generating CSS. This must not be called
|
||||
// during SSR or it will block the server.
|
||||
await server?.waitForRequestsIdle?.(id)
|
||||
}
|
||||
// if (!options?.ssr) {
|
||||
// // Wait until all other files have been processed, so we can extract
|
||||
// // all candidates before generating CSS. This must not be called
|
||||
// // during SSR or it will block the server.
|
||||
// await server?.waitForRequestsIdle?.(id)
|
||||
// }
|
||||
|
||||
let code = await transformWithPlugins(
|
||||
this,
|
||||
|
|
@ -353,10 +331,23 @@ export default function tailwindcss(): Plugin[] {
|
|||
// Step 2 (full build): Generate CSS
|
||||
name: '@tailwindcss/vite:generate:build',
|
||||
apply: 'build',
|
||||
enforce: 'pre',
|
||||
|
||||
transform(src, id) {
|
||||
async transform(src, id) {
|
||||
if (!isTailwindCssFile(id, src)) return
|
||||
onFileChange(id)
|
||||
|
||||
cssModules[id] = { content: src, handled: false }
|
||||
|
||||
// We do a first pass to generate valid CSS for the downstream plugins.
|
||||
// However, it's likely that we need to regenerate this later once all
|
||||
// candidates have been extracted.
|
||||
let code = await transformWithPlugins(
|
||||
this,
|
||||
id,
|
||||
await generateCss(src, id, (file) => this.addWatchFile(file)),
|
||||
)
|
||||
return { code }
|
||||
},
|
||||
|
||||
// renderChunk runs in the bundle generation stage after all transforms.
|
||||
|
|
@ -390,7 +381,7 @@ function getExtension(id: string) {
|
|||
function isTailwindCssFile(id: string, src: string) {
|
||||
let extension = getExtension(id)
|
||||
let isCssFile = extension === 'css' || (extension === 'vue' && id.includes('&lang.css'))
|
||||
return isCssFile && src.includes('@tailwind')
|
||||
return isCssFile
|
||||
}
|
||||
|
||||
function optimizeCss(
|
||||
|
|
@ -416,3 +407,7 @@ function optimizeCss(
|
|||
errorRecovery: true,
|
||||
}).code.toString()
|
||||
}
|
||||
|
||||
function idToPath(id: string) {
|
||||
return path.resolve(id.replace(/\?.*$/, ''))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
@import 'tailwindcss';
|
||||
@import './bar.css';
|
||||
@plugin "./plugin.js";
|
||||
|
|
|
|||
3
playgrounds/vite/src/bar.css
Normal file
3
playgrounds/vite/src/bar.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.red {
|
||||
color: red;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
export function Bar() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-red-500 underline">Bar</h2>
|
||||
<h2 className="text-red-500 underline red">Bar</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -283,10 +283,25 @@ importers:
|
|||
version: link:../../crates/node
|
||||
lightningcss:
|
||||
specifier: 'catalog:'
|
||||
<<<<<<< HEAD
|
||||
version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4)
|
||||
postcss-load-config:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(postcss@8.4.41)
|
||||
||||||| parent of 9ab6a7c5 (Simplify Vite plugin and handle all CSS transforms in pre-transform hook)
|
||||
version: 1.26.0
|
||||
postcss-load-config:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(postcss@8.4.41)
|
||||
=======
|
||||
version: 1.26.0
|
||||
postcss:
|
||||
specifier: ^8.4.41
|
||||
version: 8.4.41
|
||||
postcss-import:
|
||||
specifier: ^16.1.0
|
||||
version: 16.1.0(postcss@8.4.41)
|
||||
>>>>>>> 9ab6a7c5 (Simplify Vite plugin and handle all CSS transforms in pre-transform hook)
|
||||
tailwindcss:
|
||||
specifier: workspace:^
|
||||
version: link:../tailwindcss
|
||||
|
|
@ -294,6 +309,9 @@ 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue