Auto source detection improvements (#14820)

This PR introduces a new `source(…)` argument and improves on the
existing `@source`. The goal of this PR is to make the automatic source
detection configurable, let's dig in.

By default, we will perform automatic source detection starting at the
current working directory. Auto source detection will find plain text
files (no binaries, images, ...) and will ignore git-ignored files.

If you want to start from a different directory, you can use the new
`source(…)` next to the `@import "tailwindcss/utilities"
layer(utilities) source(…)`.

E.g.:

```css
/* ./src/styles/index.css */
@import 'tailwindcss/utilities' layer(utilities) source('../../');
```

Most people won't split their source files, and will just use the simple
`@import "tailwindcss";`, because of this reason, you can use
`source(…)` on the import as well:

E.g.:

```css
/* ./src/styles/index.css */
@import 'tailwindcss' source('../../');
```

Sometimes, you want to rely on auto source detection, but also want to
look in another directory for source files. In this case, yuo can use
the `@source` directive:

```css
/* ./src/index.css */
@import 'tailwindcss';

/* Look for `blade.php` files in `../resources/views` */
@source '../resources/views/**/*.blade.php';
```

However, you don't need to specify the extension, instead you can just
point the directory and all the same automatic source detection rules
will apply.

```css
/* ./src/index.css */
@import 'tailwindcss';

@source '../resources/views';
```

If, for whatever reason, you want to disable the default source
detection feature entirely, and only want to rely on very specific glob
patterns you define, then you can disable it via `source(none)`.

```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);

/* Only look at .blade.php files, nothing else  */
@source "../resources/views/**/*.blade.php";
```

Note: even with `source(none)`, if your `@source` points to a directory,
then auto source detection will still be performed in that directory. If
you don't want that, then you can simply add explicit files in the globs
as seen in the previous example.

```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);

/* Run auto source detection in `../resources/views` */
@source "../resources/views";
```

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
Robin Malfait 2024-10-29 21:33:34 +01:00 • committed by GitHub
parent c439cdf43c
commit d68a780f98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2341 additions and 606 deletions

View file

@ -223,7 +223,10 @@ export function toCss(ast: AstNode[]) {
// AtRule
else if (node.kind === 'at-rule') {
if (node.name === '@tailwind' && node.params === 'utilities') {
if (
node.name === '@tailwind' &&
(node.params === 'utilities' || node.params.startsWith('utilities'))
) {
for (let child of node.nodes) {
css += stringify(child, depth)
}

View file

@ -38,8 +38,12 @@ export async function substituteAtImports(
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
contextNode.nodes = buildImportNodes(
[context({ base: loaded.base }, ast)],
layer,
media,
supports,
)
})(),
)

View file

@ -8,7 +8,7 @@ import { Theme } from './theme'
const root = process.env.FOLDER || process.cwd()
// Auto content detection
const scanner = new Scanner({ detectSources: { base: root } })
const scanner = new Scanner({ sources: [{ base: root, pattern: '**/*' }] })
const candidates = scanner.scan()
const designSystem = buildDesignSystem(new Theme())

View file

@ -7,7 +7,7 @@ const root = process.env.FOLDER || process.cwd()
const css = String.raw
bench('compile', async () => {
let scanner = new Scanner({ detectSources: { base: root } })
let scanner = new Scanner({ sources: [{ base: root, pattern: '**/*' }] })
let candidates = scanner.scan()
let { build } = await compile(css`

View file

@ -4,7 +4,7 @@ import {
atRoot,
atRule,
comment,
context,
context as contextNode,
decl,
rule,
styleRule,
@ -76,21 +76,64 @@ async function parseCss(
loadStylesheet = throwOnLoadStylesheet,
}: CompileOptions = {},
) {
let ast = [context({ base }, CSS.parse(css))] as AstNode[]
let ast = [contextNode({ base }, CSS.parse(css))] as AstNode[]
await substituteAtImports(ast, base, loadStylesheet)
let important: boolean | null = null
let important = null as boolean | null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
let firstThemeRule = null as StyleRule | null
let utilitiesNode = null as AtRule | null
let globs: { base: string; pattern: string }[] = []
let root:
| null // Unknown root
| 'none' // Explicitly no root specified via `source(none)`
// Specified via `source(…)`, relative to the `base`
| { base: string; pattern: string } = null
// Handle at-rules
walk(ast, (node, { parent, replaceWith, context }) => {
if (node.kind !== 'at-rule') return
// Find `@tailwind utilities` so that we can later replace it with the
// actual generated utility class CSS.
if (
utilitiesNode === null &&
node.name === '@tailwind' &&
(node.params === 'utilities' || node.params.startsWith('utilities'))
) {
let params = segment(node.params, ' ')
for (let param of params) {
if (param.startsWith('source(')) {
let path = param.slice(7, -1)
// Keyword: `source(none)`
if (path === 'none') {
root = path
continue
}
// Explicit path: `source('…')`
if (
(path[0] === '"' && path[path.length - 1] !== '"') ||
(path[0] === "'" && path[path.length - 1] !== "'") ||
(path[0] !== "'" && path[0] !== '"')
) {
throw new Error('`source(…)` paths must be quoted.')
}
root = {
base: context.sourceBase ?? context.base,
pattern: path.slice(1, -1),
}
}
}
utilitiesNode = node
}
// Collect custom `@utility` at-rules
if (node.name === '@utility') {
if (parent !== null) {
@ -234,12 +277,27 @@ async function parseCss(
let unknownParams: string[] = []
for (let param of params) {
// Handle `@media source(…)`
if (param.startsWith('source(')) {
let path = param.slice(7, -1)
walk(node.nodes, (child, { replaceWith }) => {
if (child.kind !== 'at-rule') return
if (child.name === '@tailwind' && child.params === 'utilities') {
child.params += ` source(${path})`
replaceWith([contextNode({ sourceBase: context.base }, [child])])
return WalkAction.Stop
}
})
}
// Handle `@media theme(…)`
//
// We support `@import "tailwindcss/theme" theme(reference)` as a way to
// import an external theme file as a reference, which becomes `@media
// theme(reference) { … }` when the `@import` is processed.
if (param.startsWith('theme(')) {
else if (param.startsWith('theme(')) {
let themeParams = param.slice(6, -1)
walk(node.nodes, (child) => {
@ -417,6 +475,8 @@ async function parseCss(
designSystem,
ast,
globs,
root,
utilitiesNode,
}
}
@ -425,24 +485,13 @@ export async function compile(
opts: CompileOptions = {},
): Promise<{
globs: { base: string; pattern: string }[]
root:
| null // Unknown root
| 'none' // Explicitly no root specified via `source(none)`
| { base: string; pattern: string } // Specified via `source(…)`, relative to the `base`
build(candidates: string[]): string
}> {
let { designSystem, ast, globs } = await parseCss(css, opts)
let tailwindUtilitiesNode: AtRule | null = null
// Find `@tailwind utilities` so that we can later replace it with the actual
// generated utility class CSS.
walk(ast, (node) => {
if (node.kind === 'at-rule' && node.name === '@tailwind' && node.params === 'utilities') {
tailwindUtilitiesNode = node
// Stop walking after finding `@tailwind utilities` to avoid walking all
// of the generated CSS. This means `@tailwind utilities` can only appear
// once per file but that's the intended usage at this point in time.
return WalkAction.Stop
}
})
let { designSystem, ast, globs, root, utilitiesNode } = await parseCss(css, opts)
if (process.env.NODE_ENV !== 'test') {
ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `))
@ -462,6 +511,7 @@ export async function compile(
return {
globs,
root,
build(newRawCandidates: string[]) {
let didChange = false
@ -480,7 +530,7 @@ export async function compile(
return compiledCss
}
if (tailwindUtilitiesNode) {
if (utilitiesNode) {
let newNodes = compileCandidates(allValidCandidates, designSystem, {
onInvalidCandidate,
}).astNodes
@ -494,7 +544,7 @@ export async function compile(
previousAstNodeCount = newNodes.length
tailwindUtilitiesNode.nodes = newNodes
utilitiesNode.nodes = newNodes
compiledCss = toCss(ast)
}