Skip to content

Commit

Permalink
chore: try move some code
Browse files Browse the repository at this point in the history
  • Loading branch information
ineshbose committed Apr 27, 2024
1 parent fef31a6 commit 81f2c6c
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 112 deletions.
112 changes: 112 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useNuxt } from '@nuxt/kit'
import { relative } from 'pathe'
import _loadConfig from 'tailwindcss/loadConfig.js'
import type { ModuleOptions, TWConfig } from './types'
import logger from './logger'
import configMerger from './runtime/merger.mjs'

export const checkUnsafeConfig = (config?: ModuleOptions['config'] | Partial<TWConfig>) => {
if (!config) return

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

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

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

if (config.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 trackObjChanges = (
configOperations: Record<string, string>,
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(configOperations, 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)};`

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

configOperations[configPath] += resultingCode
const reflectResult = Reflect.set(target, key, value)
const unsafePropertyChange = checkUnsafeConfig(target)

if (unsafePropertyChange) {
logger.warn(
`A hook updated property \`${unsafePropertyChange}\` to a non-serializable value. Falling back to providing the loaded configuration inlined directly to PostCSS loader..`,
'Consider passing this configuration through a separate file (specifying in `configPath` of the module options) to enable additional support for IntelliSense and HMR.'
)
}

return reflectResult
},

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

export const createConfigLoader = (configOperations: Record<string, string>, configPaths: string[], defaultConfig: Partial<TWConfig> = {}, nuxt = useNuxt()) =>
async () => {
configPaths.forEach(p => configOperations[p] = '')

const tailwindConfig = await Promise.all((
configPaths.map(async (configPath, idx, paths) => {
let _tailwindConfig: Partial<TWConfig> | undefined

try {
_tailwindConfig = configMerger(undefined, _loadConfig(configPath))
}
catch (e) {
if (!configPath.startsWith(nuxt.options.buildDir)) {
configOperations[configPath] = 'return {};'
logger.warn(`Failed to load Tailwind config at: \`./${relative(nuxt.options.rootDir, configPath)}\``, e)
}
else {
configOperations[configPath] = nuxt.options.dev ? 'return {};' : ''
}
}

// Transform purge option from Array to object with { content }
if (_tailwindConfig?.purge && !_tailwindConfig.content) {
configOperations[configPath] += 'cfg.content = cfg.purge;'
}

await nuxt.callHook('tailwindcss:loadConfig', _tailwindConfig && new Proxy(_tailwindConfig, trackObjChanges(configOperations, configPath)), configPath, idx, paths)
return _tailwindConfig || {}
})),
).then(configs => configs.reduce(
(prev, curr) => configMerger(curr, prev),
defaultConfig
)) as TWConfig

// Allow extending tailwindcss config by other modules
configOperations['main-config'] = ''
await nuxt.callHook('tailwindcss:config', new Proxy(tailwindConfig, trackObjChanges(configOperations, 'main-config')))
return tailwindConfig
}
77 changes: 2 additions & 75 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import _loadConfig from 'tailwindcss/loadConfig.js'
import resolveConfig from 'tailwindcss/resolveConfig.js'
import type { ModuleOptions, TWConfig } from './types'
import { resolveModulePaths } from './resolvers'
import logger from './logger'
import configMerger from './runtime/merger.mjs'
import { createConfigLoader } from './config'

const CONFIG_TEMPLATE_NAME = 'tailwind.config.cjs'

Expand Down Expand Up @@ -36,80 +36,7 @@ const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNux
const configUpdatedHook: Record<string, string> = {}
const configResolvedPath = join(nuxt.options.buildDir, CONFIG_TEMPLATE_NAME)

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 resultingCode = `cfg${path.concat(key).map(k => `[${JSON.stringify(k)}]`).join('')} = ${JSON.stringify(value)};`

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

// check unsafe stuff here
if (key === 'plugins' && typeof value === 'function') {
logger.warn(
'You have injected a functional plugin into your Tailwind Config which cannot be serialized.',
'Please use a configuration file/template instead.',
)
// return false // removed for backwards compatibility
}

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

const loadConfig = async () => {
configPaths.forEach(p => configUpdatedHook[p] = '')

const tailwindConfig = await Promise.all((
configPaths.map(async (configPath, idx, paths) => {
let _tailwindConfig: Partial<TWConfig> | undefined

try {
_tailwindConfig = configMerger(undefined, _loadConfig(configPath))
}
catch (e) {
if (!configPath.startsWith(nuxt.options.buildDir)) {
configUpdatedHook[configPath] = 'return {};'
logger.warn(`Failed to load Tailwind config at: \`./${relative(nuxt.options.rootDir, configPath)}\``, e)
}
else {
configUpdatedHook[configPath] = nuxt.options.dev ? 'return {};' : ''
}
}

// Transform purge option from Array to object with { content }
if (_tailwindConfig?.purge && !_tailwindConfig.content) {
configUpdatedHook[configPath] += 'cfg.content = cfg.purge;'
}

await nuxt.callHook('tailwindcss:loadConfig', _tailwindConfig && new Proxy(_tailwindConfig, trackObjChanges(configPath)), configPath, idx, paths)
return _tailwindConfig || {}
})),
).then(configs => configs.reduce(
(prev, curr) => configMerger(curr, prev),
// internal default tailwind config
configMerger(moduleOptions.config, { content: contentPaths }),
)) as TWConfig

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

return tailwindConfig
}
const loadConfig = createConfigLoader(configUpdatedHook, configPaths, configMerger(moduleOptions.config, { content: contentPaths }), nuxt)

const generateConfig = () => addTemplate({
filename: CONFIG_TEMPLATE_NAME,
Expand Down
53 changes: 16 additions & 37 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ import {
import defaultTailwindConfig from 'tailwindcss/stubs/config.simple.js'

import { name, version, configKey, compatibility } from '../package.json'
import * as resolvers from './resolvers'
import {
resolveExposeConfig,
resolveEditorSupportConfig,
resolveViewerConfig,
resolveCSSPath,
resolveInjectPosition
} from './resolvers'
import logger, { LogLevels } from './logger'
import { createExposeTemplates } from './expose'
import { setupViewer, exportViewer } from './viewer'
import { createInternalContext } from './context'

import type { ModuleOptions, ModuleHooks } from './types'
import { checkUnsafeConfig } from './config'

export type { ModuleOptions, ModuleHooks } from './types'

Expand All @@ -40,34 +47,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 @@ -86,10 +65,10 @@ export default defineNuxtModule<ModuleOptions>({

let enableHMR = true

const unsafeProperty = unsafeInlineConfig(moduleOptions.config)
const unsafeProperty = checkUnsafeConfig(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..`,
`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
Expand All @@ -106,7 +85,7 @@ export default defineNuxtModule<ModuleOptions>({
const ctx = await createInternalContext(moduleOptions, nuxt)

if (moduleOptions.editorSupport || moduleOptions.addTwUtil) {
const editorSupportConfig = resolvers.resolveEditorSupportConfig(moduleOptions.editorSupport)
const editorSupportConfig = resolveEditorSupportConfig(moduleOptions.editorSupport)

if ((editorSupportConfig.autocompleteUtil || moduleOptions.addTwUtil) && !isNuxt2()) {
addImports({
Expand All @@ -120,7 +99,7 @@ export default defineNuxtModule<ModuleOptions>({

// css file handling
const [cssPath, cssPathConfig] = Array.isArray(moduleOptions.cssPath) ? moduleOptions.cssPath : [moduleOptions.cssPath]
const [resolvedCss, loggerInfo] = await resolvers.resolveCSSPath(cssPath, nuxt)
const [resolvedCss, loggerInfo] = await resolveCSSPath(cssPath, nuxt)
logger.info(loggerInfo)

nuxt.options.css = nuxt.options.css ?? []
Expand All @@ -130,7 +109,7 @@ export default defineNuxtModule<ModuleOptions>({
if (resolvedCss && !resolvedNuxtCss.includes(resolvedCss)) {
let injectPosition: number
try {
injectPosition = resolvers.resolveInjectPosition(nuxt.options.css, cssPathConfig?.injectPosition || moduleOptions.injectPosition)
injectPosition = resolveInjectPosition(nuxt.options.css, cssPathConfig?.injectPosition || moduleOptions.injectPosition)
}
catch (e: any) {
throw new Error('failed to resolve Tailwind CSS injection position: ' + e.message)
Expand All @@ -147,7 +126,7 @@ export default defineNuxtModule<ModuleOptions>({

// expose resolved tailwind config as an alias
if (moduleOptions.exposeConfig) {
const exposeConfig = resolvers.resolveExposeConfig({ level: moduleOptions.exposeLevel, ...(typeof moduleOptions.exposeConfig === 'object' ? moduleOptions.exposeConfig : {}) })
const exposeConfig = resolveExposeConfig({ level: moduleOptions.exposeLevel, ...(typeof moduleOptions.exposeConfig === 'object' ? moduleOptions.exposeConfig : {}) })
const exposeTemplates = createExposeTemplates(exposeConfig)
nuxt.hook('tailwindcss:internal:regenerateTemplates', () => updateTemplates({ filter: template => exposeTemplates.includes(template.dst) }))
}
Expand All @@ -170,7 +149,7 @@ export default defineNuxtModule<ModuleOptions>({
if (nuxt.options.dev) {
// add tailwind-config-viewer endpoint
if (moduleOptions.viewer) {
const viewerConfig = resolvers.resolveViewerConfig(moduleOptions.viewer)
const viewerConfig = resolveViewerConfig(moduleOptions.viewer)
setupViewer(enableHMR ? twConfig.dst : _config, viewerConfig, nuxt)

nuxt.hook('devtools:customTabs', (tabs: import('@nuxt/devtools').ModuleOptions['customTabs']) => {
Expand All @@ -190,7 +169,7 @@ export default defineNuxtModule<ModuleOptions>({
else {
// production only
if (moduleOptions.viewer) {
const viewerConfig = resolvers.resolveViewerConfig(moduleOptions.viewer)
const viewerConfig = resolveViewerConfig(moduleOptions.viewer)

if (enableHMR) {
exportViewer(twConfig.dst, viewerConfig)
Expand Down

0 comments on commit 81f2c6c

Please sign in to comment.