Skip to content

Commit

Permalink
chore: move fallback to context and in hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ineshbose committed Apr 27, 2024
1 parent c70f848 commit 1c53b6b
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 60 deletions.
69 changes: 58 additions & 11 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,50 @@ 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 trackObjChanges = (configPath: string, path: (string | symbol)[] = []): ProxyHandler<Partial<TWConfig>> => ({
get: (target, key: string) => {
Expand All @@ -44,19 +84,24 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
},

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)
}

// check unsafe stuff here
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 @@ -111,15 +156,15 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
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 @@ -136,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
54 changes: 5 additions & 49 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,6 @@ const deprecationWarnings = (moduleOptions: ModuleOptions, nuxt = useNuxt()) =>
([dOption, alternative]) => moduleOptions[dOption] !== undefined && logger.warn(`Deprecated \`${dOption}\`. ${alternative}`),
)

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 defaults = (nuxt = useNuxt()): ModuleOptions => ({
configPath: 'tailwind.config',
cssPath: join(nuxt.options.dir.assets, 'css/tailwind.css'),
Expand All @@ -84,17 +56,6 @@ export default defineNuxtModule<ModuleOptions>({
if (moduleOptions.quiet) logger.level = LogLevels.silent
deprecationWarnings(moduleOptions, nuxt)

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
}

// install postcss8 module on nuxt < 2.16
if (Number.parseFloat(getNuxtVersion()) < 2.16) {
await installModule('@nuxt/postcss8').catch((e) => {
Expand Down Expand Up @@ -142,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 @@ -163,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 @@ -192,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

0 comments on commit 1c53b6b

Please sign in to comment.