✨ Use @roots/vite-plugin (#3231)
Co-authored-by: Brandon <brandon@tendency.me>
This commit is contained in:
@@ -1,399 +0,0 @@
|
||||
import {
|
||||
defaultRequestToExternal,
|
||||
defaultRequestToHandle,
|
||||
} from '@wordpress/dependency-extraction-webpack-plugin/lib/util'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Vite plugin that handles WordPress dependencies and generates a dependency manifest.
|
||||
*
|
||||
* This plugin:
|
||||
* 1. Transforms @wordpress/* imports into global wp.* references
|
||||
* 2. Tracks WordPress script dependencies
|
||||
* 3. Generates an editor.deps.json file listing all WordPress dependencies
|
||||
*
|
||||
* @returns {import('vite').Plugin} Vite plugin
|
||||
*/
|
||||
export function wordpressPlugin() {
|
||||
const dependencies = new Set()
|
||||
|
||||
// Helper functions for import handling
|
||||
function extractNamedImports(imports) {
|
||||
const match = imports.match(/{([^}]+)}/)
|
||||
if (!match) return []
|
||||
return match[1]
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s !== '')
|
||||
}
|
||||
|
||||
function handleNamedReplacement(namedImports, external) {
|
||||
return namedImports
|
||||
.map((imports) => {
|
||||
const [name, alias = name] = imports
|
||||
.split(' as ')
|
||||
.map((script) => script.trim())
|
||||
return `const ${alias} = ${external.join('.')}.${name};`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
function handleReplacements(imports, external) {
|
||||
const importStr = Array.isArray(imports) ? imports[0] : imports
|
||||
|
||||
if (importStr.includes('{')) {
|
||||
const namedImports = extractNamedImports(importStr)
|
||||
return handleNamedReplacement(namedImports, external)
|
||||
}
|
||||
|
||||
if (importStr.includes('* as')) {
|
||||
const match = importStr.match(/\*\s+as\s+(\w+)/)
|
||||
if (!match) return ''
|
||||
const alias = match[1]
|
||||
return `const ${alias} = ${external.join('.')};`
|
||||
}
|
||||
|
||||
const name = importStr.trim()
|
||||
return `const ${name} = ${external.join('.')};`
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'wordpress-plugin',
|
||||
enforce: 'pre',
|
||||
config(config) {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve?.alias,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id.startsWith('@wordpress/')) {
|
||||
const pkg = id.replace('@wordpress/', '')
|
||||
const external = defaultRequestToExternal(id)
|
||||
const handle = defaultRequestToHandle(id)
|
||||
|
||||
if (external && handle) {
|
||||
dependencies.add(handle)
|
||||
return {
|
||||
id,
|
||||
external: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
transform(code, id) {
|
||||
if ((!id.endsWith('.js')) && !id.endsWith('.jsx') && !id.endsWith('.ts') && !id.endsWith('.tsx')) return
|
||||
|
||||
const imports = [
|
||||
...(code.match(/^import[\s\n]+(?:[^;]+?)[\s\n]+from[\s\n]+['"]@wordpress\/[^'"]+['"]/gm) || []),
|
||||
...(code.match(/^import[\s\n]+['"]@wordpress\/[^'"]+['"]/gm) || []),
|
||||
]
|
||||
|
||||
imports.forEach((statement) => {
|
||||
const match =
|
||||
statement
|
||||
.replace(/[\s\n]+/g, ' ')
|
||||
.match(/^import (.+) from ['"]@wordpress\/([^'"]+)['"]/) ||
|
||||
statement.match(/^import ['"]@wordpress\/([^'"]+)['"]/);
|
||||
|
||||
if (!match) return
|
||||
|
||||
const [, imports, pkg] = match
|
||||
if (!pkg) return
|
||||
|
||||
const external = defaultRequestToExternal(`@wordpress/${pkg}`)
|
||||
const handle = defaultRequestToHandle(`@wordpress/${pkg}`)
|
||||
|
||||
if (external && handle) {
|
||||
dependencies.add(handle)
|
||||
const replacement = imports
|
||||
? handleReplacements(imports, external)
|
||||
: `const ${pkg.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())} = ${external.join('.')};`
|
||||
|
||||
code = code.replace(statement, replacement)
|
||||
}
|
||||
})
|
||||
|
||||
return { code, map: null }
|
||||
},
|
||||
generateBundle() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
name: 'editor.deps.json',
|
||||
fileName: 'editor.deps.json',
|
||||
source: JSON.stringify([...dependencies]),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollup plugin that configures external WordPress dependencies.
|
||||
*
|
||||
* This plugin:
|
||||
* 1. Marks all @wordpress/* packages as external dependencies
|
||||
* 2. Maps external @wordpress/* imports to wp.* global variables
|
||||
*
|
||||
* This prevents WordPress core libraries from being bundled and ensures
|
||||
* they are loaded from WordPress's global scope instead.
|
||||
*
|
||||
* @returns {import('rollup').Plugin} Rollup plugin
|
||||
*/
|
||||
export function wordpressRollupPlugin() {
|
||||
return {
|
||||
name: 'wordpress-rollup-plugin',
|
||||
options(opts) {
|
||||
opts.external = (id) => id.startsWith('@wordpress/')
|
||||
opts.output = opts.output || {}
|
||||
opts.output.globals = (id) => {
|
||||
if (id.startsWith('@wordpress/')) {
|
||||
const packageName = id.replace('@wordpress/', '')
|
||||
return `wp.${packageName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}`
|
||||
}
|
||||
}
|
||||
return opts
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin that generates a WordPress theme.json file based on Tailwind v4 CSS variables.
|
||||
* This allows theme.json settings to stay in sync with your Tailwind design tokens.
|
||||
*
|
||||
* CSS variables defined in an @theme block will be transformed into theme.json format:
|
||||
*
|
||||
* @example
|
||||
* ```css
|
||||
* @theme {
|
||||
* --color-primary: #000000; -> { name: "primary", color: "#000000" }
|
||||
* --color-red-500: #ef4444; -> { name: "red-500", color: "#ef4444" }
|
||||
* --font-inter: "Inter"; -> { name: "inter", fontFamily: "Inter" }
|
||||
* --text-lg: 1.125rem; -> { name: "lg", size: "1.125rem" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @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({
|
||||
tailwindConfig,
|
||||
disableTailwindColors = false,
|
||||
disableTailwindFonts = false,
|
||||
disableTailwindFontSizes = false,
|
||||
}) {
|
||||
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 {
|
||||
name: 'wordpress-theme-json',
|
||||
enforce: 'post',
|
||||
|
||||
transform(code, id) {
|
||||
if (id.includes('app.css')) {
|
||||
cssContent = code
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
async generateBundle() {
|
||||
if (!cssContent) {
|
||||
return // No CSS file to process
|
||||
}
|
||||
|
||||
try {
|
||||
const baseThemeJson = JSON.parse(
|
||||
fs.readFileSync(path.resolve('./theme.json'), 'utf8')
|
||||
)
|
||||
|
||||
const themeContent = extractThemeContent(cssContent)
|
||||
if (!themeContent) {
|
||||
return // No @theme block to process
|
||||
}
|
||||
|
||||
// Process any CSS variables in whatever format they exist
|
||||
const colorVariables = {}
|
||||
const colorVarRegex = /--color-([^:]+):\s*([^;}]+)[;}]?/g
|
||||
let match
|
||||
|
||||
while ((match = colorVarRegex.exec(themeContent)) !== null) {
|
||||
const [, name, value] = match
|
||||
colorVariables[name] = value.trim()
|
||||
}
|
||||
|
||||
// Transform colors to theme.json format
|
||||
const colors = []
|
||||
Object.entries(colorVariables).forEach(([name, value]) => {
|
||||
if (name.endsWith('-*')) return
|
||||
|
||||
if (name.includes('-')) {
|
||||
const [colorName, shade] = name.split('-')
|
||||
if (shade && !isNaN(shade)) {
|
||||
colors.push({
|
||||
name: `${colorName}-${shade}`,
|
||||
slug: `${colorName}-${shade}`.toLowerCase(),
|
||||
color: value,
|
||||
})
|
||||
} else {
|
||||
colors.push({
|
||||
name: name,
|
||||
slug: name.toLowerCase(),
|
||||
color: value,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
colors.push({
|
||||
name: name,
|
||||
slug: name.toLowerCase(),
|
||||
color: value,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Process any font families
|
||||
const fontFamilies = []
|
||||
const fontVarRegex = /--font-([^:]+):\s*([^;}]+)[;}]?/g
|
||||
while ((match = fontVarRegex.exec(themeContent)) !== null) {
|
||||
const [, name, value] = match
|
||||
// Skip feature settings, variation settings, and any font-* properties
|
||||
if (!name.includes('feature-settings') &&
|
||||
!name.includes('variation-settings') &&
|
||||
!['family', 'size', 'smoothing', 'style', 'weight', 'stretch']
|
||||
.some(prop => name.includes(prop))) {
|
||||
fontFamilies.push({
|
||||
name: name,
|
||||
slug: name.toLowerCase(),
|
||||
fontFamily: value.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Process any font sizes
|
||||
const fontSizes = []
|
||||
const fontSizeVarRegex = /--text-([^:]+):\s*([^;}]+)[;}]?/g
|
||||
while ((match = fontSizeVarRegex.exec(themeContent)) !== null) {
|
||||
const [, name, value] = match
|
||||
// Skip line-height entries
|
||||
if (!name.includes('line-height')) {
|
||||
fontSizes.push({
|
||||
name: name,
|
||||
slug: name.toLowerCase(),
|
||||
size: value.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Build theme.json with whatever variables were found
|
||||
const themeJson = {
|
||||
__processed__: "This file was generated from Tailwind v4 CSS variables",
|
||||
...baseThemeJson,
|
||||
settings: {
|
||||
...baseThemeJson.settings,
|
||||
...((!disableTailwindColors && colors.length > 0) && {
|
||||
color: {
|
||||
...baseThemeJson.settings?.color,
|
||||
palette: colors,
|
||||
},
|
||||
}),
|
||||
typography: {
|
||||
defaultFontSizes: false,
|
||||
customFontSize: false,
|
||||
...((!disableTailwindFonts && fontFamilies.length > 0) && {
|
||||
fontFamilies,
|
||||
}),
|
||||
...(!disableTailwindFontSizes && fontSizes.length > 0 && {
|
||||
fontSizes,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
delete themeJson.__preprocessed__
|
||||
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'assets/theme.json',
|
||||
source: JSON.stringify(themeJson, null, 2)
|
||||
})
|
||||
} catch (error) {
|
||||
this.error(error.message)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,5 @@
|
||||
import domReady from '@wordpress/dom-ready';
|
||||
|
||||
domReady(() => {
|
||||
// DOM has been loaded
|
||||
//
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', (payload) => {
|
||||
const cssUpdates = payload.updates.filter(update => update.type === 'css-update');
|
||||
|
||||
if (cssUpdates.length > 0) {
|
||||
const update = cssUpdates[0];
|
||||
|
||||
// Find the iframe
|
||||
const editorIframe = document.querySelector('iframe[name="editor-canvas"]');
|
||||
if (!editorIframe?.contentDocument) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the existing style tag in the iframe
|
||||
const styles = editorIframe.contentDocument.getElementsByTagName('style');
|
||||
let editorStyle = null;
|
||||
for (const style of styles) {
|
||||
if (style.textContent.includes('editor.css')) {
|
||||
editorStyle = style;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!editorStyle) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the style content with new import and cache-busting timestamp
|
||||
const timestamp = Date.now();
|
||||
editorStyle.textContent = `@import url('${window.__vite_client_url}${update.path}?t=${timestamp}')`;
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-CSS updates, reload
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user