Skip to content

Commit

Permalink
fix: better check for unsafe config to fallback (#839)
Browse files Browse the repository at this point in the history
  • Loading branch information
ineshbose committed Apr 28, 2024
1 parent fb8fafb commit 9d78a9f
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 37 deletions.
2 changes: 1 addition & 1 deletion playground/nuxt.config.ts
Expand Up @@ -17,7 +17,7 @@ export default defineNuxtConfig({
],
tailwindcss: {
// viewer: false,
config: { plugins: [typography()] },
config: {},
exposeConfig: true,
cssPath: '~/assets/css/tailwind.css',
editorSupport: true,
Expand Down
76 changes: 62 additions & 14 deletions src/context.ts
Expand Up @@ -31,31 +31,77 @@ twCtx.set = (instance, replace = true) => {
set(resolvedConfig as unknown as TWConfig, replace)
}

const unsafeInlineConfig = (inlineConfig: ModuleOptions['config']) => {
if (!inlineConfig) return

if (
'plugins' in inlineConfig && Array.isArray(inlineConfig.plugins)
&& inlineConfig.plugins.find(p => typeof p === 'function' || typeof p?.handler === 'function')
) {
return 'plugins'
}

if (inlineConfig.content) {
const invalidProperty = ['extract', 'transform'].find((i) => i in inlineConfig.content! && typeof inlineConfig.content![i as keyof ModuleOptions['config']['content']] === 'function' )

if (invalidProperty) {
return `content.${invalidProperty}`
}
}

if (inlineConfig.safelist) {
// @ts-expect-error `s` is never
const invalidIdx = inlineConfig.safelist.findIndex((s) => typeof s === 'object' && s.pattern instanceof RegExp)

if (invalidIdx > -1) {
return `safelist[${invalidIdx}]`
}
}
}

const JSONStringifyWithRegex = (obj: any) => JSON.stringify(obj, (_, v) => v instanceof RegExp ? `__REGEXP ${v.toString()}` : v)

const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNuxt()) => {
const [configPaths, contentPaths] = await resolveModulePaths(moduleOptions.configPath, nuxt)
const configUpdatedHook: Record<string, string> = {}
const configResolvedPath = join(nuxt.options.buildDir, CONFIG_TEMPLATE_NAME)
let enableHMR = true

const unsafeProperty = unsafeInlineConfig(moduleOptions.config)
if (unsafeProperty) {
logger.warn(
`The provided Tailwind configuration in your \`nuxt.config\` is non-serializable. Check \`${unsafeProperty}\`. Falling back to providing the loaded configuration inlined directly to PostCSS loader..`,
'Please consider using `tailwind.config` or a separate file (specifying in `configPath` of the module options) to enable it with additional support for IntelliSense and HMR. Suppress this warning with `quiet: true` in the module options.',
)
enableHMR = false
}

const trackProxy = (configPath: string, path: (string | symbol)[] = []): ProxyHandler<Partial<TWConfig>> => ({
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], trackProxy(configPath, path.concat(key)))
? new Proxy(target[key], trackObjChanges(configPath, path.concat(key)))
: target[key]
},

set(target, key, value) {
const resultingCode = `cfg${path.concat(key).map(k => `[${JSON.stringify(k)}]`).join('')} = ${JSON.stringify(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}`};`
const functionalStringify = (val: any) => JSON.stringify(val, (_, v) => ['function'].includes(typeof v) ? CONFIG_TEMPLATE_NAME + 'ns' : v)

if (JSON.stringify(target[key as string]) === JSON.stringify(value) || configUpdatedHook[configPath].endsWith(resultingCode)) {
if (functionalStringify(target[key as string]) === functionalStringify(value) || configUpdatedHook[configPath].endsWith(resultingCode)) {
return Reflect.set(target, key, value)
}

if (key === 'plugins' && typeof value === 'function') {
if (functionalStringify(value).includes(`"${CONFIG_TEMPLATE_NAME + 'ns'}"`)) {
logger.warn(
'You have injected a functional plugin into your Tailwind Config which cannot be serialized.',
'Please use a configuration file/template instead.',
`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.',
)
// return false // removed for backwards compatibility
enableHMR = false
}

if (JSONStringifyWithRegex(value).includes('__REGEXP')) {
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
Expand Down Expand Up @@ -93,7 +139,7 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
configUpdatedHook[configPath] += 'cfg.content = cfg.purge;'
}

await nuxt.callHook('tailwindcss:loadConfig', _tailwindConfig && new Proxy(_tailwindConfig, trackProxy(configPath)), configPath, idx, paths)
await nuxt.callHook('tailwindcss:loadConfig', _tailwindConfig && new Proxy(_tailwindConfig, trackObjChanges(configPath)), configPath, idx, paths)
return _tailwindConfig || {}
})),
).then(configs => configs.reduce(
Expand All @@ -104,21 +150,21 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux

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

return tailwindConfig
}

const generateConfig = () => addTemplate({
const generateConfig = () => enableHMR ? addTemplate({
filename: CONFIG_TEMPLATE_NAME,
write: true,
getContents: () => {
const serializeConfig = <T extends Partial<TWConfig>>(config: T) =>
JSON.stringify(
Array.isArray(config.plugins) && config.plugins.length > 0 ? configMerger({ plugins: (defaultPlugins: TWConfig['plugins']) => defaultPlugins?.filter(p => p && typeof p !== 'function') }, config) : config,
(_, v) => typeof v === 'function' ? `() => (${JSON.stringify(v())})` : v).replace(/"(\(\) => \(.*\))"/g, (_, substr) => substr.replace(/\\"/g, '"'),
)
(_, v) => typeof v === 'function' ? `() => (${JSON.stringify(v())})` : v
).replace(/"(\(\) => \(.*\))"/g, (_, substr) => substr.replace(/\\"/g, '"'))

const layerConfigs = configPaths.map((configPath) => {
const configImport = `require(${JSON.stringify(/[/\\]node_modules[/\\]/.test(configPath) ? configPath : './' + relative(nuxt.options.buildDir, configPath))})`
Expand All @@ -135,9 +181,11 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
`module.exports = ${configUpdatedHook['main-config'] ? `(() => {const cfg=config;${configUpdatedHook['main-config']};return cfg;})()` : 'config'}\n`,
].join('\n')
},
})
}) : { dst: '' }

const registerHooks = () => {
if (!enableHMR) return

nuxt.hook('app:templatesGenerated', async (_app, templates) => {
if (templates.some(t => configPaths.includes(t.dst))) {
await loadConfig()
Expand Down
25 changes: 5 additions & 20 deletions src/module.ts
Expand Up @@ -56,16 +56,6 @@ export default defineNuxtModule<ModuleOptions>({
if (moduleOptions.quiet) logger.level = LogLevels.silent
deprecationWarnings(moduleOptions, nuxt)

let enableHMR = true

if (Array.isArray(moduleOptions.config.plugins) && moduleOptions.config.plugins.find(p => typeof p === 'function' || typeof p?.handler === 'function')) {
logger.warn(
'You have provided functional plugins in `tailwindcss.config` in your Nuxt configuration that cannot be serialized for Tailwind Config.',
'Please use `tailwind.config` or a separate file (specifying in `tailwindcss.configPath`) to enable it with additional support for IntelliSense and HMR.',
)
enableHMR = false
}

// install postcss8 module on nuxt < 2.16
if (Number.parseFloat(getNuxtVersion()) < 2.16) {
await installModule('@nuxt/postcss8').catch((e) => {
Expand Down Expand Up @@ -113,8 +103,8 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.hook('modules:done', async () => {
const _config = await ctx.loadConfig()

const twConfig = enableHMR ? ctx.generateConfig() : { dst: '' }
enableHMR && ctx.registerHooks()
const twConfig = ctx.generateConfig()
ctx.registerHooks()

// expose resolved tailwind config as an alias
if (moduleOptions.exposeConfig) {
Expand All @@ -134,15 +124,15 @@ export default defineNuxtModule<ModuleOptions>({
postcssOptions.plugins = {
...(postcssOptions.plugins || {}),
'tailwindcss/nesting': postcssOptions.plugins?.['tailwindcss/nesting'] ?? {},
'tailwindcss': enableHMR ? twConfig.dst satisfies string : _config,
'tailwindcss': twConfig.dst satisfies string || _config,
}

// enabled only in development
if (nuxt.options.dev) {
// add tailwind-config-viewer endpoint
if (moduleOptions.viewer) {
const viewerConfig = resolvers.resolveViewerConfig(moduleOptions.viewer)
setupViewer(enableHMR ? twConfig.dst : _config, viewerConfig, nuxt)
setupViewer(twConfig.dst || _config, viewerConfig, nuxt)

nuxt.hook('devtools:customTabs', (tabs: import('@nuxt/devtools').ModuleOptions['customTabs']) => {
tabs?.push({
Expand All @@ -163,12 +153,7 @@ export default defineNuxtModule<ModuleOptions>({
if (moduleOptions.viewer) {
const viewerConfig = resolvers.resolveViewerConfig(moduleOptions.viewer)

if (enableHMR) {
exportViewer(twConfig.dst, viewerConfig)
}
else {
exportViewer(addTemplate({ filename: 'tailwind.config/viewer-config.cjs', getContents: () => `module.exports = ${JSON.stringify(_config)}`, write: true }).dst, viewerConfig)
}
exportViewer(twConfig.dst || addTemplate({ filename: 'tailwind.config/viewer-config.cjs', getContents: () => `module.exports = ${JSON.stringify(_config)}`, write: true }).dst, viewerConfig)
}
}
})
Expand Down
12 changes: 10 additions & 2 deletions src/types.ts
Expand Up @@ -3,7 +3,15 @@ type Import = Exclude<Parameters<typeof import('nuxt/kit')['addImports']>[0], an
export type TWConfig = import('tailwindcss').Config
export type InjectPosition = 'first' | 'last' | number | { after: string }

interface ExtendTailwindConfig {
type _Omit<T, K extends PropertyKey> = { [P in keyof T as Exclude<P, K>]: T[P] }

type InlineTWConfig = _Omit<TWConfig, 'content' | 'plugins' | 'safelist'> & {
content?: (Extract<TWConfig['content'], any[]> | _Omit<Extract<TWConfig['content'], Record<string, any>>, 'extract' | 'transform'>)
// plugins?: Extract<NonNullable<TWConfig['plugins']>[number], string | [string, any]>[] // incoming in Oxide
safelist?: Exclude<NonNullable<TWConfig['safelist']>[number], Record<string, any>>[]
}

type ExtendTWConfig = {
content?:
| TWConfig['content']
| ((contentDefaults: Array<string>) => TWConfig['content'])
Expand Down Expand Up @@ -84,7 +92,7 @@ export interface ModuleOptions {
*
* for default, see https://tailwindcss.nuxtjs.org/tailwind/config
*/
config: Omit<TWConfig, keyof ExtendTailwindConfig> & ExtendTailwindConfig
config: InlineTWConfig
/**
* [tailwind-config-viewer](https://github.com/rogden/tailwind-config-viewer) usage *in development*
*
Expand Down

0 comments on commit 9d78a9f

Please sign in to comment.