Add @tailwindcss/turbopack loader
This commit is contained in:
parent
d494a78532
commit
c9689a44a6
9 changed files with 486 additions and 0 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Add `@tailwindcss/turbopack` loader for Tailwind CSS v4
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure watch mode detects changes to symlinked `@source` files whose real paths aren't otherwise scanned ([#20356](https://github.com/tailwindlabs/tailwindcss/pull/20356))
|
||||
|
|
|
|||
|
|
@ -612,6 +612,7 @@ function overwriteVersionsInPnpmWorkspace(content: string): string {
|
|||
workspace.overrides['@tailwindcss/upgrade>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/cli>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/postcss>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/turbopack>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/vite>tailwindcss'] = resolveVersion(pkg)
|
||||
workspace.overrides['@tailwindcss/webpack>tailwindcss'] = resolveVersion(pkg)
|
||||
} else {
|
||||
|
|
|
|||
99
packages/@tailwindcss-turbopack/README.md
Normal file
99
packages/@tailwindcss-turbopack/README.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<p align="center">
|
||||
<a href="https://tailwindcss.com" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg">
|
||||
<img alt="Tailwind CSS" src="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/HEAD/.github/logo-light.svg" width="350" height="70" style="max-width: 100%;">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
A utility-first CSS framework for rapidly building custom user interfaces.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/actions"><img src="https://img.shields.io/github/actions/workflow/status/tailwindlabs/tailwindcss/ci.yml?branch=main" alt="Build Status"></a>
|
||||
<a href="https://www.npmjs.com/package/tailwindcss"><img src="https://img.shields.io/npm/dt/tailwindcss.svg" alt="Total Downloads"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/releases"><img src="https://img.shields.io/npm/v/tailwindcss.svg" alt="Latest Release"></a>
|
||||
<a href="https://github.com/tailwindlabs/tailwindcss/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tailwindcss.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
|
||||
|
||||
## Community
|
||||
|
||||
For help, discussion about best practices, or feature ideas:
|
||||
|
||||
[Discuss Tailwind CSS on GitHub](https://github.com/tailwindlabs/tailwindcss/discussions)
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindlabs/tailwindcss/blob/main/.github/CONTRIBUTING.md) **before submitting a pull request**.
|
||||
|
||||
---
|
||||
|
||||
## @tailwindcss/turbopack
|
||||
|
||||
A Turbopack loader for Tailwind CSS v4.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install @tailwindcss/turbopack
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```javascript
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
turbopack: {
|
||||
rules: {
|
||||
'*.css': {
|
||||
loaders: ['@tailwindcss/turbopack'],
|
||||
as: '*.css',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then create a CSS file that imports Tailwind:
|
||||
|
||||
```css
|
||||
/* src/index.css */
|
||||
@import 'tailwindcss';
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
#### `base`
|
||||
|
||||
The base directory to scan for class candidates. Defaults to the current working directory.
|
||||
|
||||
```javascript
|
||||
{
|
||||
loader: '@tailwindcss/turbopack',
|
||||
options: {
|
||||
base: process.cwd(),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### `optimize`
|
||||
|
||||
Whether to optimize and minify the output CSS. Defaults to `true` in production mode.
|
||||
|
||||
```javascript
|
||||
{
|
||||
loader: '@tailwindcss/turbopack',
|
||||
options: {
|
||||
optimize: true, // or { minify: true }
|
||||
},
|
||||
}
|
||||
```
|
||||
43
packages/@tailwindcss-turbopack/package.json
Normal file
43
packages/@tailwindcss-turbopack/package.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "@tailwindcss/turbopack",
|
||||
"version": "4.3.3",
|
||||
"description": "A Turbopack loader for Tailwind CSS v4.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tailwindlabs/tailwindcss.git",
|
||||
"directory": "packages/@tailwindcss-turbopack"
|
||||
},
|
||||
"bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
|
||||
"homepage": "https://tailwindcss.com",
|
||||
"scripts": {
|
||||
"build": "tsup-node",
|
||||
"dev": "pnpm run build -- --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"publishConfig": {
|
||||
"provenance": true,
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "workspace:*",
|
||||
"@tailwindcss/oxide": "workspace:*",
|
||||
"tailwindcss": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"webpack": "catalog:"
|
||||
}
|
||||
}
|
||||
5
packages/@tailwindcss-turbopack/src/index.cts
Normal file
5
packages/@tailwindcss-turbopack/src/index.cts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import tailwindLoader from './index.ts'
|
||||
|
||||
// CommonJS export for webpack loaders - must be the function directly
|
||||
// @ts-ignore
|
||||
export = tailwindLoader
|
||||
287
packages/@tailwindcss-turbopack/src/index.ts
Normal file
287
packages/@tailwindcss-turbopack/src/index.ts
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
import QuickLRU from '@alloc/quick-lru'
|
||||
import {
|
||||
compile,
|
||||
env,
|
||||
Features,
|
||||
Instrumentation,
|
||||
normalizePath,
|
||||
optimize,
|
||||
Polyfills,
|
||||
} from '@tailwindcss/node'
|
||||
import { clearRequireCache } from '@tailwindcss/node/require-cache'
|
||||
import { Scanner } from '@tailwindcss/oxide'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { LoaderContext } from 'webpack'
|
||||
|
||||
const DEBUG = env.DEBUG
|
||||
|
||||
export interface LoaderOptions {
|
||||
/**
|
||||
* The base directory to scan for class candidates.
|
||||
*
|
||||
* Defaults to the current working directory.
|
||||
*/
|
||||
base?: string
|
||||
|
||||
/**
|
||||
* Optimize and minify the output CSS.
|
||||
*/
|
||||
optimize?: boolean | { minify?: boolean }
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
mtimes: Map<string, number>
|
||||
compiler: null | Awaited<ReturnType<typeof compile>>
|
||||
scanner: null | Scanner
|
||||
candidates: Set<string>
|
||||
fullRebuildPaths: string[]
|
||||
}
|
||||
|
||||
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
|
||||
|
||||
function getCacheKey(resourceId: string, opts: LoaderOptions): string {
|
||||
return `${resourceId}:${opts.base ?? ''}:${JSON.stringify(opts.optimize)}`
|
||||
}
|
||||
|
||||
function getContextFromCache(resourceId: string, opts: LoaderOptions): CacheEntry {
|
||||
let key = getCacheKey(resourceId, opts)
|
||||
if (cache.has(key)) return cache.get(key)!
|
||||
let entry: CacheEntry = {
|
||||
mtimes: new Map<string, number>(),
|
||||
compiler: null,
|
||||
scanner: null,
|
||||
candidates: new Set<string>(),
|
||||
fullRebuildPaths: [],
|
||||
}
|
||||
cache.set(key, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
export default async function tailwindLoader(
|
||||
this: LoaderContext<LoaderOptions>,
|
||||
source: string,
|
||||
): Promise<void> {
|
||||
let callback = this.async()
|
||||
let options = this.getOptions() ?? {}
|
||||
let inputFile = this.resourcePath
|
||||
let resourceId = this.resource
|
||||
let base = options.base ?? process.cwd()
|
||||
let shouldOptimize = options.optimize ?? process.env.NODE_ENV === 'production'
|
||||
let isCSSModuleFile = inputFile.endsWith('.module.css')
|
||||
|
||||
using I = new Instrumentation()
|
||||
|
||||
DEBUG && I.start(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
|
||||
|
||||
// Bail out early if this is guaranteed to be a non-Tailwind CSS file.
|
||||
{
|
||||
DEBUG && I.start('Quick bail check')
|
||||
let canBail = !/@(import|reference|theme|variant|config|plugin|apply|tailwind)\b/.test(source)
|
||||
if (canBail) {
|
||||
DEBUG && I.end('Quick bail check')
|
||||
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
|
||||
callback(null, source)
|
||||
return
|
||||
}
|
||||
DEBUG && I.end('Quick bail check')
|
||||
}
|
||||
|
||||
try {
|
||||
let context = getContextFromCache(resourceId, options)
|
||||
let inputBasePath = path.dirname(path.resolve(inputFile))
|
||||
|
||||
// Whether this is the first build or not
|
||||
let isInitialBuild = context.compiler === null
|
||||
|
||||
async function createCompiler() {
|
||||
DEBUG && I.start('Setup compiler')
|
||||
if (context.fullRebuildPaths.length > 0 && !isInitialBuild) {
|
||||
clearRequireCache(context.fullRebuildPaths)
|
||||
}
|
||||
|
||||
context.fullRebuildPaths = []
|
||||
|
||||
DEBUG && I.start('Create compiler')
|
||||
let compiler = await compile(source, {
|
||||
from: inputFile,
|
||||
base: inputBasePath,
|
||||
shouldRewriteUrls: true,
|
||||
onDependency: (depPath) => context.fullRebuildPaths.push(depPath),
|
||||
// In CSS Module files, we have to disable the `@property` polyfill since these will
|
||||
// emit global `*` rules which are considered to be non-pure and will cause builds
|
||||
// to fail.
|
||||
polyfills: isCSSModuleFile ? Polyfills.All ^ Polyfills.AtProperty : Polyfills.All,
|
||||
})
|
||||
DEBUG && I.end('Create compiler')
|
||||
|
||||
DEBUG && I.end('Setup compiler')
|
||||
return compiler
|
||||
}
|
||||
|
||||
// Setup the compiler if it doesn't exist yet
|
||||
context.compiler ??= await createCompiler()
|
||||
|
||||
// Early exit if no Tailwind features are used
|
||||
if (context.compiler.features === Features.None) {
|
||||
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
|
||||
callback(null, source)
|
||||
return
|
||||
}
|
||||
|
||||
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
|
||||
|
||||
// Track file modification times to CSS files
|
||||
DEBUG && I.start('Register full rebuild paths')
|
||||
{
|
||||
// Report dependencies for config files, plugins, etc.
|
||||
for (let file of context.fullRebuildPaths) {
|
||||
this.addDependency(path.resolve(file))
|
||||
}
|
||||
|
||||
let files = [...context.fullRebuildPaths, inputFile]
|
||||
|
||||
for (let file of files) {
|
||||
let changedTime: number | null = null
|
||||
try {
|
||||
changedTime = fs.statSync(file)?.mtimeMs ?? null
|
||||
} catch {
|
||||
// File might not exist
|
||||
}
|
||||
|
||||
if (changedTime === null) {
|
||||
if (file === inputFile) {
|
||||
rebuildStrategy = 'full'
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
let prevTime = context.mtimes.get(file)
|
||||
if (prevTime === changedTime) continue
|
||||
|
||||
rebuildStrategy = 'full'
|
||||
context.mtimes.set(file, changedTime)
|
||||
}
|
||||
}
|
||||
DEBUG && I.end('Register full rebuild paths')
|
||||
|
||||
if (rebuildStrategy === 'full' && !isInitialBuild) {
|
||||
context.compiler = await createCompiler()
|
||||
}
|
||||
|
||||
let compiler = context.compiler
|
||||
|
||||
// Check if we need to process this file at all
|
||||
if (
|
||||
!(
|
||||
compiler.features &
|
||||
(Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities)
|
||||
)
|
||||
) {
|
||||
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
|
||||
callback(null, source)
|
||||
return
|
||||
}
|
||||
|
||||
// Setup or update scanner if needed
|
||||
if (context.scanner === null || rebuildStrategy === 'full') {
|
||||
DEBUG && I.start('Setup scanner')
|
||||
let sources = (() => {
|
||||
// Disable auto source detection
|
||||
if (compiler.root === 'none') {
|
||||
return []
|
||||
}
|
||||
|
||||
// No root specified, use the base directory
|
||||
if (compiler.root === null) {
|
||||
return [{ base, pattern: '**/*', negated: false }]
|
||||
}
|
||||
|
||||
// Use the specified root
|
||||
return [{ ...compiler.root, negated: false }]
|
||||
})().concat(compiler.sources)
|
||||
|
||||
context.scanner = new Scanner({ sources })
|
||||
DEBUG && I.end('Setup scanner')
|
||||
}
|
||||
|
||||
// Scan for candidates if utilities are used
|
||||
if (compiler.features & Features.Utilities) {
|
||||
DEBUG && I.start('Scan for candidates')
|
||||
for (let candidate of context.scanner.scan()) {
|
||||
context.candidates.add(candidate)
|
||||
}
|
||||
DEBUG && I.end('Scan for candidates')
|
||||
|
||||
DEBUG && I.start('Register dependency messages')
|
||||
// Add all found files as direct dependencies
|
||||
let resolvedInputFile = path.resolve(base, inputFile)
|
||||
for (let file of context.scanner.files) {
|
||||
let absolutePath = path.resolve(file)
|
||||
// The CSS file cannot be a dependency of itself
|
||||
if (absolutePath === resolvedInputFile) {
|
||||
continue
|
||||
}
|
||||
this.addDependency(absolutePath)
|
||||
}
|
||||
|
||||
// Register context dependencies for glob patterns
|
||||
for (let glob of context.scanner.globs) {
|
||||
// Skip negated patterns
|
||||
if (glob.pattern[0] === '!') continue
|
||||
|
||||
// Avoid adding a dependency on the base directory itself
|
||||
if (glob.pattern === '*' && base === glob.base) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.addContextDependency(path.resolve(glob.base))
|
||||
}
|
||||
|
||||
// Validate that source(...) paths are directories
|
||||
let root = compiler.root
|
||||
if (root !== 'none' && root !== null) {
|
||||
let basePath = normalizePath(path.resolve(root.base, root.pattern))
|
||||
try {
|
||||
let stats = fs.statSync(basePath)
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(
|
||||
`The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`,
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
// Directory doesn't exist yet, which is fine
|
||||
}
|
||||
}
|
||||
DEBUG && I.end('Register dependency messages')
|
||||
}
|
||||
|
||||
DEBUG && I.start('Build utilities')
|
||||
let css = compiler.build([...context.candidates])
|
||||
DEBUG && I.end('Build utilities')
|
||||
|
||||
// Optionally optimize the output
|
||||
let result = css
|
||||
if (shouldOptimize) {
|
||||
DEBUG && I.start('Optimization')
|
||||
let optimized = optimize(css, {
|
||||
minify: typeof shouldOptimize === 'object' ? shouldOptimize.minify : true,
|
||||
})
|
||||
result = optimized.code
|
||||
DEBUG && I.end('Optimization')
|
||||
}
|
||||
|
||||
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
|
||||
callback(null, result)
|
||||
} catch (error) {
|
||||
// Clear the cache entry on error to force a full rebuild next time
|
||||
let key = getCacheKey(resourceId, options)
|
||||
cache.delete(key)
|
||||
|
||||
DEBUG && I.end(`[@tailwindcss/webpack] ${path.relative(base, inputFile)}`)
|
||||
callback(error as Error)
|
||||
}
|
||||
}
|
||||
6
packages/@tailwindcss-turbopack/tsconfig.json
Normal file
6
packages/@tailwindcss-turbopack/tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": false,
|
||||
},
|
||||
}
|
||||
19
packages/@tailwindcss-turbopack/tsup.config.ts
Normal file
19
packages/@tailwindcss-turbopack/tsup.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
format: ['esm'],
|
||||
clean: true,
|
||||
minify: true,
|
||||
cjsInterop: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
},
|
||||
{
|
||||
format: ['cjs'],
|
||||
minify: true,
|
||||
cjsInterop: true,
|
||||
dts: true,
|
||||
entry: ['src/index.cts'],
|
||||
},
|
||||
])
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
|
|
@ -431,6 +431,28 @@ importers:
|
|||
specifier: ^7.7.1
|
||||
version: 7.7.1
|
||||
|
||||
packages/@tailwindcss-turbopack:
|
||||
dependencies:
|
||||
'@alloc/quick-lru':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
'@tailwindcss/node':
|
||||
specifier: workspace:*
|
||||
version: link:../@tailwindcss-node
|
||||
'@tailwindcss/oxide':
|
||||
specifier: workspace:*
|
||||
version: link:../../crates/node
|
||||
tailwindcss:
|
||||
specifier: workspace:*
|
||||
version: link:../tailwindcss
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 22.19.21
|
||||
webpack:
|
||||
specifier: 'catalog:'
|
||||
version: 5.108.3(esbuild@0.27.7)(postcss@8.5.16)
|
||||
|
||||
packages/@tailwindcss-vite:
|
||||
dependencies:
|
||||
'@tailwindcss/node':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue