Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/oxide/src/scanner/fixtures/ignored-files.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ package-lock.json
pnpm-lock.yaml
bun.lockb
.gitignore
.env
.env.*
232 changes: 231 additions & 1 deletion integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path'
import { isRepoDirty } from '../../packages/@tailwindcss-upgrade/src/utils/git'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
import { candidate, css, html, js, json, test, ts, txt, yaml } from '../utils'

test(
'error when no CSS file with @tailwind is used',
Expand Down Expand Up @@ -171,6 +171,56 @@ test(
},
)

test(
'only migrates files matched by `config.content` when upgrading from v3 to v4',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^3",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.html'],
}
`,
'src/index.html': html`
<div class="order-[0] bg-[--my-red]"></div>
`,
'src/input.css': css`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
'templates/email.php': html`
<div class="order-[0] bg-[--my-red]"></div>
`,
'notes/unrelated.txt': `order-[0] bg-[--my-red]`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('./**/*.{html,php,txt}')).toMatchInlineSnapshot(`
"
--- notes/unrelated.txt ---
order-[0] bg-[--my-red]

--- src/index.html ---
<div class="order-0 bg-(--my-red)"></div>

--- templates/email.php ---
<div class="order-[0] bg-[--my-red]"></div>
"
`)
},
)

test(
`upgrades a v3 project with prefixes to v4`,
{
Expand Down Expand Up @@ -2972,6 +3022,186 @@ test(
},
)

test(
'v4 ignores .env files during template migration',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/app.css': css`@import 'tailwindcss';`,
'src/index.html': html`
<div class="order-[0]"></div>
`,
'src/.env': `TW_TEST_CLASS=order-[0]`,
'src/.env.production': `TW_TEST_CLASS=order-[0]`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('./src/**/{*,.env,.env.*}')).toMatchInlineSnapshot(`
"
--- ./src/index.html ---
<div class="order-0"></div>

--- ./src/.env ---
TW_TEST_CLASS=order-[0]

--- ./src/.env.production ---
TW_TEST_CLASS=order-[0]

--- ./src/app.css ---
@import 'tailwindcss';
"
`)
},
)

test(
'v4 linked configs respect `content` and still ignore gitignored files',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'tailwind.config.js': js`
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/migrate-me.html'],
}
`,
'src/app.css': css`
@import 'tailwindcss';
@config '../tailwind.config.js';
`,

// Should be ignored by .gitignore, but should explicitly be included
// because of the `content` array in the `tailwind.config.js` file.
'src/migrate-me.html': html`
<div class="order-[0]"></div>
`,
// Should be ignored by .gitignore
'src/do-not-migrate-me.html': html`
<div class="order-[0]"></div>
`,
'src/.gitignore': txt`
*.html
`,

// Should be picked up by auto-content detection
'templates/migrate-me.php': html`
<div class="order-[0]"></div>
`,
},
},
async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')

expect(await fs.dumpFiles('./{src,templates}/**/*.{css,html,php}')).toMatchInlineSnapshot(`
"
--- ./src/app.css ---
@import 'tailwindcss';
@config '../tailwind.config.js';

--- ./src/do-not-migrate-me.html ---
<div class="order-[0]"></div>

--- ./src/migrate-me.html ---
<div class="order-0"></div>

--- ./templates/migrate-me.php ---
<div class="order-0"></div>
"
`)
},
)

test(
'interrupting template migration does not truncate files',
{
timeout: 180_000,
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "^4",
"@tailwindcss/upgrade": "workspace:^"
}
}
`,
'src/app.css': css` @import 'tailwindcss'; `,
'src/index.html': html`
<div class="order-[0]"></div>
`,
'hook.cjs': js`
let fs = require('node:fs/promises')
let path = require('node:path')
let originalWriteFile = fs.writeFile.bind(fs)

fs.writeFile = async (file, contents, ...rest) => {
// Only mimic a bad write, when writing to template files
if (!file.includes('src/templates')) return originalWriteFile(file, contents, ...rest)

// Mimic a bad write
await originalWriteFile(file, '') // As-if we truncated first
console.error('__TRUNCATED_TARGET__')
await new Promise((r) => setTimeout(r, 150)) // Wait 150ms
await originalWriteFile(file, contents, ...rest) // Write the actual contents
}
`,
'src/keep.php': `
<?php

return [
'keep' => 'this file should never be truncated',
];
`,
},
},
async ({ spawn, fs, expect }) => {
let repeatedCandidates = Array.from(
{ length: 250 },
() => '<div class="order-[0]"></div>',
).join('\n')

for (let i = 0; i < 100; i++) {
await fs.write(
`src/templates/template-${i}.php`,
`<?php\n\n${repeatedCandidates}\n\nreturn ['template' => ${i}];\n`,
)
}

let originalKeepFile = await fs.read('src/keep.php')
let originalTemplate = await fs.read('src/templates/template-0.php')

let process = await spawn('npx @tailwindcss/upgrade --force', {
env: {
NODE_OPTIONS: '--require=./hook.cjs',
},
})
await process.onStderr((message) => message === '__TRUNCATED_TARGET__')
await process.kill()

expect(await fs.read('src/keep.php')).toBe(originalKeepFile)
expect(await fs.read('src/templates/template-0.php')).toBe(originalTemplate)
Comment thread
RobinMalfait marked this conversation as resolved.

for (let [file, contents] of await fs.glob('src/**/*.{html,php,css}')) {
expect(contents.trim(), `${file} should not be empty after interruption`).not.toBe('')
}
},
)

test(
'upgrades can run in a pnpm workspace',
{
Expand Down
19 changes: 18 additions & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((na
)

interface SpawnedProcess {
dispose: () => void
dispose: () => Promise<void>
kill: () => Promise<void>
flush: () => void
onStdout: (predicate: (message: string) => boolean) => Promise<void>
onStderr: (predicate: (message: string) => boolean) => Promise<void>
Expand Down Expand Up @@ -194,6 +195,21 @@ export function test(
return disposePromise
}
disposables.push(dispose)

function kill() {
child.kill('SIGKILL')
let timer = setTimeout(
() =>
rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)),
ASSERTION_TIMEOUT,
)
disposePromise.finally(() => {
clearTimeout(timer)
})
return disposePromise
}
disposables.push(kill)

function onExit() {
resolveDisposal?.()
}
Expand Down Expand Up @@ -262,6 +278,7 @@ export function test(

return {
dispose,
kill,
flush() {
stdoutActors.splice(0)
stderrActors.splice(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { pkg } from '../../utils/packages'
import { highlight, info, relative, success, warn } from '../../utils/renderer'
import { writeFileSafely } from '../../utils/write-file-safely'

// Migrates simple PostCSS setups. This is to cover non-dynamic config files
// similar to the ones we have all over our docs:
Expand Down Expand Up @@ -50,7 +51,7 @@ export async function migratePostCSSConfig(base: string) {
ranMigration = true

if (result) {
await fs.writeFile(
await writeFileSafely(
packageJsonPath,
JSON.stringify({ ...packageJson, postcss: result?.json }, null, 2),
)
Expand All @@ -76,7 +77,7 @@ export async function migratePostCSSConfig(base: string) {
ranMigration = true

if (result) {
await fs.writeFile(jsonConfigPath, JSON.stringify(result.json, null, 2))
await writeFileSafely(jsonConfigPath, JSON.stringify(result.json, null, 2))

didMigrate = true
didAddPostcssClient = result.didAddPostcssClient
Expand Down Expand Up @@ -206,7 +207,7 @@ async function migratePostCSSJSConfig(configPath: string): Promise<{
newLines.push(line)
}
}
await fs.writeFile(configPath, newLines.join('\n'))
await writeFileSafely(configPath, newLines.join('\n'))

return { didAddPostcssClient, didRemoveAutoprefixer, didRemovePostCSSImport }
}
Expand Down
10 changes: 6 additions & 4 deletions packages/@tailwindcss-upgrade/src/codemods/template/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import { spliceChangesIntoString, type StringChange } from '../../utils/splice-changes-into-string'
import { writeFileSafely } from '../../utils/write-file-safely'
import { extractRawCandidates } from './candidates'
import { isSafeMigration } from './is-safe-migration'
import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection'
Expand Down Expand Up @@ -123,8 +124,9 @@ export async function migrate(designSystem: DesignSystem, userConfig: Config | n
let fullPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file)
let contents = await fs.readFile(fullPath, 'utf-8')

await fs.writeFile(
fullPath,
await migrateContents(designSystem, userConfig, contents, extname(file)),
)
let migrated = await migrateContents(designSystem, userConfig, contents, extname(file))
if (migrated === contents) return // Nothing changed
if (migrated.trim() === '') return // Emptied out, something went horribly wrong

await writeFileSafely(fullPath, migrated)
}
Loading
Loading