Upgrade to version 11.0.1 #1

Merged
steve merged 60 commits from v11.0.1 into main 2025-10-30 22:14:46 +00:00
2 changed files with 206 additions and 124 deletions
Showing only changes of commit 464e977d5a - Show all commits

View File

@@ -37,6 +37,24 @@ jobs:
npm run build npm run build
cat public/build/manifest.json cat public/build/manifest.json
- name: Validate theme.json
run: |
THEME_JSON="public/build/assets/theme.json"
if [ ! -f "$THEME_JSON" ]; then
echo "❌ theme.json not found"
exit 1
fi
jq -e '
(.settings.color.palette | map(select(.name == "black")) | length > 0) and
(.settings.typography.fontFamilies | map(select(.name == "sans")) | length > 0) and
(.settings.typography.fontSizes | map(select(.name == "xl")) | length > 0)
' "$THEME_JSON" 2>&1 || {
echo "❌ Invalid theme.json structure or missing required values"
exit 1
}
php: php:
name: PHP ${{ matrix.php }} name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -163,20 +163,28 @@ export function wordpressRollupPlugin() {
} }
/** /**
* Generates a WordPress theme.json file by combining: * Vite plugin that generates a WordPress theme.json file based on Tailwind v4 CSS variables.
* - Base theme.json settings * This allows theme.json settings to stay in sync with your Tailwind design tokens.
* - Tailwind configuration (colors, fonts, font sizes) *
* * CSS variables defined in an @theme block will be transformed into theme.json format:
* The generated theme.json is emitted to public/build/assets/theme.json *
* and provides WordPress with theme settings that match your Tailwind configuration. * @example
* * ```css
* @param {Object} options Plugin options * @theme {
* @param {Object} options.tailwindConfig - The resolved Tailwind configuration object * --color-primary: #000000; -> { name: "primary", color: "#000000" }
* @param {boolean} [options.disableTailwindColors=false] - Disable including Tailwind colors in theme.json * --color-red-500: #ef4444; -> { name: "red-500", color: "#ef4444" }
* @param {boolean} [options.disableTailwindFonts=false] - Disable including Tailwind fonts in theme.json * --font-inter: "Inter"; -> { name: "inter", fontFamily: "Inter" }
* @param {boolean} [options.disableTailwindFontSizes=false] - Disable including Tailwind font sizes in theme.json * --text-lg: 1.125rem; -> { name: "lg", size: "1.125rem" }
* @returns {import('vite').Plugin} Vite plugin * }
*/ * ```
*
* @param {Object} options Plugin options
* @param {Object} options.tailwindConfig - The resolved Tailwind configuration object
* @param {boolean} [options.disableTailwindColors=false] - Disable including Tailwind colors in theme.json
* @param {boolean} [options.disableTailwindFonts=false] - Disable including Tailwind fonts in theme.json
* @param {boolean} [options.disableTailwindFontSizes=false] - Disable including Tailwind font sizes in theme.json
* @returns {import('vite').Plugin} Vite plugin
*/
export function wordpressThemeJson({ export function wordpressThemeJson({
tailwindConfig, tailwindConfig,
disableTailwindColors = false, disableTailwindColors = false,
@@ -185,6 +193,74 @@ export function wordpressThemeJson({
}) { }) {
let cssContent = null let cssContent = null
/**
* Safely extracts content between matched braces, handling:
* - Nested braces
* - String literals (both single and double quotes)
* - CSS comments
* - Escaped characters
*/
function extractThemeContent(css) {
const themeMatch = css.match(/@(?:layer\s+)?theme\s*{/s)
if (!themeMatch) {
return null // No @theme block - that's fine
}
const startIndex = themeMatch.index + themeMatch[0].length
let braceCount = 1
for (let i = startIndex; i < css.length; i++) {
// Skip escaped characters
if (css[i] === '\\') {
i++
continue
}
// Skip string literals
if (css[i] === '"' || css[i] === "'") {
const quote = css[i]
i++
while (i < css.length) {
if (css[i] === '\\') {
i++
} else if (css[i] === quote) {
break
}
i++
}
if (i >= css.length) {
throw new Error('Unclosed string literal in CSS')
}
continue
}
// Skip CSS comments
if (css[i] === '/' && css[i + 1] === '*') {
i += 2
while (i < css.length) {
if (css[i] === '*' && css[i + 1] === '/') {
i++
break
}
i++
}
if (i >= css.length) {
throw new Error('Unclosed comment in CSS')
}
continue
}
if (css[i] === '{') braceCount++
if (css[i] === '}') braceCount--
if (braceCount === 0) {
return css.substring(startIndex, i)
}
}
throw new Error('Unclosed @theme block - missing closing brace')
}
return { return {
name: 'wordpress-theme-json', name: 'wordpress-theme-json',
enforce: 'post', enforce: 'post',
@@ -198,63 +274,49 @@ export function wordpressThemeJson({
async generateBundle() { async generateBundle() {
if (!cssContent) { if (!cssContent) {
return; return // No CSS file to process
} }
const baseThemeJson = JSON.parse( try {
fs.readFileSync(path.resolve('./theme.json'), 'utf8') const baseThemeJson = JSON.parse(
) fs.readFileSync(path.resolve('./theme.json'), 'utf8')
)
const themeContent = (() => { const themeContent = extractThemeContent(cssContent)
const match = cssContent.match(/@(?:layer\s+)?theme\s*{/s) if (!themeContent) {
return // No @theme block to process
if (!match[0]) {
return null
} }
const startIndex = match.index + match[0].length;
let braceCount = 1; // Process any CSS variables in whatever format they exist
for (let i = startIndex; i < cssContent.length; i++) { const colorVariables = {}
if (cssContent[i] === "{") braceCount++; const colorVarRegex = /--color-([^:]+):\s*([^;}]+)[;}]?/g
if (cssContent[i] === "}") braceCount--; let match
if (braceCount === 0) {
return cssContent.substring(startIndex, i ); while ((match = colorVarRegex.exec(themeContent)) !== null) {
} const [, name, value] = match
colorVariables[name] = value.trim()
} }
return null
})()
if (!themeContent) {
return;
}
if (!themeContent.trim().startsWith(':root')) {
return;
}
const endIndex = themeContent.lastIndexOf('}'); // Transform colors to theme.json format
const rootContent = themeContent.slice(themeContent.indexOf('{') + 1, endIndex === -1 ? undefined : endIndex); const colors = []
const colorVariables = {} Object.entries(colorVariables).forEach(([name, value]) => {
if (name.endsWith('-*')) return
const colorVarRegex = /--color-([^:]+):\s*([^;}]+)[;}]?/g if (name.includes('-')) {
let match const [colorName, shade] = name.split('-')
if (shade && !isNaN(shade)) {
while ((match = colorVarRegex.exec(rootContent)) !== null) { colors.push({
const [, name, value] = match name: `${colorName}-${shade}`,
colorVariables[name] = value.trim() slug: `${colorName}-${shade}`.toLowerCase(),
} color: value,
})
const colors = [] } else {
Object.entries(colorVariables).forEach(([name, value]) => { colors.push({
if (name.endsWith('-*')) return name: name,
slug: name.toLowerCase(),
if (name.includes('-')) { color: value,
const [colorName, shade] = name.split('-') })
if (shade && !isNaN(shade)) { }
colors.push({
name: `${colorName}-${shade}`,
slug: `${colorName}-${shade}`.toLowerCase(),
color: value,
})
} else { } else {
colors.push({ colors.push({
name: name, name: name,
@@ -262,74 +324,76 @@ export function wordpressThemeJson({
color: value, color: value,
}) })
} }
} else { })
colors.push({
name: name,
slug: name.toLowerCase(),
color: value,
})
}
})
const fontFamilies = [] // Process any font families
const fontVarRegex = /--font-([^:]+):\s*([^;}]+)[;}]?/g const fontFamilies = []
while ((match = fontVarRegex.exec(rootContent)) !== null) { const fontVarRegex = /--font-([^:]+):\s*([^;}]+)[;}]?/g
const [, name, value] = match while ((match = fontVarRegex.exec(themeContent)) !== null) {
if (!name.includes('-feature-settings') && !name.includes('-variation-settings')) { const [, name, value] = match
fontFamilies.push({ // Skip feature settings, variation settings, and any font-* properties
name: name, if (!name.includes('feature-settings') &&
slug: name.toLowerCase(), !name.includes('variation-settings') &&
fontFamily: value.trim(), !['family', 'size', 'smoothing', 'style', 'weight', 'stretch']
}) .some(prop => name.includes(prop))) {
fontFamilies.push({
name: name,
slug: name.toLowerCase(),
fontFamily: value.trim(),
})
}
} }
}
const fontSizes = [] // Process any font sizes
const fontSizeVarRegex = /--text-([^:]+):\s*([^;}]+)[;}]?/g const fontSizes = []
while ((match = fontSizeVarRegex.exec(rootContent)) !== null) { const fontSizeVarRegex = /--text-([^:]+):\s*([^;}]+)[;}]?/g
const [, name, value] = match while ((match = fontSizeVarRegex.exec(themeContent)) !== null) {
if (!name.includes('--line-height')) { const [, name, value] = match
fontSizes.push({ // Skip line-height entries
name: name, if (!name.includes('line-height')) {
slug: name.toLowerCase(), fontSizes.push({
size: value.trim(), name: name,
}) slug: name.toLowerCase(),
size: value.trim(),
})
}
} }
}
const themeJson = { // Build theme.json with whatever variables were found
__processed__: "This file was generated from Tailwind v4 CSS variables", const themeJson = {
...baseThemeJson, __processed__: "This file was generated from Tailwind v4 CSS variables",
settings: { ...baseThemeJson,
...baseThemeJson.settings, settings: {
...((!disableTailwindColors && colors.length > 0) && { ...baseThemeJson.settings,
color: { ...((!disableTailwindColors && colors.length > 0) && {
...baseThemeJson.settings?.color, color: {
palette: colors, ...baseThemeJson.settings?.color,
}, palette: colors,
}), },
...((!disableTailwindFonts && fontFamilies.length > 0) && { }),
typography: { typography: {
...baseThemeJson.settings?.typography, defaultFontSizes: false,
fontFamilies, customFontSize: false,
...((!disableTailwindFonts && fontFamilies.length > 0) && {
fontFamilies,
}),
...(!disableTailwindFontSizes && fontSizes.length > 0 && {
fontSizes,
}),
}, },
}), },
...((!disableTailwindFontSizes && fontSizes.length > 0) && { }
typography: {
...baseThemeJson.settings?.typography, delete themeJson.__preprocessed__
fontSizes,
}, this.emitFile({
}), type: 'asset',
}, fileName: 'assets/theme.json',
source: JSON.stringify(themeJson, null, 2)
})
} catch (error) {
this.error(error.message)
} }
delete themeJson.__preprocessed__
this.emitFile({
type: 'asset',
fileName: 'assets/theme.json',
source: JSON.stringify(themeJson, null, 2)
})
}, },
} }
} }