Skip to content

Commit ce485d7

Browse files
committed
refactor(tailwindcss-patch): extract config source migration engine
1 parent 7a91dd1 commit ce485d7

3 files changed

Lines changed: 375 additions & 317 deletions

File tree

packages/tailwindcss-patch/src/commands/migrate-config.ts

Lines changed: 3 additions & 317 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import type { ObjectExpression, ObjectMethod, ObjectProperty } from '@babel/types'
2-
import generate from '@babel/generator'
3-
import { parse } from '@babel/parser'
4-
import * as t from '@babel/types'
51
import fs from 'fs-extra'
62
import path from 'pathe'
73
import { pkgName, pkgVersion } from '../constants'
@@ -17,18 +13,11 @@ import {
1713
MIGRATION_REPORT_KIND,
1814
MIGRATION_REPORT_SCHEMA_VERSION,
1915
} from './migration-report'
16+
import { migrateConfigSource } from './migration-source'
2017
export { DEFAULT_CONFIG_FILENAMES } from './migration-target-files'
2118
export { MIGRATION_REPORT_KIND, MIGRATION_REPORT_SCHEMA_VERSION } from './migration-report'
22-
23-
const ROOT_LEGACY_KEYS = ['cwd', 'overwrite', 'tailwind', 'features', 'output', 'applyPatches'] as const
24-
25-
type OptionObjectScope = 'root' | 'registry' | 'patch'
26-
27-
export interface ConfigSourceMigrationResult {
28-
changed: boolean
29-
code: string
30-
changes: string[]
31-
}
19+
export { migrateConfigSource } from './migration-source'
20+
export type { ConfigSourceMigrationResult } from './migration-source'
3221

3322
export interface ConfigFileMigrationEntry {
3423
file: string
@@ -94,309 +83,6 @@ export interface RestoreConfigFilesResult {
9483
restored: string[]
9584
}
9685

97-
function getPropertyKeyName(property: ObjectProperty | ObjectMethod): string | undefined {
98-
if (!property.computed && t.isIdentifier(property.key)) {
99-
return property.key.name
100-
}
101-
if (t.isStringLiteral(property.key)) {
102-
return property.key.value
103-
}
104-
return undefined
105-
}
106-
107-
function findObjectProperty(objectExpression: ObjectExpression, name: string): ObjectProperty | undefined {
108-
for (const property of objectExpression.properties) {
109-
if (!t.isObjectProperty(property)) {
110-
continue
111-
}
112-
if (getPropertyKeyName(property) === name) {
113-
return property
114-
}
115-
}
116-
return undefined
117-
}
118-
119-
function findObjectExpressionProperty(objectExpression: ObjectExpression, name: string): ObjectExpression | undefined {
120-
const property = findObjectProperty(objectExpression, name)
121-
if (!property) {
122-
return undefined
123-
}
124-
if (t.isObjectExpression(property.value)) {
125-
return property.value
126-
}
127-
return undefined
128-
}
129-
130-
function removeObjectProperty(objectExpression: ObjectExpression, property: ObjectProperty) {
131-
const index = objectExpression.properties.indexOf(property)
132-
if (index >= 0) {
133-
objectExpression.properties.splice(index, 1)
134-
}
135-
}
136-
137-
function hasObjectProperty(objectExpression: ObjectExpression, name: string) {
138-
return findObjectProperty(objectExpression, name) !== undefined
139-
}
140-
141-
function keyAsIdentifier(name: string) {
142-
return t.identifier(name)
143-
}
144-
145-
function mergeObjectProperties(target: ObjectExpression, source: ObjectExpression) {
146-
let changed = false
147-
for (const sourceProperty of source.properties) {
148-
if (t.isSpreadElement(sourceProperty)) {
149-
target.properties.push(sourceProperty)
150-
changed = true
151-
continue
152-
}
153-
const sourceKey = getPropertyKeyName(sourceProperty)
154-
if (!sourceKey) {
155-
target.properties.push(sourceProperty)
156-
changed = true
157-
continue
158-
}
159-
if (hasObjectProperty(target, sourceKey)) {
160-
continue
161-
}
162-
target.properties.push(sourceProperty)
163-
changed = true
164-
}
165-
return changed
166-
}
167-
168-
function moveProperty(
169-
objectExpression: ObjectExpression,
170-
from: string,
171-
to: string,
172-
changes: Set<string>,
173-
scope: OptionObjectScope,
174-
) {
175-
const source = findObjectProperty(objectExpression, from)
176-
if (!source) {
177-
return false
178-
}
179-
const target = findObjectProperty(objectExpression, to)
180-
if (!target) {
181-
source.key = keyAsIdentifier(to)
182-
source.computed = false
183-
source.shorthand = false
184-
changes.add(`${scope}.${from} -> ${scope}.${to}`)
185-
return true
186-
}
187-
188-
if (t.isObjectExpression(source.value) && t.isObjectExpression(target.value)) {
189-
const merged = mergeObjectProperties(target.value, source.value)
190-
if (merged) {
191-
changes.add(`${scope}.${from} merged into ${scope}.${to}`)
192-
}
193-
}
194-
removeObjectProperty(objectExpression, source)
195-
changes.add(`${scope}.${from} removed (preferred ${scope}.${to})`)
196-
return true
197-
}
198-
199-
function migrateExtractOptions(extract: ObjectExpression, changes: Set<string>, scope: OptionObjectScope) {
200-
let changed = false
201-
changed = moveProperty(extract, 'enabled', 'write', changes, scope) || changed
202-
changed = moveProperty(extract, 'stripUniversalSelector', 'removeUniversalSelector', changes, scope) || changed
203-
return changed
204-
}
205-
206-
function migrateTailwindOptions(tailwindcss: ObjectExpression, changes: Set<string>, scope: OptionObjectScope) {
207-
let changed = false
208-
changed = moveProperty(tailwindcss, 'package', 'packageName', changes, scope) || changed
209-
changed = moveProperty(tailwindcss, 'legacy', 'v2', changes, scope) || changed
210-
changed = moveProperty(tailwindcss, 'classic', 'v3', changes, scope) || changed
211-
changed = moveProperty(tailwindcss, 'next', 'v4', changes, scope) || changed
212-
return changed
213-
}
214-
215-
function migrateApplyOptions(apply: ObjectExpression, changes: Set<string>, scope: OptionObjectScope) {
216-
return moveProperty(apply, 'exportContext', 'exposeContext', changes, scope)
217-
}
218-
219-
function ensureObjectExpressionProperty(
220-
objectExpression: ObjectExpression,
221-
name: string,
222-
changes: Set<string>,
223-
scope: OptionObjectScope,
224-
) {
225-
const existing = findObjectProperty(objectExpression, name)
226-
if (existing) {
227-
return t.isObjectExpression(existing.value) ? existing.value : undefined
228-
}
229-
const value = t.objectExpression([])
230-
objectExpression.properties.push(t.objectProperty(keyAsIdentifier(name), value))
231-
changes.add(`${scope}.${name} created`)
232-
return value
233-
}
234-
235-
function moveOverwriteToApply(objectExpression: ObjectExpression, changes: Set<string>, scope: OptionObjectScope) {
236-
const overwrite = findObjectProperty(objectExpression, 'overwrite')
237-
if (!overwrite) {
238-
return false
239-
}
240-
const apply = ensureObjectExpressionProperty(objectExpression, 'apply', changes, scope)
241-
if (!apply) {
242-
return false
243-
}
244-
const hasApplyOverwrite = hasObjectProperty(apply, 'overwrite')
245-
if (!hasApplyOverwrite) {
246-
apply.properties.push(
247-
t.objectProperty(keyAsIdentifier('overwrite'), overwrite.value),
248-
)
249-
changes.add(`${scope}.overwrite -> ${scope}.apply.overwrite`)
250-
}
251-
removeObjectProperty(objectExpression, overwrite)
252-
return true
253-
}
254-
255-
function hasAnyRootLegacyKeys(objectExpression: ObjectExpression) {
256-
return ROOT_LEGACY_KEYS.some(key => hasObjectProperty(objectExpression, key))
257-
}
258-
259-
function migrateOptionObject(objectExpression: ObjectExpression, scope: OptionObjectScope, changes: Set<string>) {
260-
let changed = false
261-
changed = moveProperty(objectExpression, 'cwd', 'projectRoot', changes, scope) || changed
262-
changed = moveProperty(objectExpression, 'tailwind', 'tailwindcss', changes, scope) || changed
263-
changed = moveProperty(objectExpression, 'features', 'apply', changes, scope) || changed
264-
changed = moveProperty(objectExpression, 'applyPatches', 'apply', changes, scope) || changed
265-
changed = moveProperty(objectExpression, 'output', 'extract', changes, scope) || changed
266-
changed = moveOverwriteToApply(objectExpression, changes, scope) || changed
267-
268-
const extract = findObjectExpressionProperty(objectExpression, 'extract')
269-
if (extract) {
270-
changed = migrateExtractOptions(extract, changes, scope) || changed
271-
}
272-
const tailwindcss = findObjectExpressionProperty(objectExpression, 'tailwindcss')
273-
if (tailwindcss) {
274-
changed = migrateTailwindOptions(tailwindcss, changes, scope) || changed
275-
}
276-
const apply = findObjectExpressionProperty(objectExpression, 'apply')
277-
if (apply) {
278-
changed = migrateApplyOptions(apply, changes, scope) || changed
279-
}
280-
281-
return changed
282-
}
283-
284-
function unwrapExpression(node: t.Node): t.Node {
285-
let current = node
286-
while (
287-
t.isTSAsExpression(current)
288-
|| t.isTSSatisfiesExpression(current)
289-
|| t.isTSTypeAssertion(current)
290-
|| t.isParenthesizedExpression(current)
291-
) {
292-
current = current.expression
293-
}
294-
return current
295-
}
296-
297-
function resolveObjectExpressionFromExpression(expression: t.Node): ObjectExpression | undefined {
298-
const unwrapped = unwrapExpression(expression)
299-
if (t.isObjectExpression(unwrapped)) {
300-
return unwrapped
301-
}
302-
if (t.isCallExpression(unwrapped)) {
303-
const [firstArg] = unwrapped.arguments
304-
if (!firstArg || !t.isExpression(firstArg)) {
305-
return undefined
306-
}
307-
const firstArgUnwrapped = unwrapExpression(firstArg)
308-
if (t.isObjectExpression(firstArgUnwrapped)) {
309-
return firstArgUnwrapped
310-
}
311-
}
312-
return undefined
313-
}
314-
315-
function resolveObjectExpressionFromProgram(program: t.Program, name: string): ObjectExpression | undefined {
316-
for (const statement of program.body) {
317-
if (!t.isVariableDeclaration(statement)) {
318-
continue
319-
}
320-
for (const declaration of statement.declarations) {
321-
if (!t.isIdentifier(declaration.id) || declaration.id.name !== name || !declaration.init) {
322-
continue
323-
}
324-
const objectExpression = resolveObjectExpressionFromExpression(declaration.init)
325-
if (objectExpression) {
326-
return objectExpression
327-
}
328-
}
329-
}
330-
return undefined
331-
}
332-
333-
function resolveRootConfigObjectExpression(program: t.Program): ObjectExpression | undefined {
334-
for (const statement of program.body) {
335-
if (!t.isExportDefaultDeclaration(statement)) {
336-
continue
337-
}
338-
const declaration = statement.declaration
339-
if (t.isIdentifier(declaration)) {
340-
return resolveObjectExpressionFromProgram(program, declaration.name)
341-
}
342-
const objectExpression = resolveObjectExpressionFromExpression(declaration)
343-
if (objectExpression) {
344-
return objectExpression
345-
}
346-
}
347-
return undefined
348-
}
349-
350-
export function migrateConfigSource(source: string): ConfigSourceMigrationResult {
351-
const ast = parse(source, {
352-
sourceType: 'module',
353-
plugins: ['typescript', 'jsx'],
354-
})
355-
const root = resolveRootConfigObjectExpression(ast.program)
356-
if (!root) {
357-
return {
358-
changed: false,
359-
code: source,
360-
changes: [],
361-
}
362-
}
363-
364-
const changes = new Set<string>()
365-
let changed = false
366-
367-
const registry = findObjectExpressionProperty(root, 'registry')
368-
if (registry) {
369-
changed = migrateOptionObject(registry, 'registry', changes) || changed
370-
}
371-
372-
const patch = findObjectExpressionProperty(root, 'patch')
373-
if (patch) {
374-
changed = migrateOptionObject(patch, 'patch', changes) || changed
375-
}
376-
377-
if (hasAnyRootLegacyKeys(root)) {
378-
changed = migrateOptionObject(root, 'root', changes) || changed
379-
}
380-
381-
if (!changed) {
382-
return {
383-
changed: false,
384-
code: source,
385-
changes: [],
386-
}
387-
}
388-
389-
const generated = generate(ast, {
390-
comments: true,
391-
}).code
392-
const code = source.endsWith('\n') ? `${generated}\n` : generated
393-
return {
394-
changed: true,
395-
code,
396-
changes: [...changes],
397-
}
398-
}
399-
40086
export async function migrateConfigFiles(options: MigrateConfigFilesOptions): Promise<ConfigFileMigrationReport> {
40187
const cwd = path.resolve(options.cwd)
40288
const dryRun = options.dryRun ?? false

0 commit comments

Comments
 (0)