diff --git a/package.json b/package.json index 43332ab4..be9dfefd 100755 --- a/package.json +++ b/package.json @@ -11,9 +11,14 @@ }, "exports": { ".": { - "types": "./dist/types.d.ts", + "types": "./dist/module.d.ts", "require": "./dist/module.cjs", "import": "./dist/module.mjs" + }, + "./config": { + "types": "./dist/config.d.ts", + "require": "./dist/config.cjs", + "import": "./dist/config.mjs" } }, "main": "./dist/module.cjs", @@ -21,6 +26,10 @@ "files": [ "dist" ], + "build": { + "entries": ["./src/config"], + "rollup": { "emitCJS": true } + }, "scripts": { "play": "pnpm dev", "prepare": "nuxt-module-build prepare", @@ -46,6 +55,7 @@ "dependencies": { "@nuxt/kit": "^3.14.1592", "autoprefixer": "^10.4.20", + "c12": "^2.0.1", "consola": "^3.2.3", "defu": "^6.1.4", "h3": "^1.13.0", diff --git a/playground/modules/template.js b/playground/modules/template.js index c86b17ab..f5db9900 100644 --- a/playground/modules/template.js +++ b/playground/modules/template.js @@ -21,7 +21,7 @@ export default defineNuxtModule((_, nuxt) => { nuxt.options.tailwindcss = nuxt.options.tailwindcss ?? {} if (!Array.isArray(nuxt.options.tailwindcss.configPath)) { - nuxt.options.tailwindcss.configPath = nuxt.options.tailwindcss.configPath ? [nuxt.options.tailwindcss.configPath] : ['tailwind.config'] + nuxt.options.tailwindcss.configPath = nuxt.options.tailwindcss.configPath ? [nuxt.options.tailwindcss.configPath] : [] } nuxt.options.tailwindcss.configPath.push(template.dst) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 246ad087..6e987ec8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) + c12: + specifier: ^2.0.1 + version: 2.0.1(magicast@0.3.5) consola: specifier: ^3.2.3 version: 3.2.3 diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..d0d5fc87 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,5 @@ +import { createDefineConfig } from 'c12' +import type { Config } from 'tailwindcss' + +export const defineConfig = createDefineConfig>() +export default defineConfig diff --git a/src/context.ts b/src/context.ts deleted file mode 100644 index 1a8bd9a9..00000000 --- a/src/context.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { getContext } from 'unctx' -import { addTemplate, createResolver, updateTemplates, useNuxt } from '@nuxt/kit' -import { join, relative } from 'pathe' -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.js' - -const CONFIG_TEMPLATE_NAME = 'tailwind.config.cjs' - -const twCtx = getContext('twcss') -const { tryUse, set } = twCtx -twCtx.tryUse = () => { - const ctx = tryUse() - - if (!ctx) { - try { - return resolveConfig(_loadConfig(join(useNuxt().options.buildDir, CONFIG_TEMPLATE_NAME))) as unknown as TWConfig - } - catch { /* empty */ } - } - - return ctx -} -twCtx.set = (instance, replace = true) => { - const resolvedConfig = instance && resolveConfig(instance) - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - resolvedConfig && useNuxt().callHook('tailwindcss:resolvedConfig', resolvedConfig, twCtx.tryUse() ?? undefined) - - 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 = {} - const configResolvedPath = join(nuxt.options.buildDir, CONFIG_TEMPLATE_NAME) - let enableHMR = true - - if (moduleOptions.disableHMR) { - enableHMR = false - } - - const unsafeProperty = unsafeInlineConfig(moduleOptions.config) - if (unsafeProperty && enableHMR) { - 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> => ({ - 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}`};` - const functionalStringify = (val: any) => JSON.stringify(val, (_, v) => ['function'].includes(typeof v) ? CONFIG_TEMPLATE_NAME + 'ns' : v) - - if (functionalStringify(target[key as string]) === functionalStringify(value) || configUpdatedHook[configPath].endsWith(resultingCode)) { - return Reflect.set(target, key, value) - } - - if (functionalStringify(value).includes(`"${CONFIG_TEMPLATE_NAME + 'ns'}"`) && enableHMR) { - 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.', - ) - enableHMR = false - } - - if (JSONStringifyWithRegex(value).includes('__REGEXP') && enableHMR) { - 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) - }, - }) - - const loadConfig = async () => { - configPaths.forEach(p => configUpdatedHook[p] = '') - - const tailwindConfig = await Promise.all(( - configPaths.map(async (configPath, idx, paths) => { - const _tailwindConfig = ((): Partial | undefined => { - try { - return configMerger(undefined, _loadConfig(configPath)) - } - catch (e) { - const error = e instanceof Error ? ('code' in e ? e.code as string : e.name).toUpperCase() : typeof e === 'string' ? e.toUpperCase() : '' - - if (configPath.startsWith(nuxt.options.buildDir) && ['MODULE_NOT_FOUND'].includes(error)) { - configUpdatedHook[configPath] = nuxt.options.dev ? 'return {};' : '' - return - } - - configUpdatedHook[configPath] = 'return {};' - logger.warn(`Failed to load config \`./${relative(nuxt.options.rootDir, configPath)}\` due to the error below. Skipping..\n`, e) - } - })() - - // 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: { files: 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 generateConfig = () => enableHMR - ? addTemplate({ - filename: CONFIG_TEMPLATE_NAME, - write: true, - getContents: () => { - const serializeConfig = >(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, '"')) - - const layerConfigs = configPaths.map((configPath) => { - const configImport = `require(${JSON.stringify(/[/\\]node_modules[/\\]/.test(configPath) ? configPath : './' + relative(nuxt.options.buildDir, configPath))})` - return configUpdatedHook[configPath] ? configUpdatedHook[configPath].startsWith('return {};') ? '' : `(() => {const cfg=configMerger(undefined, ${configImport});${configUpdatedHook[configPath]};return cfg;})()` : configImport - }).filter(Boolean) - - return [ - `// generated by the @nuxtjs/tailwindcss module at ${(new Date()).toLocaleString()}`, - `const configMerger = require(${JSON.stringify(createResolver(import.meta.url).resolve('./runtime/merger.js'))});`, - `\nconst inlineConfig = ${serializeConfig(moduleOptions.config as Partial)};\n`, - 'const config = [', - layerConfigs.join(',\n'), - `].reduce((prev, curr) => configMerger(curr, prev), configMerger(inlineConfig, { content: { files: ${JSON.stringify(contentPaths)} } }));\n`, - `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 (Array.isArray(templates) && templates?.some(t => configPaths.includes(t.dst))) { - await loadConfig() - setTimeout(async () => { - await updateTemplates({ filter: t => t.filename === CONFIG_TEMPLATE_NAME }) - await nuxt.callHook('tailwindcss:internal:regenerateTemplates', { configTemplateUpdated: true }) - }, 100) - } - }) - - nuxt.hook('vite:serverCreated', (server) => { - nuxt.hook('tailwindcss:internal:regenerateTemplates', (data) => { - if (!data || !data.configTemplateUpdated) return - const configFile = server.moduleGraph.getModuleById(configResolvedPath) - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - configFile && server.moduleGraph.invalidateModule(configFile) - }) - }) - - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - moduleOptions.exposeConfig && nuxt.hook('builder:watch', async (_, path) => { - if (configPaths.includes(join(nuxt.options.rootDir, path))) { - twCtx.set(_loadConfig(configResolvedPath)) - setTimeout(async () => { - await nuxt.callHook('tailwindcss:internal:regenerateTemplates') - }, 100) - } - }) - } - - return { - loadConfig, - generateConfig, - registerHooks, - } -} - -export { twCtx, createInternalContext } diff --git a/src/expose.ts b/src/expose.ts index 6434ed5a..00b872b8 100644 --- a/src/expose.ts +++ b/src/expose.ts @@ -2,16 +2,20 @@ import { dirname, join } from 'pathe' import { useNuxt, addTemplate, addTypeTemplate } from '@nuxt/kit' import type { ResolvedNuxtTemplate } from 'nuxt/schema' import type { ExposeConfig } from './types' -import { twCtx } from './context' +import { twCtx } from './internal-context/context' const NON_ALPHANUMERIC_RE = /^[0-9a-z]+$/i const isJSObject = (value: any) => typeof value === 'object' && !Array.isArray(value) export const createExposeTemplates = (config: ExposeConfig, nuxt = useNuxt()) => { const templates: ResolvedNuxtTemplate[] = [] - const getTWConfig = (objPath: string[] = []) => objPath.reduce((prev, curr) => prev?.[curr], twCtx.tryUse() as any) - const populateMap = (obj: any, path: string[] = [], level = 1) => { + const getTWConfig = ( + objPath: string[] = [], + twConfig = twCtx.use().config, + ) => objPath.reduce((prev, curr) => prev?.[curr], twConfig as Record) + + const populateMap = (obj: any = twCtx.use().config, path: string[] = [], level = 1) => { Object.entries(obj).forEach(([key, value = {} as any]) => { const subpathComponents = path.concat(key) const subpath = subpathComponents.join('/') @@ -63,7 +67,7 @@ export const createExposeTemplates = (config: ExposeConfig, nuxt = useNuxt()) => }) } - populateMap(twCtx.tryUse()) + populateMap() const entryTemplate = addTemplate({ filename: 'tailwind.config/index.mjs', diff --git a/src/internal-context/context.ts b/src/internal-context/context.ts new file mode 100644 index 00000000..a5b3b6ce --- /dev/null +++ b/src/internal-context/context.ts @@ -0,0 +1,20 @@ +import { defu } from 'defu' +import { getContext } from 'unctx' +import type { TWConfig } from '../types' + +type Context = { + config: Partial + dst: string + meta: { + disableHMR?: boolean + } +} + +const twCtx = getContext>('twcss') +const { set } = twCtx + +twCtx.set = (instance, replace = true) => { + set(defu(instance, twCtx.tryUse()), replace) +} + +export { twCtx } diff --git a/src/internal-context/load.ts b/src/internal-context/load.ts new file mode 100644 index 00000000..976d0efa --- /dev/null +++ b/src/internal-context/load.ts @@ -0,0 +1,241 @@ +import { addTemplate, findPath, resolveAlias, updateTemplates, useNuxt } from '@nuxt/kit' +import type { NuxtOptions, NuxtConfig } from '@nuxt/schema' +import { join, relative, resolve } from 'pathe' +import { getContext } from 'unctx' +import { loadConfig as loadConfigC12, type ResolvedConfig as ResolvedC12Config } from 'c12' +import type { ModuleOptions, TWConfig } from '../types' +import logger from '../logger' +import configMerger from '../runtime/merger.js' +import { twCtx } from './context' +import { checkUnsafeInlineConfig } from './validate' +import { createObjProxy } from './proxy' + +const loadConfig = loadConfigC12> +type ResolvedConfig = ResolvedC12Config> + +const resolvedConfigsCtx = getContext>('twcss-resolved-configs') + +const createInternalContext = async (moduleOptions: ModuleOptions, nuxt = useNuxt()) => { + const configUpdatedHook: Record = {} + const { meta = { disableHMR: moduleOptions.disableHMR } } = twCtx.tryUse() ?? {} + const trackObjChanges = createObjProxy(configUpdatedHook, meta) + + const resolveConfigs = | string | undefined>(configs: T | T[], nuxt = useNuxt()) => + ((Array.isArray(configs) ? configs : [configs]) + .filter(Boolean) + .map(async (config, idx, arr): Promise => { + if (typeof config !== 'string') { + const hasUnsafeProperty = checkUnsafeInlineConfig(config) + if (hasUnsafeProperty && !meta.disableHMR) { + logger.warn( + `The provided Tailwind configuration in your \`nuxt.config\` is non-serializable. Check \`${hasUnsafeProperty}\`. 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.', + ) + meta.disableHMR = true + twCtx.set({ meta }) + } + + return { config } as { config: NonNullable } + } + + const configFile = await (config.startsWith(nuxt.options.buildDir) ? config : findPath(config, { extensions: ['.js', '.cjs', '.mjs', '.ts'] })) + return configFile + ? loadConfig({ configFile }).then(async (resolvedConfig) => { + const { configFile: resolvedConfigFile = configFile } = resolvedConfig + const config = configMerger(undefined, resolvedConfig.config) + configUpdatedHook[resolvedConfigFile] = '' + + if (resolvedConfig.config?.purge && !resolvedConfig.config.content) { + configUpdatedHook[resolvedConfigFile] += 'cfg.content = cfg.purge;' + } + + await nuxt.callHook('tailwindcss:loadConfig', new Proxy(config, trackObjChanges(resolvedConfigFile)), resolvedConfigFile, idx, arr as any) + return { ...resolvedConfig, config } + }).catch((e) => { + logger.warn(`Failed to load config \`./${relative(nuxt.options.rootDir, configFile)}\` due to the error below. Skipping..\n`, e) + return null + }) + : null + })) + + const resolveContentConfig = (srcDir: string, nuxtOptions: NuxtOptions | NuxtConfig = useNuxt().options): ResolvedConfig => { + const r = (p: string) => p.startsWith(srcDir) ? p : resolve(srcDir, p) + const extensionFormat = (s: string[]) => s.length > 1 ? `.{${s.join(',')}}` : `.${s.join('') || 'vue'}` + + const defaultExtensions = extensionFormat(['js', 'ts', 'mjs']) + const sfcExtensions = extensionFormat(Array.from(new Set(['.vue', ...(nuxtOptions.extensions || nuxt.options.extensions)])).map(e => e?.replace(/^\.*/, '')).filter((v): v is string => Boolean(v))) + + const importDirs = [...(nuxtOptions.imports?.dirs || [])].filter((v): v is string => Boolean(v)).map(r) + const [composablesDir, utilsDir] = [resolve(srcDir, 'composables'), resolve(srcDir, 'utils')] + + if (!importDirs.includes(composablesDir)) importDirs.push(composablesDir) + if (!importDirs.includes(utilsDir)) importDirs.push(utilsDir) + + return { + config: { + content: [ + r(`components/**/*${sfcExtensions}`), + ...(() => { + if (nuxtOptions.components) { + return (Array.isArray(nuxtOptions.components) ? nuxtOptions.components : typeof nuxtOptions.components === 'boolean' ? ['components'] : (nuxtOptions.components.dirs || [])).map((d) => { + const valueToResolve = typeof d === 'string' ? d : d?.path + return valueToResolve ? `${resolveAlias(valueToResolve)}/**/*${sfcExtensions}` : '' + }).filter(Boolean) + } + return [] + })(), + + nuxtOptions.dir?.layouts && r(`${nuxtOptions.dir.layouts}/**/*${sfcExtensions}`), + ...([true, undefined].includes(nuxtOptions.pages) && nuxtOptions.dir?.pages ? [r(`${nuxtOptions.dir.pages}/**/*${sfcExtensions}`)] : []), + + nuxtOptions.dir?.plugins && r(`${nuxtOptions.dir.plugins}/**/*${defaultExtensions}`), + ...importDirs.map(d => `${d}/**/*${defaultExtensions}`), + + r(`{A,a}pp${sfcExtensions}`), + r(`{E,e}rror${sfcExtensions}`), + r(`app.config${defaultExtensions}`), + !nuxtOptions.ssr && nuxtOptions.spaLoadingTemplate !== false && r(typeof nuxtOptions.spaLoadingTemplate === 'string' ? nuxtOptions.spaLoadingTemplate : 'app/spa-loading-template.html'), + ].filter((p): p is string => Boolean(p)), + }, + } + } + + const getModuleConfigs = () => { + const thenCallHook = async (resolvedConfig: ResolvedConfig) => { + const { configFile: resolvedConfigFile } = resolvedConfig + if (!resolvedConfigFile || !resolvedConfig.config) { + return resolvedConfig + } + + const config = configMerger(undefined, resolvedConfig.config) + configUpdatedHook[resolvedConfigFile] = '' + + if (resolvedConfig.config?.purge && !resolvedConfig.config.content) { + configUpdatedHook[resolvedConfigFile] += 'cfg.content = cfg.purge;' + } + + await nuxt.callHook('tailwindcss:loadConfig', new Proxy(config, trackObjChanges(resolvedConfigFile)), resolvedConfigFile, 0, []) + return { ...resolvedConfig, config } + } + + return Promise.all([ + resolveContentConfig(nuxt.options.srcDir, nuxt.options), + ...resolveConfigs(moduleOptions.config, nuxt), + loadConfig({ name: 'tailwind', cwd: nuxt.options.rootDir, merger: configMerger, packageJson: true, extend: false }).then(thenCallHook), + ...resolveConfigs(moduleOptions.configPath, nuxt), + + ...(nuxt.options._layers || []).slice(1).flatMap(nuxtLayer => [ + resolveContentConfig(nuxtLayer.config?.srcDir || nuxtLayer.cwd, nuxtLayer.config), + // @ts-expect-error layer config + ...resolveConfigs(nuxtLayer.config.tailwindcss?.config, nuxt), + loadConfig({ name: 'tailwind', cwd: nuxtLayer.cwd, merger: configMerger, packageJson: true, extend: false }).then(thenCallHook), + // @ts-expect-error layer config + ...resolveConfigs(nuxtLayer.config.tailwindcss?.configPath, nuxt), + ]), + ]) + } + + const resolveTWConfig = await import('tailwindcss/resolveConfig').then(m => m.default || m).catch(() => (c: unknown) => c) as >(config: T) => T + + const loadConfigs = async () => { + const moduleConfigs = await getModuleConfigs() + resolvedConfigsCtx.set(moduleConfigs, true) + const tailwindConfig = moduleConfigs.reduce((acc, curr) => configMerger(acc, curr?.config ?? {}), {} as Partial) + + // Allow extending tailwindcss config by other modules + configUpdatedHook['main-config'] = '' + await nuxt.callHook('tailwindcss:config', new Proxy(tailwindConfig, trackObjChanges('main-config'))) + + const resolvedConfig = resolveTWConfig(tailwindConfig) + await nuxt.callHook('tailwindcss:resolvedConfig', resolvedConfig as any, twCtx.tryUse()?.config as any ?? undefined) + twCtx.set({ config: resolvedConfig }) + + return tailwindConfig + } + + const generateConfig = (options: { mergerPath: string }) => { + const ctx = twCtx.tryUse() + + const template = !meta.disableHMR || !ctx?.meta?.disableHMR + ? addTemplate({ + filename: 'tailwind.config.cjs', + write: true, + getContents: () => { + const serializeConfig = >(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, '"')) + + const layerConfigs = resolvedConfigsCtx.use().map((c) => { + if (c?.configFile) { + const configImport = `require(${JSON.stringify(/[/\\]node_modules[/\\]/.test(c.configFile) ? c.configFile : './' + relative(nuxt.options.buildDir, c.configFile))})` + return configUpdatedHook[c.configFile] ? `(() => {const cfg=configMerger(undefined, ${configImport});${configUpdatedHook[c.configFile]};return cfg;})()` : configImport + } + + return c && serializeConfig(c.config) + }).filter(Boolean) + + return [ + `// generated by the @nuxtjs/tailwindcss module at ${(new Date()).toLocaleString()}`, + `const configMerger = require(${JSON.stringify(options.mergerPath)});\n`, + 'const config = [', + layerConfigs.join(',\n'), + `].reduce((acc, curr) => configMerger(acc, curr), {});\n`, + `module.exports = ${configUpdatedHook['main-config'] ? `(() => {const cfg=config;${configUpdatedHook['main-config']};return cfg;})()` : 'config'}\n`, + ].join('\n') + }, + }) + : { dst: '' } + + twCtx.set({ dst: template.dst }) + return template + } + + const registerHooks = () => { + if (twCtx.use().meta?.disableHMR) return + + nuxt.hook('app:templatesGenerated', async (_app, templates) => { + if (Array.isArray(templates) && templates?.some(t => Object.keys(configUpdatedHook).includes(t.dst))) { + const { dst } = twCtx.use() + await loadConfigs() + + setTimeout(async () => { + await updateTemplates({ filter: t => t.dst === dst || dst?.endsWith(t.filename) || false }) + await nuxt.callHook('tailwindcss:internal:regenerateTemplates', { configTemplateUpdated: true }) + }, 100) + } + }) + + nuxt.hook('vite:serverCreated', (server) => { + nuxt.hook('tailwindcss:internal:regenerateTemplates', (data) => { + if (!data || !data.configTemplateUpdated) return + const ctx = twCtx.use() + const configFile = ctx.dst && server.moduleGraph.getModuleById(ctx.dst) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + configFile && server.moduleGraph.invalidateModule(configFile) + }) + }) + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + moduleOptions.exposeConfig && nuxt.hook('builder:watch', async (_, path) => { + if (Object.keys(configUpdatedHook).includes(join(nuxt.options.rootDir, path))) { + const ctx = twCtx.use() + setTimeout(async () => { + await import(ctx.dst).then(async (config) => { + twCtx.set({ config: resolveTWConfig(_config) }) + await nuxt.callHook('tailwindcss:internal:regenerateTemplates') + }) + }, 100) + } + }) + } + + return { + loadConfigs, + generateConfig, + registerHooks, + } +} + +export { createInternalContext } diff --git a/src/internal-context/proxy.ts b/src/internal-context/proxy.ts new file mode 100644 index 00000000..aca79a83 --- /dev/null +++ b/src/internal-context/proxy.ts @@ -0,0 +1,48 @@ +import logger from '../logger' +import type { TWConfig } from '../types' +import { twCtx } from './context' + +const UNSUPPORTED_VAL_STR = 'UNSUPPORTED_VAL_STR' +const JSONStringifyWithUnsupportedVals = (val: any) => JSON.stringify(val, (_, v) => ['function'].includes(typeof v) ? UNSUPPORTED_VAL_STR : v) +const JSONStringifyWithRegex = (obj: any) => JSON.stringify(obj, (_, v) => v instanceof RegExp ? `__REGEXP ${v.toString()}` : v) + +export const createObjProxy = (configUpdatedHook: Record, meta: ReturnType['meta']) => { + const trackObjChanges = (configPath: string, path: (string | symbol)[] = []): ProxyHandler> => ({ + 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) + } + + 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 +} diff --git a/src/internal-context/validate.ts b/src/internal-context/validate.ts new file mode 100644 index 00000000..cfd72f21 --- /dev/null +++ b/src/internal-context/validate.ts @@ -0,0 +1,29 @@ +import type { TWConfig } from '../types' + +export const checkUnsafeInlineConfig = >(inlineConfig: T | undefined) => { + 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) { + // @ts-expect-error indexing content with different possibilities + const invalidProperty = ['extract', 'transform'].find(i => i in inlineConfig.content! && typeof inlineConfig.content![i] === 'function') + + if (invalidProperty) { + return `content.${invalidProperty}` + } + } + + if (inlineConfig.safelist) { + const invalidIdx = inlineConfig.safelist.findIndex(s => typeof s === 'object' && s.pattern instanceof RegExp) + + if (invalidIdx > -1) { + return `safelist[${invalidIdx}]` + } + } +} diff --git a/src/module.ts b/src/module.ts index d95c7703..d6fe962d 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,5 +1,4 @@ import { join } from 'pathe' -import { joinURL } from 'ufo' import { defineNuxtModule, installModule, @@ -13,37 +12,21 @@ import { isNuxtMajorVersion, } from '@nuxt/kit' -// @ts-expect-error no declaration file -import defaultTailwindConfig from 'tailwindcss/stubs/config.simple.js' - import { name, version, configKey, compatibility } from '../package.json' import * as resolvers from './resolvers' import logger, { LogLevels } from './logger' import { createExposeTemplates } from './expose' import { setupViewer, exportViewer } from './viewer' -import { createInternalContext } from './context' +import { createInternalContext } from './internal-context/load' import type { ModuleOptions, ModuleHooks } from './types' export type { ModuleOptions, ModuleHooks } from './types' -const deprecationWarnings = (moduleOptions: ModuleOptions, nuxt = useNuxt()) => - ([ - ['addTwUtil', 'Use `editorSupport.autocompleteUtil` instead.'], - ['exposeLevel', 'Use `exposeConfig.level` instead.'], - ['injectPosition', `Use \`cssPath: [${ - moduleOptions.cssPath === join(nuxt.options.dir.assets, 'css/tailwind.css') - ? '"~/assets/css/tailwind.css"' - : typeof moduleOptions.cssPath === 'string' ? `"${moduleOptions.cssPath}"` : moduleOptions.cssPath - }, { injectPosition: ${JSON.stringify(moduleOptions.injectPosition)} }]\` instead.`], - ] satisfies Array<[keyof ModuleOptions, string]>).forEach( - ([dOption, alternative]) => moduleOptions[dOption] !== undefined && logger.warn(`Deprecated \`${dOption}\`. ${alternative}`), - ) - const defaults = (nuxt = useNuxt()): ModuleOptions => ({ - configPath: 'tailwind.config', + configPath: [], cssPath: join(nuxt.options.dir.assets, 'css/tailwind.css'), - config: defaultTailwindConfig, + config: {}, viewer: true, exposeConfig: false, quiet: nuxt.options.logLevel === 'silent', @@ -54,7 +37,6 @@ export default defineNuxtModule({ meta: { name, version, configKey, compatibility }, defaults, async setup(moduleOptions, nuxt) { if (moduleOptions.quiet) logger.level = LogLevels.silent - deprecationWarnings(moduleOptions, nuxt) // install postcss8 module on nuxt < 2.16 if (Number.parseFloat(getNuxtVersion()) < 2.16) { @@ -66,10 +48,10 @@ export default defineNuxtModule({ const ctx = await createInternalContext(moduleOptions, nuxt) - if (moduleOptions.editorSupport || moduleOptions.addTwUtil) { + if (moduleOptions.editorSupport) { const editorSupportConfig = resolvers.resolveEditorSupportConfig(moduleOptions.editorSupport) - if ((editorSupportConfig.autocompleteUtil || moduleOptions.addTwUtil) && !isNuxtMajorVersion(2, nuxt)) { + if ((editorSupportConfig.autocompleteUtil) && !isNuxtMajorVersion(2, nuxt)) { addImports({ name: 'autocompleteUtil', from: createResolver(import.meta.url).resolve('./runtime/utils'), @@ -91,7 +73,7 @@ export default defineNuxtModule({ if (resolvedCss && !resolvedNuxtCss.includes(resolvedCss)) { let injectPosition: number try { - injectPosition = resolvers.resolveInjectPosition(nuxt.options.css, cssPathConfig?.injectPosition || moduleOptions.injectPosition) + injectPosition = resolvers.resolveInjectPosition(nuxt.options.css, cssPathConfig?.injectPosition) } catch (e: any) { throw new Error('failed to resolve Tailwind CSS injection position: ' + e.message) @@ -104,16 +86,16 @@ export default defineNuxtModule({ let nuxt2ViewerConfig: Parameters[0] = join(nuxt.options.buildDir, 'tailwind.config.cjs') nuxt.hook('modules:done', async () => { - const _config = await ctx.loadConfig() + const _config = await ctx.loadConfigs() - const twConfig = ctx.generateConfig() + const twConfig = ctx.generateConfig({ mergerPath: createResolver(import.meta.url).resolve('./runtime/merger.js') }) ctx.registerHooks() nuxt2ViewerConfig = twConfig.dst || _config // 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 = resolvers.resolveExposeConfig(moduleOptions.exposeConfig) const exposeTemplates = createExposeTemplates(exposeConfig) nuxt.hook('tailwindcss:internal:regenerateTemplates', () => updateTemplates({ filter: template => exposeTemplates.includes(template.dst) })) } @@ -138,19 +120,6 @@ export default defineNuxtModule({ if (moduleOptions.viewer) { const viewerConfig = resolvers.resolveViewerConfig(moduleOptions.viewer) setupViewer(twConfig.dst || _config, viewerConfig, nuxt) - - nuxt.hook('devtools:customTabs', (tabs: import('@nuxt/devtools').ModuleOptions['customTabs']) => { - tabs?.push({ - title: 'Tailwind CSS', - name: 'tailwindcss', - icon: 'logos-tailwindcss-icon', - category: 'modules', - view: { - type: 'iframe', - src: joinURL(nuxt.options.app?.baseURL, viewerConfig.endpoint), - }, - }) - }) } } else { diff --git a/src/resolvers.ts b/src/resolvers.ts index 1bb7d0dd..7d71136a 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -1,87 +1,9 @@ import { existsSync } from 'node:fs' import { defu } from 'defu' -import { join, relative, resolve } from 'pathe' -import { findPath, useNuxt, tryResolveModule, resolveAlias, resolvePath } from '@nuxt/kit' +import { join, relative } from 'pathe' +import { useNuxt, tryResolveModule, resolvePath } from '@nuxt/kit' import type { EditorSupportConfig, ExposeConfig, InjectPosition, ModuleOptions, ViewerConfig } from './types' -/** - * Resolves all configPath values for an application - * - * @param path configPath for a layer - * @returns array of resolved paths - */ -const resolveConfigPath = async (path: ModuleOptions['configPath'], nuxtOptions = useNuxt().options) => - Promise.all( - (Array.isArray(path) ? path : [path]) - .filter(Boolean) - .map(path => path.startsWith(nuxtOptions.buildDir) ? path : findPath(path, { extensions: ['.js', '.cjs', '.mjs', '.ts'] })), - ).then(paths => paths.filter((p): p is string => Boolean(p))) - -/** - * - * @param srcDir - * @returns array of resolved content globs - */ -const resolveContentPaths = (srcDir: string, nuxtOptions = useNuxt().options) => { - const r = (p: string) => p.startsWith(srcDir) ? p : resolve(srcDir, p) - const extensionFormat = (s: string[]) => s.length > 1 ? `.{${s.join(',')}}` : `.${s.join('') || 'vue'}` - - const defaultExtensions = extensionFormat(['js', 'ts', 'mjs']) - const sfcExtensions = extensionFormat(Array.from(new Set(['.vue', ...nuxtOptions.extensions])).map(e => e.replace(/^\.*/, ''))) - - const importDirs = [...(nuxtOptions.imports?.dirs || [])].map(r) - const [composablesDir, utilsDir] = [resolve(srcDir, 'composables'), resolve(srcDir, 'utils')] - - if (!importDirs.includes(composablesDir)) importDirs.push(composablesDir) - if (!importDirs.includes(utilsDir)) importDirs.push(utilsDir) - - return [ - r(`components/**/*${sfcExtensions}`), - ...(() => { - if (nuxtOptions.components) { - return (Array.isArray(nuxtOptions.components) ? nuxtOptions.components : typeof nuxtOptions.components === 'boolean' ? ['components'] : nuxtOptions.components.dirs).map(d => `${resolveAlias(typeof d === 'string' ? d : d.path)}/**/*${sfcExtensions}`) - } - return [] - })(), - - nuxtOptions.dir.layouts && r(`${nuxtOptions.dir.layouts}/**/*${sfcExtensions}`), - ...([true, undefined].includes(nuxtOptions.pages) ? [r(`${nuxtOptions.dir.pages}/**/*${sfcExtensions}`)] : []), - - nuxtOptions.dir.plugins && r(`${nuxtOptions.dir.plugins}/**/*${defaultExtensions}`), - ...importDirs.map(d => `${d}/**/*${defaultExtensions}`), - - r(`{A,a}pp${sfcExtensions}`), - r(`{E,e}rror${sfcExtensions}`), - r(`app.config${defaultExtensions}`), - !nuxtOptions.ssr && nuxtOptions.spaLoadingTemplate !== false && r(typeof nuxtOptions.spaLoadingTemplate === 'string' ? nuxtOptions.spaLoadingTemplate : 'app/spa-loading-template.html'), - ].filter((p): p is string => Boolean(p)) -} - -/** - * - * @param configPath - * @param nuxt - * @returns [configuration paths, default resolved content paths] - */ -export const resolveModulePaths = async (configPath: ModuleOptions['configPath'], nuxt = useNuxt()) => { - const mainPaths: [string[], string[]] = [await resolveConfigPath(configPath), resolveContentPaths(nuxt.options.srcDir, nuxt.options)] - - if (Array.isArray(nuxt.options._layers) && nuxt.options._layers.length > 1) { - const layerPaths = await Promise.all( - nuxt.options._layers.slice(1).map(async (layer): Promise<[string[], string[]]> => ([ - await resolveConfigPath(layer?.config?.tailwindcss?.configPath || join(layer.cwd, 'tailwind.config'), nuxt.options), - resolveContentPaths(layer?.config?.srcDir || layer.cwd, defu(layer.config, nuxt.options) as typeof nuxt.options), - ]))) - - layerPaths.forEach(([configPaths, contentPaths]) => { - mainPaths[0].unshift(...configPaths) - mainPaths[1].unshift(...contentPaths) - }) - } - - return mainPaths -} - /** * * @param cssPath diff --git a/src/runtime/merger.js b/src/runtime/merger.js index 45b3439a..07bdca2d 100644 --- a/src/runtime/merger.js +++ b/src/runtime/merger.js @@ -10,7 +10,7 @@ const isJSObject = value => typeof value === 'object' && !Array.isArray(value) * * Read . * - * @type {(...p: Array | Record | undefined>) => Partial} + * @type {(...p: Array | Record | null | undefined>) => Partial} */ export default (base, ...defaults) => { if (!base) { @@ -27,12 +27,16 @@ export default (base, ...defaults) => { obj[key] = { ...value, files: [...obj[key], ...(value.files || [])] } return true } - } - // keeping arrayFn - if (obj[key] && typeof value === 'function') { - obj[key] = value(Array.isArray(obj[key]) ? obj[key] : obj[key]['files']) - return true + // keeping arrayFn + if (obj[key] && typeof value === 'function') { + obj[key] = value(Array.isArray(obj[key]) ? obj[key] : obj[key]['files']) + return true + } + if (typeof obj[key] === 'function' && value) { + obj[key] = obj[key](Array.isArray(value) ? value : value['files']) + return true + } } })(klona(base), ...defaults.map(klona)) } diff --git a/src/types.ts b/src/types.ts index 8dc9dc29..8cb83667 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,7 @@ type InlineTWConfig = _Omit & { } type BoolObj> = boolean | Partial +type Arrayable = T | T[] export type ViewerConfig = { /** @@ -35,6 +36,7 @@ export type ExposeConfig = { * Import name for the configuration * * @default '#tailwind-config' + * @deprecated use `alias` in `nuxt.config` instead - https://nuxt.com/docs/api/nuxt-config#alias */ alias: string /** @@ -45,6 +47,8 @@ export type ExposeConfig = { level: number /** * To write the templates to file-system for usage with code that does not have access to the Virtual File System. This applies only for Nuxt 3 with Vite. + * + * @deprecated use a module if a necessary using the `app:templates` hook to write templates like so: https://github.com/nuxt/module-builder/blob/4697f18429efb83b82f3b256dd8926bb94d3df77/src/commands/prepare.ts#L37-L43 */ write?: boolean } @@ -72,9 +76,10 @@ export interface ModuleOptions { /** * The path of the Tailwind configuration file. The extension can be omitted, in which case it will try to find a `.js`, `.cjs`, `.mjs`, or `.ts` file. * - * @default 'tailwind.config' + * @default [] + * @deprecated provide string in `config` */ - configPath: string | string[] + configPath: Arrayable /** * The path of the Tailwind CSS file. If the file does not exist, the module's default CSS file will be imported instead. * @@ -82,11 +87,11 @@ export interface ModuleOptions { */ cssPath: string | false | [string | false, { injectPosition: InjectPosition }] /** - * Configuration for Tailwind CSS + * Configuration for Tailwind CSS. Accepts (array of) string and inline configurations. * * for default, see https://tailwindcss.nuxtjs.org/tailwind/config */ - config: InlineTWConfig + config: Arrayable /** * [tailwind-config-viewer](https://github.com/rogden/tailwind-config-viewer) usage *in development* * @@ -99,33 +104,12 @@ export interface ModuleOptions { * @default false // if true, { alias: '#tailwind-config', level: 2 } */ exposeConfig: BoolObj - /** - * Deeper references within configuration for optimal tree-shaking. - * - * @default 2 - * @deprecated use exposeConfig as object - */ - exposeLevel?: number - /** - * The position of CSS injection affecting CSS priority - * - * @default 'first' - * @deprecated use cssPath as [string | false, { injectPosition: InjectPosition }] - */ - injectPosition?: InjectPosition /** * Suppress logging to the console when everything is ok * * @default nuxt.options.logLevel === 'silent' */ quiet: boolean - /** - * Add util to write Tailwind CSS classes inside strings with `` tw`{classes}` `` - * - * @default false - * @deprecated use `editorSupport.autocompleteUtil` as object - */ - addTwUtil?: boolean /** * Enable some utilities for better editor support and DX. * @@ -146,6 +130,8 @@ export interface ModuleOptions { disableHMR?: boolean } +// Hooks TODO: either deprecate or make hooks read-only. Modifications from hooks should rather be done by addition of new configs with defuFn strategy. + export interface ModuleHooks { /** * Passes any Tailwind configuration read by the module for each (extended) [layer](https://nuxt.com/docs/getting-started/layers) and [path](https://tailwindcss.nuxtjs.org/getting-started/options#configpath) before merging all of them. diff --git a/src/viewer.ts b/src/viewer.ts index 33585681..71afb0e2 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -2,17 +2,19 @@ import { colors } from 'consola/utils' import { eventHandler, sendRedirect, H3Event } from 'h3' import { addDevServerHandler, isNuxtMajorVersion, useNuxt } from '@nuxt/kit' import { withTrailingSlash, withoutTrailingSlash, joinURL, cleanDoubleSlashes } from 'ufo' -import loadConfig from 'tailwindcss/loadConfig.js' import { relative } from 'pathe' import logger from './logger' import type { TWConfig, ViewerConfig } from './types' -export const setupViewer = async (twConfig: string | TWConfig, config: ViewerConfig, nuxt = useNuxt()) => { +export const setupViewer = async (twConfig: string | Partial, config: ViewerConfig, nuxt = useNuxt()) => { const route = joinURL(nuxt.options.app?.baseURL, config.endpoint) const [routeWithSlash, routeWithoutSlash] = [withTrailingSlash(route), withoutTrailingSlash(route)] - // @ts-expect-error untyped package export - const viewerServer = (await import('tailwind-config-viewer/server/index.js').then(r => r.default || r))({ tailwindConfigProvider: typeof twConfig === 'string' ? () => loadConfig(twConfig) : () => twConfig }).asMiddleware() + const viewerServer = await Promise.all([ + // @ts-expect-error untyped package export + import('tailwind-config-viewer/server/index.js').then(r => r.default || r), + typeof twConfig === 'string' ? import('tailwindcss/loadConfig.js').then(r => r.default || r).then(loadConfig => () => loadConfig(twConfig)) : () => twConfig, + ]).then(([server, tailwindConfigProvider]) => server({ tailwindConfigProvider }).asMiddleware()) const viewerDevMiddleware = eventHandler(event => viewerServer(event.node?.req || event.req, event.node?.res || event.res)) if (!isNuxtMajorVersion(2, nuxt)) { @@ -41,6 +43,16 @@ export const setupViewer = async (twConfig: string | TWConfig, config: ViewerCon ) } + nuxt.hook('devtools:customTabs', (tabs: import('@nuxt/devtools').ModuleOptions['customTabs']) => { + tabs?.push({ + title: 'Tailwind CSS', + name: 'tailwindcss', + icon: 'logos-tailwindcss-icon', + category: 'modules', + view: { type: 'iframe', src: route }, + }) + }) + nuxt.hook('listen', (_, listener) => { const viewerUrl = cleanDoubleSlashes(joinURL(listener.url, config.endpoint)) logger.info(`Tailwind Viewer: ${colors.underline(colors.yellow(withTrailingSlash(viewerUrl)))}`) @@ -55,8 +67,6 @@ export const exportViewer = async (twConfig: string, config: ViewerConfig, nuxt const cli = await import('tailwind-config-viewer/cli/export.js').then(r => r.default || r) as any nuxt.hook('nitro:build:public-assets', (nitro) => { - // nitro.options.prerender.ignore.push(config.endpoint); - const dir = joinURL(nitro.options.output.publicDir, config.endpoint) cli(dir, twConfig) logger.success(`Exported viewer to ${colors.yellow(relative(nuxt.options.srcDir, dir))}`) diff --git a/test/configs.test.ts b/test/configs.test.ts index 629d0ec8..50ddc141 100644 --- a/test/configs.test.ts +++ b/test/configs.test.ts @@ -64,7 +64,7 @@ describe('tailwindcss module configs', async () => { }) test('js config file is loaded and merged', () => { - // set from ts-tailwind.config.ts + // set from alt-tailwind.config.js expect(getVfsFile('test-tailwind.config.mjs')).contains('"javascriptYellow": "#f1e05a"') }) @@ -73,16 +73,16 @@ describe('tailwindcss module configs', async () => { expect(contentFiles.find(c => /my-pluggable-modules|my-modular-plugins/.test(c))).toBeDefined() expect(contentFiles.filter(c => c.includes('my-imports-dir')).length).toBe(2) - expect(contentFiles.find(c => c.includes('components/**/*'))?.includes('json,mdc,mdx,coffee')).toBeTruthy() + expect(contentFiles.find(c => c.includes('/components/**/*'))?.includes('json,mdc,mdx,coffee')).toBeTruthy() }) test('content is overridden', () => { // set from override-tailwind.config.ts const { content: { files: contentFiles } } = destr(getVfsFile('test-tailwind.config.mjs')!.replace(/^export default /, '')) - expect(contentFiles[1]).toBe('./custom-theme/**/*.vue') + expect(contentFiles).includes('./custom-theme/**/*.vue') expect(contentFiles.filter(c => /\{[AE],[ae]\}/.test(c)).length).toBe(0) - expect([...contentFiles].pop()).toBe('my-custom-content') + expect(contentFiles).includes('my-custom-content') }) test('content merges with objects', () => { diff --git a/test/fixtures/basic/alt-tailwind.config.js b/test/fixtures/basic/alt-tailwind.config.js index 9a2afd99..abd8c280 100644 --- a/test/fixtures/basic/alt-tailwind.config.js +++ b/test/fixtures/basic/alt-tailwind.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { theme: { extend: { colors: { diff --git a/test/fixtures/basic/override-tailwind.config.js b/test/fixtures/basic/override-tailwind.config.js index c01797ae..f463f764 100644 --- a/test/fixtures/basic/override-tailwind.config.js +++ b/test/fixtures/basic/override-tailwind.config.js @@ -2,7 +2,7 @@ export default { content: contentDefaults => [ contentDefaults?.[0], './custom-theme/**/*.vue', - ...(contentDefaults || []).filter(c => !/\{[AE],[ae]\}/.test(c)), + ...(contentDefaults || []).filter(c => (c.includes('coffee') || c.includes('my-')) && !/\{[AE],[ae]\}/.test(c)), ], theme: { extend: { diff --git a/test/fixtures/basic/tailwind.config.js b/test/fixtures/basic/tailwind.config.js index 09c0ace5..15412fba 100644 --- a/test/fixtures/basic/tailwind.config.js +++ b/test/fixtures/basic/tailwind.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { content: [ 'content/**/*.md', ],