Skip to content

refactor(proxy): use ohash diff to determine changes instead #985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"defu": "^6.1.4",
"h3": "^1.15.1",
"klona": "^2.0.6",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"postcss": "^8.5.3",
Expand Down
18 changes: 8 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions src/internal-context/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
configUpdatedHook[resolvedConfigFile] += 'cfg.content = cfg.purge;'
}

await nuxt.callHook('tailwindcss:loadConfig', new Proxy(config, trackObjChanges(resolvedConfigFile)), resolvedConfigFile, idx, arr as any)
await nuxt.callHook('tailwindcss:loadConfig', config, resolvedConfigFile, idx, arr as any)
trackObjChanges(resolvedConfigFile, resolvedConfig.config, config)
return { ...resolvedConfig, config }
}).catch((e) => {
logger.warn(`Failed to load config \`./${relative(nuxt.options.rootDir, configFile)}\` due to the error below. Skipping..\n`, e)
Expand Down Expand Up @@ -160,7 +161,8 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
configUpdatedHook[resolvedConfigFile] += 'cfg.content = cfg.purge;'
}

await nuxt.callHook('tailwindcss:loadConfig', new Proxy(config, trackObjChanges(resolvedConfigFile)), resolvedConfigFile, 0, [])
await nuxt.callHook('tailwindcss:loadConfig', config, resolvedConfigFile, 0, [])
trackObjChanges(resolvedConfigFile, resolvedConfig.config, config)
return { ...resolvedConfig, config }
}

Expand All @@ -185,12 +187,14 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
const moduleConfigs = await getModuleConfigs()
resolvedConfigsCtx.set(moduleConfigs, true)
const tailwindConfig = moduleConfigs.reduce((acc, curr) => configMerger(acc, curr?.config ?? {}), {} as Partial<TWConfig>)
const clonedConfig = configMerger(undefined, tailwindConfig)

// Allow extending tailwindcss config by other modules
configUpdatedHook['main-config'] = ''
await nuxt.callHook('tailwindcss:config', new Proxy(tailwindConfig, trackObjChanges('main-config')))
await nuxt.callHook('tailwindcss:config', clonedConfig)
trackObjChanges('main-config', tailwindConfig, clonedConfig)

const resolvedConfig = resolveTWConfig(tailwindConfig)
const resolvedConfig = resolveTWConfig(clonedConfig)
await nuxt.callHook('tailwindcss:resolvedConfig', resolvedConfig as any, twCtx.tryUse()?.config as any ?? undefined)
twCtx.set({ config: resolvedConfig })

Expand Down
69 changes: 32 additions & 37 deletions src/internal-context/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { diff } from 'ohash/utils'
import logger from '../logger'
import type { TWConfig } from '../types'
import { twCtx } from './context'
Expand All @@ -7,42 +8,36 @@ const JSONStringifyWithUnsupportedVals = (val: any) => JSON.stringify(val, (_, v
const JSONStringifyWithRegex = (obj: any) => JSON.stringify(obj, (_, v) => v instanceof RegExp ? `__REGEXP ${v.toString()}` : v)

export const createObjProxy = (configUpdatedHook: Record<string, string>, meta: ReturnType<typeof twCtx.use>['meta']) => {
const trackObjChanges = (configPath: string, path: (string | symbol)[] = []): ProxyHandler<Partial<TWConfig>> => ({
get: (target, key: string) => {
return (typeof target[key] === 'object' && target[key] !== null)
? new Proxy(target[key], trackObjChanges(configPath, path.concat(key)))
: target[key]
},

set(target, key, value) {
const cfgKey = path.concat(key).map(k => `[${JSON.stringify(k)}]`).join('')
const resultingCode = `cfg${cfgKey} = ${JSONStringifyWithRegex(value)?.replace(/"__REGEXP (.*)"/g, (_, substr) => substr.replace(/\\"/g, '"')) || `cfg${cfgKey}`};`

if (JSONStringifyWithUnsupportedVals(target[key as string]) === JSONStringifyWithUnsupportedVals(value) || configUpdatedHook[configPath].endsWith(resultingCode)) {
return Reflect.set(target, key, value)
return (configPath: string, oldConfig: Partial<TWConfig>, newConfig: Partial<TWConfig>) =>
diff(oldConfig, newConfig).forEach((change) => {
const path = change.key.split('.').map(k => `[${JSON.stringify(k)}]`).join('')
const newValue = change.newValue?.value

switch (change.type) {
case 'removed': configUpdatedHook[configPath] += `delete cfg${path};`
break
case 'added':
case 'changed': {
const resultingCode = `cfg${path} = ${JSONStringifyWithRegex(newValue)?.replace(/"__REGEXP (.*)"/g, (_, substr) => substr.replace(/\\"/g, '"')) || `cfg${path}`};`

if (JSONStringifyWithUnsupportedVals(change.oldValue?.value) === JSONStringifyWithUnsupportedVals(newValue) || configUpdatedHook[configPath].endsWith(resultingCode)) {
return
}

if (JSONStringifyWithUnsupportedVals(newValue).includes(`"${UNSUPPORTED_VAL_STR}"`) && !meta?.disableHMR) {
logger.warn(
`A hook has injected a non-serializable value in \`config${path}\`, so the Tailwind Config cannot be serialized. Falling back to providing the loaded configuration inlined directly to PostCSS loader..`,
'Please consider using a configuration file/template instead (specifying in `configPath` of the module options) to enable additional support for IntelliSense and HMR.',
)
twCtx.set({ meta: { disableHMR: true } })
}

if (JSONStringifyWithRegex(newValue).includes('__REGEXP') && !meta?.disableHMR) {
logger.warn(`A hook is injecting RegExp values in your configuration (check \`config${path}\`) which may be unsafely serialized. Consider moving your safelist to a separate configuration file/template instead (specifying in \`configPath\` of the module options)`)
}

configUpdatedHook[configPath] += resultingCode
}
}

if (JSONStringifyWithUnsupportedVals(value).includes(`"${UNSUPPORTED_VAL_STR}"`) && !meta?.disableHMR) {
logger.warn(
`A hook has injected a non-serializable value in \`config${cfgKey}\`, so the Tailwind Config cannot be serialized. Falling back to providing the loaded configuration inlined directly to PostCSS loader..`,
'Please consider using a configuration file/template instead (specifying in `configPath` of the module options) to enable additional support for IntelliSense and HMR.',
)
twCtx.set({ meta: { disableHMR: true } })
}

if (JSONStringifyWithRegex(value).includes('__REGEXP') && !meta?.disableHMR) {
logger.warn(`A hook is injecting RegExp values in your configuration (check \`config${cfgKey}\`) which may be unsafely serialized. Consider moving your safelist to a separate configuration file/template instead (specifying in \`configPath\` of the module options)`)
}

configUpdatedHook[configPath] += resultingCode
return Reflect.set(target, key, value)
},

deleteProperty(target, key) {
configUpdatedHook[configPath] += `delete cfg${path.concat(key).map(k => `[${JSON.stringify(k)}]`).join('')};`
return Reflect.deleteProperty(target, key)
},
})

return trackObjChanges
})
}