This PR bumps dependencies in all the packages, typically just bumping to the latest patch release. Closes: #19936 Closes: #19917 Closes: #19899 Closes: #19897 Closes: #19845 Closes: #19832 Closes: #19967 Closes: #19968 ## Test plan 1. All tests still pass 2. All integration tests still pass [ci-all] to verify Linux, Windows and macOS
317 lines
8.3 KiB
TypeScript
317 lines
8.3 KiB
TypeScript
import { __unstable__loadDesignSystem, compileAst } from '@tailwindcss/node'
|
|
import * as fsSync from 'node:fs'
|
|
import * as fs from 'node:fs/promises'
|
|
import * as path from 'node:path'
|
|
import * as util from 'node:util'
|
|
import * as postcss from 'postcss'
|
|
import { postCssAstToCssAst } from '../../@tailwindcss-postcss/src/ast'
|
|
|
|
export type StylesheetId = string
|
|
|
|
export interface StylesheetConnection {
|
|
item: Stylesheet
|
|
meta: {
|
|
layers: string[]
|
|
}
|
|
}
|
|
|
|
export class Stylesheet {
|
|
/**
|
|
* A unique identifier for this stylesheet
|
|
*
|
|
* Used to track the stylesheet in PostCSS nodes.
|
|
*/
|
|
id: StylesheetId
|
|
|
|
/**
|
|
* The PostCSS AST that represents this stylesheet.
|
|
*/
|
|
root: postcss.Root
|
|
|
|
/**
|
|
* Whether or not this stylesheet is a Tailwind CSS root stylesheet.
|
|
*/
|
|
isTailwindRoot = false
|
|
|
|
/**
|
|
* The Tailwind config path that is linked to this stylesheet. Essentially the
|
|
* contents of `@config`.
|
|
*/
|
|
linkedConfigPath: string | null = null
|
|
|
|
/**
|
|
* The path to the file that this stylesheet was loaded from.
|
|
*
|
|
* If this stylesheet was not loaded from a file this will be `null`.
|
|
*/
|
|
file: string | null = null
|
|
|
|
/**
|
|
* Stylesheets that import this stylesheet.
|
|
*/
|
|
parents = new Set<StylesheetConnection>()
|
|
|
|
/**
|
|
* Stylesheets that are imported by stylesheet.
|
|
*/
|
|
children = new Set<StylesheetConnection>()
|
|
|
|
/**
|
|
* Whether or not this stylesheet can be migrated
|
|
*/
|
|
canMigrate = true
|
|
|
|
/**
|
|
* Whether or not this stylesheet can be migrated
|
|
*/
|
|
extension: string | null = null
|
|
|
|
static async load(filepath: string) {
|
|
filepath = path.resolve(process.cwd(), filepath)
|
|
|
|
let css = await fs.readFile(filepath, 'utf-8')
|
|
let root = postcss.parse(css, { from: filepath })
|
|
|
|
return new Stylesheet(root, filepath)
|
|
}
|
|
|
|
static loadSync(filepath: string) {
|
|
filepath = path.resolve(process.cwd(), filepath)
|
|
|
|
let css = fsSync.readFileSync(filepath, 'utf-8')
|
|
let root = postcss.parse(css, { from: filepath })
|
|
|
|
return new Stylesheet(root, filepath)
|
|
}
|
|
|
|
static async fromString(css: string) {
|
|
let root = postcss.parse(css)
|
|
|
|
return new Stylesheet(root)
|
|
}
|
|
|
|
static async fromRoot(root: postcss.Root, file?: string) {
|
|
return new Stylesheet(root, file)
|
|
}
|
|
|
|
constructor(root: postcss.Root, file?: string) {
|
|
this.id = Math.random().toString(36).slice(2)
|
|
this.root = root
|
|
this.file = file ?? null
|
|
|
|
if (file) {
|
|
this.extension = path.extname(file)
|
|
}
|
|
}
|
|
|
|
get importRules() {
|
|
let imports = new Set<postcss.AtRule>()
|
|
|
|
this.root.walkAtRules('import', (rule) => {
|
|
imports.add(rule)
|
|
})
|
|
|
|
return imports
|
|
}
|
|
|
|
get isEmpty() {
|
|
return this.root.toString().trim() === ''
|
|
}
|
|
|
|
*ancestors() {
|
|
for (let { item } of walkDepth(this, (sheet) => sheet.parents)) {
|
|
yield item
|
|
}
|
|
}
|
|
|
|
*descendants() {
|
|
for (let { item } of walkDepth(this, (sheet) => sheet.children)) {
|
|
yield item
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the layers the stylesheet is imported into directly or indirectly
|
|
*/
|
|
layers() {
|
|
let layers = new Set<string>()
|
|
|
|
for (let { item, path } of walkDepth(this, (sheet) => sheet.parents)) {
|
|
if (item.parents.size > 0) {
|
|
continue
|
|
}
|
|
|
|
for (let { meta } of path) {
|
|
for (let layer of meta.layers) {
|
|
layers.add(layer)
|
|
}
|
|
}
|
|
}
|
|
|
|
return layers
|
|
}
|
|
|
|
/**
|
|
* Iterate all paths from a stylesheet through its ancestors to all roots
|
|
*
|
|
* For example, given the following structure:
|
|
*
|
|
* ```
|
|
* c.css
|
|
* -> a.1.css @import "…"
|
|
* -> a.css
|
|
* -> root.1.css (utility: no)
|
|
* -> root.2.css (utility: no)
|
|
* -> b.css
|
|
* -> root.1.css (utility: no)
|
|
* -> root.2.css (utility: no)
|
|
*
|
|
* -> a.2.css @import "…" layer(foo)
|
|
* -> a.css
|
|
* -> root.1.css (utility: no)
|
|
* -> root.2.css (utility: no)
|
|
* -> b.css
|
|
* -> root.1.css (utility: no)
|
|
* -> root.2.css (utility: no)
|
|
*
|
|
* -> b.1.css @import "…" layer(components / utilities)
|
|
* -> a.css
|
|
* -> root.1.css (utility: yes)
|
|
* -> root.2.css (utility: yes)
|
|
* -> b.css
|
|
* -> root.1.css (utility: yes)
|
|
* -> root.2.css (utility: yes)
|
|
* ```
|
|
*
|
|
* We can see there are a total of 12 import paths with various layers.
|
|
* We need to be able to iterate every one of these paths and inspect
|
|
* the layers used in each path..
|
|
*/
|
|
*pathsToRoot(): Iterable<StylesheetConnection[]> {
|
|
for (let { item, path } of walkDepth(this, (sheet) => sheet.parents)) {
|
|
// Skip over intermediate stylesheets since all paths from a leaf to a
|
|
// root will encompass all possible intermediate stylesheet paths.
|
|
if (item.parents.size > 0) {
|
|
continue
|
|
}
|
|
|
|
yield path
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze a stylesheets import paths to see if some can be considered
|
|
* for conversion to utility rules and others can't.
|
|
*
|
|
* If a stylesheet is imported directly or indirectly and some imports are in
|
|
* a utility layer and some are not that means that we can't safely convert
|
|
* the rules in the stylesheet to `@utility`. Doing so would mean that we
|
|
* would need to replicate the stylesheet and change one to have `@utility`
|
|
* rules and leave the other as is.
|
|
*
|
|
* We can see, given the same structure from the `pathsToRoot` example, that
|
|
* `css.css` is imported into different layers:
|
|
* - `a.1.css` has no layers and should not be converted
|
|
* - `a.2.css` has a layer `foo` and should not be converted
|
|
* - `b.1.css` has a layer `utilities` (or `components`) which should be
|
|
*
|
|
* Since this means that `c.css` must both not be converted and converted
|
|
* we can't do this without replicating the stylesheet, any ancestors, and
|
|
* adjusting imports which is a non-trivial task.
|
|
*/
|
|
analyzeImportPaths() {
|
|
let convertiblePaths: StylesheetConnection[][] = []
|
|
let nonConvertiblePaths: StylesheetConnection[][] = []
|
|
|
|
for (let path of this.pathsToRoot()) {
|
|
let isConvertible = false
|
|
|
|
for (let { meta } of path) {
|
|
for (let layer of meta.layers) {
|
|
isConvertible ||= layer === 'utilities' || layer === 'components'
|
|
}
|
|
}
|
|
|
|
if (isConvertible) {
|
|
convertiblePaths.push(path)
|
|
} else {
|
|
nonConvertiblePaths.push(path)
|
|
}
|
|
}
|
|
|
|
return { convertiblePaths, nonConvertiblePaths }
|
|
}
|
|
|
|
containsRule(cb: (rule: postcss.AnyNode) => boolean) {
|
|
let contains = false
|
|
|
|
this.root.walk((rule) => {
|
|
if (cb(rule)) {
|
|
contains = true
|
|
return false
|
|
}
|
|
})
|
|
|
|
if (contains) {
|
|
return true
|
|
}
|
|
|
|
for (let child of this.children) {
|
|
if (child.item.containsRule(cb)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
#compiler?: Promise<Awaited<ReturnType<typeof compileAst>> | null>
|
|
#designSystem?: Promise<Awaited<ReturnType<typeof __unstable__loadDesignSystem>> | null>
|
|
|
|
async compiler(): Promise<Awaited<ReturnType<typeof compileAst>> | null> {
|
|
if (!this.isTailwindRoot) return null
|
|
if (!this.file) return null
|
|
|
|
return (this.#compiler ??= compileAst(postCssAstToCssAst(this.root), {
|
|
base: path.dirname(this.file),
|
|
onDependency() {},
|
|
}))
|
|
}
|
|
|
|
async designSystem(): Promise<Awaited<ReturnType<typeof __unstable__loadDesignSystem>> | null> {
|
|
if (!this.isTailwindRoot) return null
|
|
if (!this.file) return null
|
|
|
|
return (this.#designSystem ??= __unstable__loadDesignSystem(this.root.toString(), {
|
|
base: path.dirname(this.file),
|
|
}))
|
|
}
|
|
|
|
[util.inspect.custom]() {
|
|
return {
|
|
...this,
|
|
root: this.root.toString(),
|
|
layers: Array.from(this.layers()),
|
|
parents: Array.from(this.parents, (s) => s.item.id),
|
|
children: Array.from(this.children, (s) => s.item.id),
|
|
parentsMeta: Array.from(this.parents, (s) => s.meta),
|
|
childrenMeta: Array.from(this.children, (s) => s.meta),
|
|
}
|
|
}
|
|
}
|
|
|
|
function* walkDepth(
|
|
value: Stylesheet,
|
|
connections: (value: Stylesheet) => Iterable<StylesheetConnection>,
|
|
path: StylesheetConnection[] = [],
|
|
): Iterable<{ item: Stylesheet; path: StylesheetConnection[] }> {
|
|
for (let connection of connections(value)) {
|
|
let newPath = [...path, connection]
|
|
|
|
yield* walkDepth(connection.item, connections, newPath)
|
|
yield {
|
|
item: connection.item,
|
|
path: newPath,
|
|
}
|
|
}
|
|
}
|