This commit is contained in:
Jordan Pittman 2024-10-04 11:25:15 -04:00
parent bef1f724f9
commit 87900fe5bc
3 changed files with 125 additions and 169 deletions

View file

@ -268,7 +268,7 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
postcssPlugin: '@tailwindcss/upgrade/migrate-at-layer-utilities',
OnceExit: (root, { atRule }) => {
let isUtilityStylesheet =
stylesheet.layers?.includes('utilities') || stylesheet.layers?.includes('components')
stylesheet.layers?.has('utilities') || stylesheet.layers?.has('components')
if (isUtilityStylesheet) {
let rule = atRule({ name: 'layer', params: 'utilities' })

View file

@ -123,7 +123,15 @@ async function run() {
let stylesheets: Stylesheet[] = files.map((file) => ({ file }))
// Load and parse all stylesheets
await Promise.allSettled(stylesheets.map((sheet) => prepareStylesheet(sheet)))
let prepareResults = await Promise.allSettled(
stylesheets.map((sheet) => prepareStylesheet(sheet)),
)
for (let result of prepareResults) {
if (result.status === 'rejected') {
error(`${result.reason}`)
}
}
// Analyze the stylesheets
await analyzeStylesheets(stylesheets)
@ -134,7 +142,16 @@ async function run() {
designSystem: parsedConfig?.designSystem,
userConfig: parsedConfig?.userConfig,
}
await Promise.allSettled(stylesheets.map((sheet) => migrateStylesheet(sheet, options)))
let migrateResults = await Promise.allSettled(
stylesheets.map((sheet) => migrateStylesheet(sheet, options)),
)
for (let result of migrateResults) {
if (result.status === 'rejected') {
error(`${result.reason}`)
}
}
// Split up stylesheets (as needed)
await splitStylesheets(stylesheets)

View file

@ -1,7 +1,6 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import postcss, { AtRule } from 'postcss'
import postcssImport from 'postcss-import'
import type { Config } from 'tailwindcss'
import type { DesignSystem } from '../../tailwindcss/src/design-system'
import { segment } from '../../tailwindcss/src/utils/segment'
@ -9,6 +8,7 @@ import { migrateAtApply } from './codemods/migrate-at-apply'
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
import { resolveCssId } from './utils/resolve'
import { walk, WalkAction } from './utils/walk'
export interface MigrateOptions {
@ -26,10 +26,12 @@ export interface Stylesheet {
content?: string | null
root?: postcss.Root | null
layers?: string[]
layers?: Set<string>
parents?: Set<Stylesheet>
importRules?: Set<AtRule>
readonly ancestors?: Set<Stylesheet>
}
export async function migrateContents(
@ -43,6 +45,9 @@ export async function migrateContents(
}
}
console.log(stylesheet.file)
console.log(stylesheet.root!.toString())
return postcss()
.use(migrateAtApply(options))
.use(migrateAtLayerUtilities(stylesheet))
@ -67,173 +72,84 @@ export async function analyze(stylesheets: Stylesheet[]) {
if (!stylesheet.file) continue
stylesheetsByFile.set(stylesheet.file, stylesheet)
stylesheet.layers ??= []
stylesheet.layers ??= new Set()
stylesheet.importRules ??= new Set()
stylesheet.parents ??= new Set()
Object.defineProperty(stylesheet, 'ancestors', {
get: () => {
function* ancestors(sheet: Stylesheet): Iterable<Stylesheet> {
for (let parent of sheet.parents ?? []) {
yield parent
yield* ancestors(parent)
}
}
return new Set(ancestors(stylesheet))
},
})
}
// A list of all marker nodes used to annotate and analyze the AST
let importMarkers = new Set<postcss.Node>()
let fileMarkers = new Set<postcss.Node>()
// Step 1: Record which `@import` rules point to which stylesheets
// and which stylesheets are parents/children of each other
let processor = postcss([
// Step 1: Add markers around the `@import` rules
//
// We need to mark the start and end of each `@import` rule so we can
// keep track of where they were in the original AST. We do this by cloning
// the node, renaming the original, and adding start/end markers around it.
{
postcssPlugin: 'import-thing',
Once(root) {
let imports = new Set<AtRule>()
postcssPlugin: 'mark-import-nodes',
AtRule: {
import(node) {
// Find what the import points to
let id = node.params.match(/['"](.*)['"]/)?.[1]
if (!id) return
root.walkAtRules('import', (node) => {
imports.add(node)
})
let basePath = node.source?.input.file
? path.dirname(node.source.input.file)
: process.cwd()
for (let node of imports) {
// Duplicate the `@import` rule
// this will be the one that `postcss-import` processes
node.cloneAfter({
params: `${node.params} ${mediaWrapper}`,
})
// Resolve the import to a file path
let resolvedPath: string | false
try {
resolvedPath = resolveCssId(id, basePath)
} catch (err) {
console.warn(`Failed to resolve import: ${id}. Skipping.`)
console.error(err)
return
}
// Replace the original `@import` rule with a dummy comment
// it'll retain the original node in `raws`
let importMarker = postcss.comment({
text: `__import_node__`,
raws: { original: node },
})
if (!resolvedPath) return
importMarkers.add(importMarker)
node.replaceWith(importMarker)
}
},
},
// Find the stylesheet pointing to the resolved path
let stylesheet = stylesheetsByFile.get(resolvedPath)
// Step 2: Expand `@import` rules and mark imported files
//
// Since valid imports are only at the top some files may not have any other
// nodes we can use to determine where in the AST a file was imported. To
// solve this, we'll add a marker at the top of each imported file which
// guarantees that we have a way to determine where the file was imported.
postcssImport({
plugins: [
{
postcssPlugin: 'import-marker',
Once(root) {
let marker = postcss.comment({
text: `marker:imported-file`,
source: root.source,
})
// If it _does not_ exist in stylesheets we don't care and skip it
// this is likely because its in node_modules or a workspace package
// that we don't want to modify
if (!stylesheet) return
fileMarkers.add(marker)
root.prepend(marker)
},
// If it does then this import node get added to that sylesheets `importRules` set
let parent = stylesheetsByFile.get(node.source?.input.file ?? '')
if (!parent) return
// Record the import node for this sheet so it can be modified later
stylesheet.importRules!.add(node)
// Connect all stylesheets together in a dependency graph
// The way this works is it uses the knowledge that we have a list of
// the `@import` nodes that cause a given stylesheet to be imported.
// That import has a `source` pointing to parent stylesheet's file path
// which can be used to look it up
stylesheet.parents!.add(parent)
for (let part of segment(node.params, ' ')) {
if (!part.startsWith('layer(')) continue
if (!part.endsWith(')')) continue
stylesheet.layers!.add(part.slice(6, -1).trim())
}
},
],
}),
// Step 3: Analyze the AST so each stylesheet can have each import node
// associated with it
{
postcssPlugin: 'import-thing2',
Once() {
// Associate import nodes with each stylesheet
for (let fileMarker of fileMarkers) {
let sourceFile = fileMarker.source?.input.file
if (!sourceFile) continue
let stylesheet = stylesheetsByFile.get(sourceFile)
if (!stylesheet) continue
// Find the closest import marker that precedes the file marker
let node = fileMarker
while (node) {
if (importMarkers.has(node)) {
break
}
let prev = node.prev()
if (prev) {
// Walk backwards until we find a node that is a marker
node = prev
} else if (node.parent) {
// If there are no earlier siblings, go up a level and try again
node = node.parent
} else {
break
}
}
// We were unable to find an import marker
// TODO: This should be an error
if (node === fileMarker) continue
// TODO: This shouldn't be possible
if (!node.raws.original) continue
stylesheet.importRules!.add(node.raws.original as AtRule)
}
// Analyze import nodes to determine layers
for (let sheet of stylesheets) {
for (let node of sheet.importRules ?? []) {
let parts = segment(node.params, ' ')
for (let part of parts) {
if (!part.startsWith('layer(')) continue
if (!part.endsWith(')')) continue
let layers = segment(part.slice(6, -1), ',').map((name) => name.trim())
sheet.layers!.push(...layers)
}
}
}
// Connect all stylesheets together in a dependency graph
// The way this works is it uses the knowledge that we have a list of
// the `@import` nodes that cause a given stylesheet to be imported.
// That import has a `source` pointing to parent stylesheet's file path
// which can be used to look it up
for (let sheet of stylesheets) {
for (let node of sheet.importRules ?? []) {
if (!node.source?.input.file) continue
let sourceFile = node.source.input.file
for (let parent of stylesheets) {
if (parent.file !== sourceFile) continue
// Track root import and `@import` node. This will allow us to
// hoist everything up to the root import when splitting the
// stylesheet.
sheet.rootFile = parent.rootFile ?? parent.file
sheet.rootImport = parent.rootImport ?? node
sheet.parents!.add(parent)
}
}
}
},
},
// Step 4: Restore the AST to its original state
{
postcssPlugin: 'import-thing2',
Once(root) {
// Replace the dummy comment nodes with the original `@import` nodes
for (let node of importMarkers) {
node.replaceWith(node.raws.original)
}
// Remove all imported nodes
root.walkAtRules('media', (rule) => {
if (!rule.params.includes(mediaWrapper)) return
rule.remove()
})
},
},
])
@ -244,6 +160,15 @@ export async function analyze(stylesheets: Stylesheet[]) {
await processor.process(sheet.root, { from: sheet.file })
}
// Step 2: Analyze the AST so each stylesheet can know what layers it is inside
for (let sheet of stylesheets) {
for (let ancestor of sheet.ancestors ?? []) {
for (let layer of ancestor.layers ?? []) {
sheet.layers!.add(layer)
}
}
}
}
export async function prepare(stylesheet: Stylesheet) {
@ -268,7 +193,7 @@ export async function split(stylesheets: Stylesheet[]) {
if (!sheet.file) continue
// We only care about stylesheets that were imported into a layer e.g. `layer(utilities)`
let isLayered = sheet.layers?.includes('utilities') || sheet.layers?.includes('components')
let isLayered = sheet.layers?.has('utilities') || sheet.layers?.has('components')
if (!isLayered) continue
// We only care about stylesheets that contain an `@utility`
@ -283,6 +208,9 @@ export async function split(stylesheets: Stylesheet[]) {
return WalkAction.Stop
})
console.log(sheet.file)
console.log(sheet.root.toString())
if (!hasUtilities) continue
// Split the stylesheet into two parts: one with the utilities and one without
@ -311,13 +239,13 @@ export async function split(stylesheets: Stylesheet[]) {
}
// Only interested in the main import rule with the layer
if (!node.params.includes('layer(utilities)') && !node.params.includes('layer(components)')) {
continue
}
// if (!node.params.includes('layer(utilities)') && !node.params.includes('layer(components)')) {
// continue
// }
if (node !== sheet.rootImport) {
continue
}
// if (node !== sheet.rootImport) {
// continue
// }
// We want to use the name of the main import, not the name of the
// transitive import.
@ -347,7 +275,7 @@ export async function split(stylesheets: Stylesheet[]) {
let name = path.basename(relativePath[1])
let utilitySheet: Stylesheet = {
file: path.join(path.dirname(sheet.rootFile!), name.replace(/\.css$/, '.utilities.css')),
file: path.join(path.dirname(sheet.file!), name.replace(/\.css$/, '.utilities.css')),
root: utilities,
}
@ -373,6 +301,17 @@ export async function split(stylesheets: Stylesheet[]) {
}
}
console.dir(
[
...Array.from(stylesheets.slice(), (s) => [
s.file,
Array.from(s.importRules ?? [], (r) => r.toString()),
]),
Array.from(utilitySheets.values(), (s) => s.file),
],
{ depth: 1 },
)
// Merge utility sheets.
// It could be that the same type of file is created from two different
// locations. In this case, the final file will exist twice.
@ -468,6 +407,9 @@ export async function split(stylesheets: Stylesheet[]) {
}
}
// @import './a.css' layer(utilities) ;
// -> @utility { … }
// @import './a.css' layer(utilities);
// -> @import './b.css';
// -> @import './c.css';
@ -477,11 +419,8 @@ export async function split(stylesheets: Stylesheet[]) {
// -> other stuff
// @import './a.css' layer(utilities);
// -> @import './b.css';
// -> @import './b.css'; (layers: utilities)
// -> @import './c.css';
// -> #main
// -> @import './d.css';
// -> #main
// -> other stuff
// @import './a.utility.css';
// -> @import './b.utility.css';
// -> @import './c.utility.css';
// -> @utility .utility-class