mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Resolve @import in core (#14446)
This PR brings `@import` resolution into Tailwind CSS core. This means that our clients (PostCSS, Vite, and CLI) no longer need to depend on `postcss` and `postcss-import` to resolve `@import`. Furthermore this simplifies the handling of relative paths for `@source`, `@plugin`, or `@config` in transitive CSS files (where the relative root should always be relative to the CSS file that contains the directive). This PR also fixes a plugin resolution bug where non-relative imports (e.g. directly importing node modules like `@plugin '@tailwindcss/typography';`) would not work in CSS files that are based in a different npm package. ### Resolving `@import` The core of the `@import` resolution is inside `packages/tailwindcss/src/at-import.ts`. There, to keep things performant, we do a two-step process to resolve imports. Imagine the following input CSS file: ```css @import "tailwindcss/theme.css"; @import "tailwindcss/utilities.css"; ``` Since our AST walks are synchronous, we will do a first traversal where we start a loading request for each `@import` directive. Once all loads are started, we will await the promise and do a second walk where we actually replace the AST nodes with their resolved stylesheets. All of this is recursive, so that `@import`-ed files can again `@import` other files. The core `@import` resolver also includes extensive test cases for [various combinations of media query and supports conditionals as well als layered imports](https://developer.mozilla.org/en-US/docs/Web/CSS/@import). When the same file is imported multiple times, the AST nodes are duplicated but duplicate I/O is avoided on a per-file basis, so this will only load one file, but include the `@theme` rules twice: ```css @import "tailwindcss/theme.css"; @import "tailwindcss/theme.css"; ``` ### Adding a new `context` node to the AST One limitation we had when working with the `postcss-import` plugin was the need to do an additional traversal to rewrite relative `@source`, `@plugin`, and `@config` directives. This was needed because we want these paths to be relative to the CSS file that defines the directive but when flattening a CSS file, this information is no longer part of the stringifed CSS representation. We worked around this by rewriting the content of these directives to be relative to the input CSS file, which resulted in added complexity and caused a lot of issues with Windows paths in the beginning. Now that we are doing the `@import` resolution in core, we can use a different data structure to persist this information. This PR adds a new `context` node so that we can store arbitrary context like this inside the Ast directly. This allows us to share information with the sub tree _while doing the Ast walk_. Here's an example of how the new `context` node can be used to share information with subtrees: ```ts const ast = [ rule('.foo', [decl('color', 'red')]), context({ value: 'a' }, [ rule('.bar', [ decl('color', 'blue'), context({ value: 'b' }, [ rule('.baz', [decl('color', 'green')]), ]), ]), ]), ] walk(ast, (node, { context }) => { if (node.kind !== 'declaration') return switch (node.value) { case 'red': assert(context.value === undefined) case 'blue': assert(context.value === 'a') case 'green': assert(context.value === 'b') } }) ``` In core, we use this new Ast node specifically to persist the `base` path of the current CSS file. We put the input CSS file `base` at the root of the Ast and then overwrite the `base` on every `@import` substitution. ### Removing the dependency on `postcss-import` Now that we support `@import` resolution in core, our clients no longer need a dependency on `postcss-import`. Furthermore, most dependencies also don't need to know about `postcss` at all anymore (except the PostCSS client, of course!). This also means that our workaround for rewriting `@source`, the `postcss-fix-relative-paths` plugin, can now go away as a shared dependency between all of our clients. Note that we still have it for the PostCSS plugin only, where it's possible that users already have `postcss-import` running _before_ the `@tailwindcss/postcss` plugin. Here's an example of the changes to the dependencies for our Vite client ✨ : <img width="854" alt="Screenshot 2024-09-19 at 16 59 45" src="https://github.com/user-attachments/assets/ae1f9d5f-d93a-4de9-9244-61af3aff1237"> ### Performance Since our Vite and CLI clients now no longer need to use `postcss` at all, we have also measured a significant improvement to the initial build times. For a small test setup that contains only a hand full of files (nothing super-complex), we measured an improvement in the **3.5x** range: <img width="1334" alt="Screenshot 2024-09-19 at 14 52 49" src="https://github.com/user-attachments/assets/06071fb0-7f2a-4de6-8ec8-f202d2cc78e5"> The code for this is in the commit history if you want to reproduce the results. The test was based on the Vite client. ### Caveats One thing to note is that we previously relied on finding specific symbols in the input CSS to _bail out of Tailwind processing completely_. E.g. if a file does not contain a `@tailwind` or `@apply` directive, it can never be a Tailwind file. Since we no longer have a string representation of the flattened CSS file, we can no longer do this check. However, the current implementation was already inconsistent with differences on the allowed symbol list between our clients. Ideally, Tailwind CSS should figure out wether a CSS file is a Tailwind CSS file. This, however, is left as an improvement for a future API since it goes hand-in-hand with our planned API changes for the core `tailwindcss` package. --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
parent
6d43a8be99
commit
79794744a9
@ -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
|
||||
|
||||
|
||||
@ -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`
|
||||
<div
|
||||
class="underline 2xl:font-bold hocus:underline inverted:flex"
|
||||
></div>
|
||||
`,
|
||||
'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`
|
||||
<div class="flex" />
|
||||
`,
|
||||
'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)',
|
||||
{
|
||||
|
||||
@ -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:^"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ReturnType<typeof options>>) {
|
||||
|
||||
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<ReturnType<typeof options>>) {
|
||||
|
||||
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<ReturnType<typeof options>>) {
|
||||
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<ReturnType<typeof options>>) {
|
||||
// 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<ReturnType<typeof options>>) {
|
||||
// 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 } = {},
|
||||
|
||||
@ -5,5 +5,4 @@ export default defineConfig({
|
||||
clean: true,
|
||||
minify: true,
|
||||
entry: ['src/index.ts'],
|
||||
noExternal: ['internal-postcss-fix-relative-paths'],
|
||||
})
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"tailwindcss": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"jiti": "^2.0.0-beta.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import EnhancedResolve from 'enhanced-resolve'
|
||||
import { createJiti, type Jiti } from 'jiti'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import fsPromises from 'node:fs/promises'
|
||||
import path, { dirname, extname } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { compile as _compile } from 'tailwindcss'
|
||||
import { getModuleDependencies } from './get-module-dependencies'
|
||||
@ -9,12 +12,25 @@ export async function compile(
|
||||
{ base, onDependency }: { base: string; onDependency: (path: string) => void },
|
||||
) {
|
||||
return await _compile(css, {
|
||||
loadPlugin: async (pluginPath) => {
|
||||
if (pluginPath[0] !== '.') {
|
||||
return importModule(pluginPath).then((m) => m.default ?? m)
|
||||
base,
|
||||
async loadModule(id, base) {
|
||||
if (id[0] !== '.') {
|
||||
let resolvedPath = await resolveJsId(id, base)
|
||||
if (!resolvedPath) {
|
||||
throw new Error(`Could not resolve '${id}' from '${base}'`)
|
||||
}
|
||||
|
||||
let module = await importModule(pathToFileURL(resolvedPath).href)
|
||||
return {
|
||||
base: dirname(resolvedPath),
|
||||
module: module.default ?? module,
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedPath = path.resolve(base, pluginPath)
|
||||
let resolvedPath = await 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),
|
||||
@ -24,25 +40,31 @@ export async function compile(
|
||||
for (let file of moduleDependencies) {
|
||||
onDependency(file)
|
||||
}
|
||||
return module.default ?? module
|
||||
return {
|
||||
base: dirname(resolvedPath),
|
||||
module: module.default ?? module,
|
||||
}
|
||||
},
|
||||
|
||||
loadConfig: async (configPath) => {
|
||||
if (configPath[0] !== '.') {
|
||||
return importModule(configPath).then((m) => m.default ?? m)
|
||||
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 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)
|
||||
let file = await fsPromises.readFile(resolvedPath, 'utf-8')
|
||||
return {
|
||||
base: path.dirname(resolvedPath),
|
||||
content: file,
|
||||
}
|
||||
return module.default ?? module
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -62,3 +84,58 @@ async function importModule(path: string): Promise<any> {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const cssResolver = EnhancedResolve.ResolverFactory.createResolver({
|
||||
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
|
||||
useSyncFileSystemCalls: true,
|
||||
extensions: ['.css'],
|
||||
mainFields: ['style'],
|
||||
conditionNames: ['style'],
|
||||
})
|
||||
async function resolveCssId(id: string, base: string): Promise<string | false | undefined> {
|
||||
if (typeof globalThis.__tw_resolve === 'function') {
|
||||
let resolved = globalThis.__tw_resolve(id, base)
|
||||
if (resolved) {
|
||||
return Promise.resolve(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
// CSS imports that do not have a dir prefix are considered relative. Since
|
||||
// the resolver does not account for this, we need to do a first pass with an
|
||||
// assumed relative import by prefixing `./${path}`. We don't have to do this
|
||||
// when the path starts with a `.` or when the path has no extension (at which
|
||||
// case it's likely an npm package and not a relative stylesheet).
|
||||
let skipRelativeCheck = extname(id) === '' || id.startsWith('.')
|
||||
|
||||
if (!skipRelativeCheck) {
|
||||
try {
|
||||
let dotResolved = await runResolver(cssResolver, `./${id}`, base)
|
||||
if (!dotResolved) throw new Error()
|
||||
return dotResolved
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return runResolver(cssResolver, id, base)
|
||||
}
|
||||
|
||||
const jsResolver = EnhancedResolve.ResolverFactory.createResolver({
|
||||
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
|
||||
useSyncFileSystemCalls: true,
|
||||
})
|
||||
|
||||
function resolveJsId(id: string, base: string): Promise<string | false | undefined> {
|
||||
return runResolver(jsResolver, id, base)
|
||||
}
|
||||
|
||||
function runResolver(
|
||||
resolver: EnhancedResolve.Resolver,
|
||||
id: string,
|
||||
base: string,
|
||||
): Promise<string | false | undefined> {
|
||||
return new Promise((resolve, reject) =>
|
||||
resolver.resolve({}, base, id, {}, (err, result) => {
|
||||
if (err) return reject(err)
|
||||
resolve(result)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -33,14 +33,13 @@
|
||||
"@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:^"
|
||||
"postcss-import": "^16.1.0",
|
||||
"@types/postcss-import": "14.0.3",
|
||||
"internal-example-plugin": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +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, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss'
|
||||
import postcssImport from 'postcss-import'
|
||||
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.
|
||||
@ -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,23 +118,14 @@ 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
|
||||
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,
|
||||
})
|
||||
|
||||
//
|
||||
let candidates = scanner.scan()
|
||||
|
||||
// Add all found files as direct dependencies
|
||||
@ -177,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) {
|
||||
|
||||
@ -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<AtRule> = new WeakSet()
|
||||
@ -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'],
|
||||
},
|
||||
])
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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<string | undefined>)
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import { compile } from '@tailwindcss/node'
|
||||
import { compile, normalizePath } 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 { 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[] {
|
||||
@ -269,18 +265,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 } = {},
|
||||
@ -378,30 +362,7 @@ 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
|
||||
|
||||
// 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)
|
||||
@ -410,12 +371,7 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -6,5 +6,4 @@ export default defineConfig({
|
||||
minify: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
noExternal: ['internal-postcss-fix-relative-paths'],
|
||||
})
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
}
|
||||
@ -89,6 +89,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/oxide": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"lightningcss": "catalog:"
|
||||
"lightningcss": "catalog:",
|
||||
"dedent": "1.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
@ -16,7 +16,13 @@ export type Comment = {
|
||||
value: string
|
||||
}
|
||||
|
||||
export type AstNode = Rule | Declaration | Comment
|
||||
export type Context = {
|
||||
kind: 'context'
|
||||
context: Record<string, string>
|
||||
nodes: AstNode[]
|
||||
}
|
||||
|
||||
export type AstNode = Rule | Declaration | Comment | Context
|
||||
|
||||
export function rule(selector: string, nodes: AstNode[]): Rule {
|
||||
return {
|
||||
@ -42,6 +48,14 @@ export function comment(value: string): Comment {
|
||||
}
|
||||
}
|
||||
|
||||
export function context(context: Record<string, string>, nodes: AstNode[]): Context {
|
||||
return {
|
||||
kind: 'context',
|
||||
context,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
export enum WalkAction {
|
||||
/** Continue walking, which is the default */
|
||||
Continue,
|
||||
@ -60,12 +74,23 @@ export function walk(
|
||||
utils: {
|
||||
parent: AstNode | null
|
||||
replaceWith(newNode: AstNode | AstNode[]): void
|
||||
context: Record<string, string>
|
||||
},
|
||||
) => void | WalkAction,
|
||||
parent: AstNode | null = null,
|
||||
context: Record<string, string> = {},
|
||||
) {
|
||||
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,
|
||||
@ -76,6 +101,7 @@ export function walk(
|
||||
// will process this position (containing the replaced node) again.
|
||||
i--
|
||||
},
|
||||
context,
|
||||
}) ?? WalkAction.Continue
|
||||
|
||||
// Stop the walk entirely
|
||||
@ -85,7 +111,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 +197,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`
|
||||
|
||||
572
packages/tailwindcss/src/at-import.test.ts
Normal file
572
packages/tailwindcss/src/at-import.test.ts
Normal file
@ -0,0 +1,572 @@
|
||||
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'
|
||||
|
||||
let css = String.raw
|
||||
|
||||
async function run(
|
||||
css: 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 })
|
||||
let result = compiler.build(candidates)
|
||||
return optimize ? optimizeCss(result) : result
|
||||
}
|
||||
|
||||
test('can resolve relative @imports', async () => {
|
||||
let loadStylesheet = async (id: string, base: string) => {
|
||||
expect(base).toBe('/root')
|
||||
expect(id).toBe('./foo/bar.css')
|
||||
return {
|
||||
content: css`
|
||||
.foo {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
base: '/root/foo',
|
||||
}
|
||||
}
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import './foo/bar.css';
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
".foo {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('can recursively resolve relative @imports', async () => {
|
||||
let loadStylesheet = async (id: string, base: string) => {
|
||||
if (base === '/root' && id === './foo/bar.css') {
|
||||
return {
|
||||
content: css`
|
||||
@import './bar/baz.css';
|
||||
`,
|
||||
base: '/root/foo',
|
||||
}
|
||||
} else if (base === '/root/foo' && id === './bar/baz.css') {
|
||||
return {
|
||||
content: css`
|
||||
.baz {
|
||||
color: blue;
|
||||
}
|
||||
`,
|
||||
base: '/root/foo/bar',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected import: ${id}`)
|
||||
}
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import './foo/bar.css';
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
".baz {
|
||||
color: #00f;
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
let exampleCSS = css`
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
`
|
||||
let loadStylesheet = async (id: string) => {
|
||||
if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id)
|
||||
return {
|
||||
content: exampleCSS,
|
||||
base: '/root',
|
||||
}
|
||||
}
|
||||
|
||||
test('extracts path from @import nodes', async () => {
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css';
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"a {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import './example.css';
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"a {
|
||||
color: red;
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css' print, screen;
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"@media print, screen {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css' screen and (orientation: landscape);
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"@media screen and (orientation: landscape) {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css' foo(bar);
|
||||
`,
|
||||
{ loadStylesheet, optimize: false },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"@media foo(bar) {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('@supports', async () => {
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css' supports(display: grid);
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"@supports (display: grid) {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('@layer', async () => {
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css' layer(utilities);
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
|
||||
await expect(
|
||||
run(
|
||||
css`
|
||||
@import 'example.css' layer();
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
"@layer {
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('supports theme(reference) imports', async () => {
|
||||
expect(
|
||||
run(
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@import 'example.css' theme(reference);
|
||||
`,
|
||||
{
|
||||
loadStylesheet: () =>
|
||||
Promise.resolve({
|
||||
content: css`
|
||||
@theme {
|
||||
--color-red-500: red;
|
||||
}
|
||||
`,
|
||||
base: '',
|
||||
}),
|
||||
candidates: ['text-red-500'],
|
||||
},
|
||||
),
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
".text-red-500 {
|
||||
color: var(--color-red-500, red);
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test('updates the base when loading modules inside nested files', async () => {
|
||||
let loadStylesheet = () =>
|
||||
Promise.resolve({
|
||||
content: css`
|
||||
@config './nested-config.js';
|
||||
@plugin './nested-plugin.js';
|
||||
`,
|
||||
base: '/root/foo',
|
||||
})
|
||||
let loadModule = vi.fn().mockResolvedValue({ base: '', module: () => {} })
|
||||
|
||||
expect(
|
||||
(
|
||||
await run(
|
||||
css`
|
||||
@import './foo/bar.css';
|
||||
@config './root-config.js';
|
||||
@plugin './root-plugin.js';
|
||||
`,
|
||||
{ loadStylesheet, loadModule },
|
||||
)
|
||||
).trim(),
|
||||
).toBe('')
|
||||
|
||||
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 () => {
|
||||
let loadStylesheet = () =>
|
||||
Promise.resolve({
|
||||
content: css`
|
||||
@source './nested/**/*.css';
|
||||
`,
|
||||
base: '/root/foo',
|
||||
})
|
||||
|
||||
let compiler = await compile(
|
||||
css`
|
||||
@import './foo/bar.css';
|
||||
@source './root/**/*.css';
|
||||
`,
|
||||
{ base: '/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';
|
||||
`,
|
||||
{ base: '/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' },
|
||||
])
|
||||
})
|
||||
|
||||
test('it crashes when inside a cycle', async () => {
|
||||
let loadStylesheet = () =>
|
||||
Promise.resolve({
|
||||
content: css`
|
||||
@import 'foo.css';
|
||||
`,
|
||||
base: '/root',
|
||||
})
|
||||
|
||||
expect(
|
||||
run(
|
||||
css`
|
||||
@import 'foo.css';
|
||||
`,
|
||||
{ loadStylesheet },
|
||||
),
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`,
|
||||
)
|
||||
})
|
||||
147
packages/tailwindcss/src/at-import.ts
Normal file
147
packages/tailwindcss/src/at-import.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { context, rule, walk, WalkAction, type AstNode } from './ast'
|
||||
import * as CSS from './css-parser'
|
||||
import * as ValueParser from './value-parser'
|
||||
|
||||
type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }>
|
||||
|
||||
export async function substituteAtImports(
|
||||
ast: AstNode[],
|
||||
base: string,
|
||||
loadStylesheet: LoadStylesheet,
|
||||
recurseCount = 0,
|
||||
) {
|
||||
let promises: Promise<void>[] = []
|
||||
|
||||
walk(ast, (node, { replaceWith }) => {
|
||||
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 or remote URIs
|
||||
if (uri.startsWith('data:')) return
|
||||
if (uri.startsWith('http://') || uri.startsWith('https://')) return
|
||||
|
||||
let contextNode = context({}, [])
|
||||
|
||||
promises.push(
|
||||
(async () => {
|
||||
// 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}\`)`,
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
// Modified and inlined version of `parse-statements` from
|
||||
// `postcss-import` <https://github.com/postcss/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
|
||||
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' && 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') &&
|
||||
node.value.toLowerCase() === 'layer'
|
||||
) {
|
||||
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' && node.value.toLowerCase() === 'supports') {
|
||||
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
|
||||
}
|
||||
@ -15,21 +15,25 @@ import { registerThemeVariantOverrides } from './theme-variants'
|
||||
|
||||
export async function applyCompatibilityHooks({
|
||||
designSystem,
|
||||
base,
|
||||
ast,
|
||||
loadPlugin,
|
||||
loadConfig,
|
||||
loadModule,
|
||||
globs,
|
||||
}: {
|
||||
designSystem: DesignSystem
|
||||
base: string
|
||||
ast: AstNode[]
|
||||
loadPlugin: (path: string) => Promise<Plugin>
|
||||
loadConfig: (path: string) => Promise<UserConfig>
|
||||
loadModule: (
|
||||
path: string,
|
||||
base: string,
|
||||
resourceHint: 'plugin' | 'config',
|
||||
) => 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 +90,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.base },
|
||||
Object.keys(options).length > 0 ? options : null,
|
||||
])
|
||||
|
||||
replaceWith([])
|
||||
return
|
||||
@ -102,7 +109,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.base })
|
||||
replaceWith([])
|
||||
return
|
||||
}
|
||||
@ -142,38 +149,48 @@ export async function applyCompatibilityHooks({
|
||||
// any additional backwards compatibility hooks.
|
||||
if (!pluginPaths.length && !configPaths.length) return
|
||||
|
||||
let configs = await Promise.all(
|
||||
configPaths.map(async (configPath) => ({
|
||||
path: configPath,
|
||||
config: await loadConfig(configPath),
|
||||
})),
|
||||
)
|
||||
let pluginDetails = await Promise.all(
|
||||
pluginPaths.map(async ([pluginPath, pluginOptions]) => ({
|
||||
path: pluginPath,
|
||||
plugin: await loadPlugin(pluginPath),
|
||||
options: pluginOptions,
|
||||
})),
|
||||
)
|
||||
let [configs, pluginDetails] = await Promise.all([
|
||||
Promise.all(
|
||||
configPaths.map(async ({ id, base }) => {
|
||||
let loaded = await loadModule(id, base, 'config')
|
||||
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, 'plugin')
|
||||
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)
|
||||
|
||||
@ -221,7 +238,7 @@ export async function applyCompatibilityHooks({
|
||||
)
|
||||
}
|
||||
|
||||
globs.push({ origin: file.base, pattern: file.pattern })
|
||||
globs.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ test('Config values can be merged into the theme', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
},
|
||||
])
|
||||
applyConfigToTheme(design, resolvedUserConfig)
|
||||
|
||||
@ -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,10 +12,10 @@ test('Config files can add content', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({ content: ['./file.txt'] }),
|
||||
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 () => {
|
||||
@ -25,7 +25,7 @@ test('Config files can change dark mode (media)', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
fontSize: {
|
||||
base: ['200rem', { lineHeight: '201rem' }],
|
||||
md: ['200rem', { lineHeight: '201rem' }],
|
||||
xl: ['200rem', { lineHeight: '201rem' }],
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
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],
|
||||
},
|
||||
'[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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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, {
|
||||
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',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -1096,14 +1170,17 @@ test('merges css breakpoints with js config screens', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '44rem',
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '44rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@ -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',
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -20,14 +20,17 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '44rem',
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '44rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -100,17 +103,20 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
xs: '30rem',
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '60rem',
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
xs: '30rem',
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '60rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -195,14 +201,17 @@ test('JS config `screens` only setup, even if those match the default-theme expo
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '64rem',
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
screens: {
|
||||
sm: '40rem',
|
||||
md: '48rem',
|
||||
lg: '64rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -271,14 +280,17 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -374,16 +386,19 @@ test('JS config with `theme: { extends }` should not include the `default-config
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
mini: '40rem',
|
||||
midi: '48rem',
|
||||
maxi: '64rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -449,22 +464,25 @@ describe('complex screen configs', () => {
|
||||
`
|
||||
|
||||
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)' },
|
||||
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',
|
||||
}),
|
||||
})
|
||||
|
||||
@ -533,15 +551,18 @@ describe('complex screen configs', () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
loadConfig: async () => ({
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '40rem',
|
||||
portrait: { raw: 'screen and (orientation: portrait)' },
|
||||
loadModule: async () => ({
|
||||
module: {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
sm: '40rem',
|
||||
portrait: { raw: 'screen and (orientation: portrait)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@ -336,7 +336,7 @@ describe('theme function', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadConfig: async () => ({}),
|
||||
loadModule: async () => ({ module: {}, base: '/root' }),
|
||||
},
|
||||
)
|
||||
|
||||
@ -795,23 +795,26 @@ describe('in plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
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',
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -850,31 +853,34 @@ describe('in JS config files', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
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)',
|
||||
},
|
||||
})
|
||||
plugins: [
|
||||
plugin(({ addBase, addUtilities }) => {
|
||||
addBase({
|
||||
'.my-base-rule': {
|
||||
background: 'theme(colors.primary)',
|
||||
color: 'theme(colors.secondary)',
|
||||
},
|
||||
})
|
||||
|
||||
addUtilities({
|
||||
'.my-utility': {
|
||||
color: 'theme(colors.red)',
|
||||
},
|
||||
})
|
||||
}),
|
||||
],
|
||||
addUtilities({
|
||||
'.my-utility': {
|
||||
color: 'theme(colors.red)',
|
||||
},
|
||||
})
|
||||
}),
|
||||
],
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
@ -1429,17 +1429,20 @@ describe('Parsing themes values from CSS', () => {
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{
|
||||
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',
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -1472,16 +1475,19 @@ describe('Parsing themes values from CSS', () => {
|
||||
@tailwind utilities;
|
||||
`,
|
||||
{
|
||||
loadConfig: async () => {
|
||||
loadModule: async () => {
|
||||
return {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
red: 'tomato',
|
||||
orange: '#f28500',
|
||||
module: {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
red: 'tomato',
|
||||
orange: '#f28500',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -1511,11 +1517,12 @@ describe('plugins', () => {
|
||||
@plugin;
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('hocus', '&:hover, &:focus')
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`))
|
||||
@ -1527,11 +1534,12 @@ describe('plugins', () => {
|
||||
@plugin '';
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('hocus', '&:hover, &:focus')
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`))
|
||||
@ -1545,11 +1553,12 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('hocus', '&:hover, &:focus')
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` cannot be nested.]`))
|
||||
@ -1565,8 +1574,8 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return plugin.withOptions((options) => {
|
||||
loadModule: async () => ({
|
||||
module: plugin.withOptions((options) => {
|
||||
expect(options).toEqual({
|
||||
color: 'red',
|
||||
})
|
||||
@ -1578,8 +1587,9 @@ describe('plugins', () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
@ -1616,8 +1626,8 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return plugin.withOptions((options) => {
|
||||
loadModule: async () => ({
|
||||
module: plugin.withOptions((options) => {
|
||||
expect(options).toEqual({
|
||||
'is-null': null,
|
||||
'is-true': true,
|
||||
@ -1636,8 +1646,9 @@ describe('plugins', () => {
|
||||
})
|
||||
|
||||
return () => {}
|
||||
})
|
||||
},
|
||||
}),
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
})
|
||||
@ -1655,8 +1666,8 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return plugin.withOptions((options) => {
|
||||
loadModule: async () => ({
|
||||
module: plugin.withOptions((options) => {
|
||||
return ({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.text-primary': {
|
||||
@ -1664,8 +1675,9 @@ describe('plugins', () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
@ -1692,15 +1704,16 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return plugin(({ addUtilities }) => {
|
||||
loadModule: async () => ({
|
||||
module: plugin(({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.text-primary': {
|
||||
color: 'red',
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}),
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
@ -1717,7 +1730,7 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => plugin(() => {}),
|
||||
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
@ -1738,7 +1751,7 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => plugin(() => {}),
|
||||
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
|
||||
},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
@ -1763,11 +1776,12 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('hocus', '&:hover, &:focus')
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
let compiled = build(['hocus:underline', 'group-hocus:flex'])
|
||||
@ -1794,11 +1808,12 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('hocus', ['&:hover', '&:focus'])
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
@ -1826,14 +1841,15 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
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'])
|
||||
@ -1860,16 +1876,17 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
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'])
|
||||
@ -1908,8 +1925,8 @@ describe('plugins', () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('hocus', {
|
||||
'&': {
|
||||
'--custom-property': '@slot',
|
||||
@ -1917,8 +1934,9 @@ describe('plugins', () => {
|
||||
'&:focus': '@slot',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
let compiled = build(['hocus:underline'])
|
||||
@ -1944,13 +1962,13 @@ describe('plugins', () => {
|
||||
@tailwind utilities;
|
||||
}
|
||||
`,
|
||||
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addVariant }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addVariant }: PluginAPI) => {
|
||||
addVariant('dark', '&:is([data-theme=dark] *)')
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
let compiled = build(
|
||||
@ -1981,20 +1999,29 @@ 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";
|
||||
`,
|
||||
{ base: '/root' },
|
||||
)
|
||||
|
||||
expect(globs).toEqual([{ pattern: './foo/bar/*.ts' }])
|
||||
expect(globs).toEqual([{ pattern: './foo/bar/*.ts', base: '/root' }])
|
||||
})
|
||||
|
||||
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";
|
||||
`,
|
||||
{ base: '/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' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -2513,17 +2540,17 @@ test('addBase', async () => {
|
||||
@tailwind utilities;
|
||||
}
|
||||
`,
|
||||
|
||||
{
|
||||
loadPlugin: async () => {
|
||||
return ({ addBase }: PluginAPI) => {
|
||||
loadModule: async () => ({
|
||||
module: ({ addBase }: PluginAPI) => {
|
||||
addBase({
|
||||
body: {
|
||||
'font-feature-settings': '"tnum"',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
base: '/root',
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
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 AstNode,
|
||||
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'
|
||||
@ -15,16 +26,21 @@ export type Config = UserConfig
|
||||
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
|
||||
|
||||
type CompileOptions = {
|
||||
loadPlugin?: (path: string) => Promise<Plugin>
|
||||
loadConfig?: (path: string) => Promise<UserConfig>
|
||||
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 }>
|
||||
}
|
||||
|
||||
function throwOnPlugin(): never {
|
||||
throw new Error('No `loadPlugin` function provided to `compile`')
|
||||
function throwOnLoadModule(): never {
|
||||
throw new Error('No `loadModule` function provided to `compile`')
|
||||
}
|
||||
|
||||
function throwOnConfig(): never {
|
||||
throw new Error('No `loadConfig` function provided to `compile`')
|
||||
function throwOnLoadStylesheet(): never {
|
||||
throw new Error('No `loadStylesheet` function provided to `compile`')
|
||||
}
|
||||
|
||||
function parseThemeOptions(selector: string) {
|
||||
@ -45,9 +61,15 @@ function parseThemeOptions(selector: string) {
|
||||
|
||||
async function parseCss(
|
||||
css: string,
|
||||
{ loadPlugin = throwOnPlugin, loadConfig = throwOnConfig }: CompileOptions = {},
|
||||
{
|
||||
base = '',
|
||||
loadModule = throwOnLoadModule,
|
||||
loadStylesheet = throwOnLoadStylesheet,
|
||||
}: CompileOptions = {},
|
||||
) {
|
||||
let ast = CSS.parse(css)
|
||||
let ast = [context({ base }, CSS.parse(css))] as AstNode[]
|
||||
|
||||
await substituteAtImports(ast, base, loadStylesheet)
|
||||
|
||||
// Find all `@theme` declarations
|
||||
let theme = new Theme()
|
||||
@ -55,9 +77,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
|
||||
@ -104,7 +126,7 @@ async function parseCss(
|
||||
) {
|
||||
throw new Error('`@source` paths must be quoted.')
|
||||
}
|
||||
globs.push({ pattern: path.slice(1, -1) })
|
||||
globs.push({ base: context.base, pattern: path.slice(1, -1) })
|
||||
replaceWith([])
|
||||
return
|
||||
}
|
||||
@ -234,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, loadPlugin, loadConfig, globs })
|
||||
await applyCompatibilityHooks({ designSystem, base, ast, loadModule, globs })
|
||||
|
||||
for (let customVariant of customVariants) {
|
||||
customVariant(designSystem)
|
||||
@ -316,7 +338,7 @@ export async function compile(
|
||||
css: 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, opts)
|
||||
|
||||
@ -10,15 +10,16 @@ test('plugin', async () => {
|
||||
`
|
||||
|
||||
let compiler = await compile(input, {
|
||||
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, {
|
||||
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(`
|
||||
|
||||
79
pnpm-lock.yaml
generated
79
pnpm-lock.yaml
generated
@ -156,25 +156,15 @@ 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:
|
||||
enhanced-resolve:
|
||||
specifier: ^5.17.1
|
||||
version: 5.17.1
|
||||
jiti:
|
||||
specifier: ^2.0.0-beta.3
|
||||
version: 2.0.0-beta.3
|
||||
@ -194,9 +184,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
|
||||
@ -205,17 +192,17 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 20.14.13
|
||||
'@types/postcss-import':
|
||||
specifier: ^14.0.3
|
||||
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:
|
||||
@ -326,12 +313,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
|
||||
@ -339,33 +320,12 @@ 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
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
|
||||
|
||||
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':
|
||||
@ -374,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)
|
||||
@ -1055,7 +1018,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':
|
||||
@ -1067,7 +1029,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':
|
||||
@ -1091,7 +1052,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':
|
||||
@ -1103,7 +1063,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':
|
||||
@ -1115,7 +1074,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':
|
||||
@ -1127,7 +1085,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':
|
||||
@ -1151,7 +1108,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':
|
||||
@ -1487,13 +1443,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
|
||||
|
||||
@ -2260,13 +2214,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:
|
||||
@ -2284,25 +2236,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:
|
||||
@ -2314,7 +2262,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:
|
||||
@ -4442,7 +4389,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)
|
||||
@ -4466,7 +4413,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
|
||||
@ -4488,7 +4435,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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user