-
Notifications
You must be signed in to change notification settings - Fork 181
/
module.ts
249 lines (218 loc) · 8.45 KB
/
module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import { existsSync } from 'fs'
import { join, relative } from 'pathe'
import defu, { defuArrayFn } from 'defu'
import chalk from 'chalk'
import consola from 'consola'
import {
defineNuxtModule,
installModule,
addTemplate,
addDevServerHandler,
isNuxt2,
createResolver,
resolvePath,
addVitePlugin,
isNuxt3, findPath, requireModule
} from '@nuxt/kit'
import { Config } from 'tailwindcss'
import { name, version } from '../package.json'
import vitePlugin from './hmr'
import defaultTailwindConfig from './tailwind.config'
import { InjectPosition, resolveInjectPosition } from './utils'
const logger = consola.withScope('nuxt:tailwindcss')
const layerPaths = (srcDir: string) => ([
`${srcDir}/components/**/*.{vue,js,ts}`,
`${srcDir}/layouts/**/*.vue`,
`${srcDir}/pages/**/*.vue`,
`${srcDir}/composables/**/*.{js,ts}`,
`${srcDir}/plugins/**/*.{js,ts}`,
`${srcDir}/App.{js,ts,vue}`,
`${srcDir}/app.{js,ts,vue}`,
`${srcDir}/Error.{js,ts,vue}`,
`${srcDir}/error.{js,ts,vue}`
])
export interface ModuleHooks {
'tailwindcss:config': (tailwindConfig: any) => void
}
type Arrayable<T> = T | T[]
export interface ModuleOptions {
configPath: Arrayable<string>;
cssPath: string;
config: Config;
viewer: boolean;
exposeConfig: boolean;
injectPosition: InjectPosition;
disableHmrHotfix: boolean;
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name,
version,
configKey: 'tailwindcss'
},
defaults: nuxt => ({
configPath: 'tailwind.config',
cssPath: join(nuxt.options.dir.assets, 'css/tailwind.css'),
config: defaultTailwindConfig(),
viewer: true,
exposeConfig: false,
injectPosition: 'first',
disableHmrHotfix: false
}),
async setup (moduleOptions, nuxt) {
/**
* Config file handling
*/
const configPaths = []
const contentPaths = []
/**
* Push config paths into `configPaths` without extension.
* Allows next steps of processing to try both .js / .ts when resolving the config.
*/
const addConfigPath = async (path: Arrayable<string>) => {
// filter in case an empty path is provided
const paths = (Array.isArray(path) ? path : [path]).filter(Boolean)
for (const path of paths) {
const resolvedPath = (await findPath(path, { extensions: ['.js', '.mjs', '.ts'] }, 'file'))
// only if the path is found
if (resolvedPath) {
configPaths.push(resolvedPath)
}
}
}
// Support `extends` directories
if (nuxt.options._layers && nuxt.options._layers.length > 1) {
interface NuxtLayer {
config: any
configFile: string
cwd: string
}
for (const layer of (nuxt.options._layers as NuxtLayer[])) {
await addConfigPath(layer?.config?.tailwindcss?.configPath || join(layer.cwd, 'tailwind.config'))
contentPaths.push(...layerPaths(layer.cwd))
}
} else {
await addConfigPath(moduleOptions.configPath)
contentPaths.push(...layerPaths(nuxt.options.srcDir))
}
// Watch the Tailwind config file to restart the server
if (nuxt.options.dev) {
configPaths.forEach(path => nuxt.options.watch.push(path))
}
// Recursively resolve each config and merge tailwind configs together.
let tailwindConfig: any = {}
for (const configPath of configPaths) {
let _tailwindConfig
try {
_tailwindConfig = requireModule(configPath, { clearCache: true })
} catch (e) {
logger.warn(`Failed to load Tailwind config at: \`./${relative(nuxt.options.rootDir, configPath)}\``, e)
}
// Transform purge option from Array to object with { content }
if (_tailwindConfig && Array.isArray(_tailwindConfig.purge)) {
_tailwindConfig.content = _tailwindConfig.purge
}
tailwindConfig = defu(_tailwindConfig || {}, tailwindConfig)
}
tailwindConfig.content = [...(tailwindConfig.content || []), ...contentPaths]
// Merge with our default purgecss default
tailwindConfig = defuArrayFn(tailwindConfig, moduleOptions.config)
// Expose resolved tailwind config as an alias
// https://tailwindcss.com/docs/configuration/#referencing-in-javascript
if (moduleOptions.exposeConfig) {
const resolveConfig: any = await import('tailwindcss/resolveConfig.js').then(r => r.default || r)
const resolvedConfig = resolveConfig(tailwindConfig)
const template = addTemplate({
filename: 'tailwind.config.mjs',
getContents: () => `export default ${JSON.stringify(resolvedConfig, null, 2)}`
})
addTemplate({
filename: 'tailwind.config.d.ts',
getContents: () => 'declare const config: import("tailwindcss").Config\nexport { config as default }',
write: true
})
nuxt.options.alias['#tailwind-config'] = template.dst
}
// Allow extending tailwindcss config by other modules
// @ts-ignore
await nuxt.callHook('tailwindcss:config', tailwindConfig)
// Compute tailwindConfig hash
tailwindConfig._hash = String(Date.now())
/**
* CSS file handling
*/
const cssPath = await resolvePath(moduleOptions.cssPath, { extensions: ['.css', '.sass', '.scss', '.less', '.styl'] })
// Include CSS file in project css
let resolvedCss: string
if (typeof cssPath === 'string') {
if (existsSync(cssPath)) {
logger.info(`Using Tailwind CSS from ~/${relative(nuxt.options.srcDir, cssPath)}`)
resolvedCss = cssPath
} else {
logger.info('Using default Tailwind CSS file from runtime/tailwind.css')
// @ts-ignore
resolvedCss = createResolver(import.meta.url).resolve('runtime/tailwind.css')
}
}
nuxt.options.css = nuxt.options.css ?? []
const resolvedNuxtCss = await Promise.all(nuxt.options.css.map(p => resolvePath(p)))
// Inject only if this file isn't listed already by user (e.g. user may put custom path both here and in css):
if (!resolvedNuxtCss.includes(resolvedCss)) {
let injectPosition: number
try {
injectPosition = resolveInjectPosition(nuxt.options.css, moduleOptions.injectPosition)
} catch (e) {
throw new Error('failed to resolve Tailwind CSS injection position: ' + e.message)
}
nuxt.options.css.splice(injectPosition, 0, resolvedCss)
}
/**
* PostCSS 8 support for Nuxt 2
*/
// Setup postcss plugins
// https://tailwindcss.com/docs/using-with-preprocessors#future-css-features
const postcssOptions =
nuxt.options.postcss || /* nuxt 3 */
nuxt.options.build.postcss.postcssOptions || /* older nuxt3 */
nuxt.options.build.postcss as any
postcssOptions.plugins = postcssOptions.plugins || {}
postcssOptions.plugins['tailwindcss/nesting'] = postcssOptions.plugins['tailwindcss/nesting'] ?? {}
postcssOptions.plugins['postcss-custom-properties'] = postcssOptions.plugins['postcss-custom-properties'] ?? {}
postcssOptions.plugins.tailwindcss = tailwindConfig
if (isNuxt2()) {
await installModule('@nuxt/postcss8')
}
/**
* Vite HMR support
*/
if (nuxt.options.dev && !moduleOptions.disableHmrHotfix) {
// Insert Vite plugin to work around HMR issue
addVitePlugin(vitePlugin(tailwindConfig, nuxt.options.rootDir, resolvedCss))
}
/**
* Viewer
*/
// Add _tailwind config viewer endpoint
// TODO: Fix `addServerHandler` on Nuxt 2 w/o Bridge
if (nuxt.options.dev && moduleOptions.viewer) {
const route = '/_tailwind'
const createServer = await import('tailwind-config-viewer/server/index.js').then(r => r.default || r) as any
const { withTrailingSlash, withoutTrailingSlash } = await import('ufo')
const routerPrefix = isNuxt3() ? route : undefined
const _viewerDevMiddleware = createServer({ tailwindConfigProvider: () => tailwindConfig, routerPrefix }).asMiddleware()
const viewerDevMiddleware = (req, res) => {
if (req.originalUrl === withoutTrailingSlash(route)) {
res.writeHead(301, { Location: withTrailingSlash(req.originalUrl) })
return res.end()
}
_viewerDevMiddleware(req, res)
}
if (isNuxt3()) { addDevServerHandler({ route, handler: viewerDevMiddleware }) }
if (isNuxt2()) { nuxt.options.serverMiddleware.push({ route, handler: viewerDevMiddleware }) }
nuxt.hook('listen', (_, listener) => {
const viewerUrl = `${withoutTrailingSlash(listener.url)}${route}`
logger.info(`Tailwind Viewer: ${chalk.underline.yellow(withTrailingSlash(viewerUrl))}`)
})
}
}
})