', '!visible'],
+
+ // Alpine/Livewire wire:…
+ ['
', 'blur'],
+
+ // Vue 3 events
+ [`emit('blur', props.modelValue)\n`, 'blur'],
+ [`$emit('blur', props.modelValue)\n`, 'blur'],
+
+ // JavaScript / TypeScript
+ [`document.addEventListener('blur',handleBlur)`, 'blur'],
+ [`document.addEventListener('blur', handleBlur)`, 'blur'],
+
+ [`function foo({ outline = true })`, 'outline'],
+ [`function foo({ before = false, outline = true })`, 'outline'],
+ [`function foo({before=false,outline=true })`, 'outline'],
+ [`function foo({outline=true })`, 'outline'],
+ // https://github.com/tailwindlabs/tailwindcss/issues/18675
+ [
+ // With default value
+ `function foo({ size = "1.25rem", digit, outline = true, textClass = "", className = "" })`,
+ 'outline',
+ ],
+ [
+ // Without default value
+ `function foo({ size = "1.25rem", digit, outline, textClass = "", className = "" })`,
+ 'outline',
+ ],
+ [
+ // As the last argument
+ `function foo({ size = "1.25rem", digit, outline })`,
+ 'outline',
+ ],
+ [
+ // As the last argument, but there is techinically another `"` on the same line
+ `function foo({ size = "1.25rem", digit, outline }): { return "foo" }`,
+ 'outline',
+ ],
+ [
+ // Tricky quote balancing
+ `function foo({ before = "'", outline, after = "'" }): { return "foo" }`,
+ 'outline',
+ ],
+
+ [`function foo(blur, foo)`, 'blur'],
+ [`function foo(blur,foo)`, 'blur'],
+ ])('does not replace classes in invalid positions #%#', async (example, candidate) => {
expect(
await migrateCandidate(designSystem, {}, candidate, {
contents: example,
@@ -17,52 +99,5 @@ test('does not replace classes in invalid positions', async () => {
end: example.indexOf(candidate) + candidate.length,
}),
).toEqual(candidate)
- }
-
- await shouldNotReplace(`let notBorder = !border \n`)
- await shouldNotReplace(`{ "foo": !border.something + ""}\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
- await shouldNotReplace(`
\n`)
-
- await shouldNotReplace(`let notShadow = shadow \n`, 'shadow')
- await shouldNotReplace(`{ "foo": shadow.something + ""}\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(`
\n`, 'shadow')
- await shouldNotReplace(
- `
\n`,
- 'shadow',
- )
-
- // Next.js Image placeholder cases
- await shouldNotReplace(`
`, 'blur')
- await shouldNotReplace(`
`, 'blur')
- await shouldNotReplace(`
`, 'blur')
-
- // https://github.com/tailwindlabs/tailwindcss/issues/17974
- await shouldNotReplace('
', '!duration')
- await shouldNotReplace('
', '!duration')
- await shouldNotReplace('
', '!visible')
-
- // Alpine/Livewire wire:…
- await shouldNotReplace('', 'blur')
-
- // Vue 3 events
- await shouldNotReplace(`emit('blur', props.modelValue)\n`, 'blur')
- await shouldNotReplace(`$emit('blur', props.modelValue)\n`, 'blur')
+ })
})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
index 7ac24d645fb3..e4ec86b2802c 100644
--- a/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
+++ b/packages/@tailwindcss-upgrade/src/codemods/template/is-safe-migration.ts
@@ -3,7 +3,6 @@ import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import * as version from '../../utils/version'
-const QUOTES = ['"', "'", '`']
const LOGICAL_OPERATORS = ['&&', '||', '?', '===', '==', '!=', '!==', '>', '>=', '<', '<=']
const CONDITIONAL_TEMPLATE_SYNTAX = [
// Vue
@@ -12,13 +11,16 @@ const CONDITIONAL_TEMPLATE_SYNTAX = [
/v-show=['"]$/,
/(? currentLineBeforeCandidate.includes(quote))
- let isQuoteAfterCandidate = QUOTES.some((quote) => currentLineAfterCandidate.includes(quote))
+ let isQuoteBeforeCandidate = isMiddleOfString(currentLineBeforeCandidate)
+ let isQuoteAfterCandidate = isMiddleOfString(currentLineAfterCandidate)
if (!isQuoteBeforeCandidate || !isQuoteAfterCandidate) {
return false
}
@@ -210,3 +212,38 @@ const styleBlockRanges = new DefaultMap((source: string) => {
ranges.push(startTag, endTag)
}
})
+
+const BACKSLASH = 0x5c
+const DOUBLE_QUOTE = 0x22
+const SINGLE_QUOTE = 0x27
+const BACKTICK = 0x60
+
+function isMiddleOfString(line: string): boolean {
+ let currentQuote: number | null = null
+
+ for (let i = 0; i < line.length; i++) {
+ let char = line.charCodeAt(i)
+ switch (char) {
+ // Escaped character, skip the next character
+ case BACKSLASH:
+ i++
+ break
+
+ case SINGLE_QUOTE:
+ case DOUBLE_QUOTE:
+ case BACKTICK:
+ // Found matching quote, we are outside of a string
+ if (currentQuote === char) {
+ currentQuote = null
+ }
+
+ // Found a quote, we are inside a string
+ else if (currentQuote === null) {
+ currentQuote = char
+ }
+ break
+ }
+ }
+
+ return currentQuote !== null
+}