Skip to content

Commit e88c23a

Browse files
ineshboseatinux
andauthored
feat(exposeConfig): nested properties (#583)
* refactor(expose-config): recursively iterating over config * chore: type definitions and a bugfix * bugfix: escaping quotes and special characters * refactor: using templates now * feat: exports depending on exposeLevel * chore: set start level to 1 * chore: updated docs and added test * chore: more docs, and reverting d.ts file * fix: passing write prop to test setup Co-authored-by: Sébastien Chopin <[email protected]>
1 parent 7da555c commit e88c23a

File tree

4 files changed

+86
-6
lines changed

4 files changed

+86
-6
lines changed

docs/content/1.getting-started/2.options.md

+29
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,35 @@ export default {
9090

9191
Learn more about it in the [Referencing in the application](/tailwind/config#referencing-in-the-application) section.
9292

93+
## `exposeLevel`
94+
95+
- Default: `2`
96+
97+
If you want to only import *really* specific parts of your tailwind config, you can enable imports for each property in the config:
98+
99+
```ts [nuxt.config]
100+
export default {
101+
tailwindcss: {
102+
exposeConfig: true,
103+
exposeLevel: 3
104+
}
105+
}
106+
```
107+
108+
This is only relevant when [`exposeConfig`](/getting-started/options#exposeconfig) is `true`. Using `exposeLevel` to be ≤ 0 will only expose root properties.
109+
110+
::alert{type="warning"}
111+
112+
It is unlikely for `exposeLevel` to ever be over 4 - the usual depth of a Tailwind config. A higher value is also likely to increase boot-time and disk space in dev.
113+
114+
::
115+
116+
::alert{type="info"}
117+
118+
Named exports for properties below [root options](https://tailwindcss.com/docs/configuration#configuration-options) are prefixed with `_` (`_colors`, `_900`, `_2xl`) to ensure safe variable names. You can use default imports to provide any identifier or rename named imports using `as`. Properties with unsafe variable names (`spacing['1.5']`, `height['1/2']`, `keyframes.ping['75%, 100%']`) do not get exported individually.
119+
120+
::
121+
93122
## `injectPosition`
94123

95124
- Default: `'first'`

docs/content/2.tailwind/1.config.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,15 @@ This merging strategy of with a function only applies to `plugins` and `content`
199199

200200
::
201201

202-
### Whitelisting classes
202+
### Safelisting classes
203203

204-
If you need to whitelist classes and avoid the content purge system, you need to specify the `safelist` option:
204+
If you need to safelist classes and avoid the content purge system, you need to specify the `safelist` option:
205205

206206
```js{}[tailwind.config.js]
207207
module.exports = {
208-
// Whitelisting some classes to avoid content purge
208+
// Safelisting some classes to avoid content purge
209209
safelist: [
210-
'whitelisted',
210+
'safelisted',
211211
{
212212
pattern: /bg-(red|green|blue)-(100|200|300)/,
213213
},
@@ -224,7 +224,8 @@ If you need resolved Tailwind config at runtime, you can enable the [exposeConfi
224224
```js{}[nuxt.config]
225225
export default {
226226
tailwindcss: {
227-
exposeConfig: true
227+
exposeConfig: true,
228+
// exposeLevel: 1, // determines tree-shaking (optional)
228229
}
229230
}
230231
```
@@ -237,6 +238,11 @@ import tailwindConfig from '#tailwind-config'
237238

238239
// Import only part which is required to allow tree-shaking
239240
import { theme } from '#tailwind-config'
241+
242+
// Import within properties for further tree-shaking (based on exposeLevel)
243+
import screens from '#tailwind-config/theme/screens' // default import
244+
import { _neutral } from '#tailwind-config/theme/colors' // named (with _ prefix)
245+
import { _800 as slate800 } from '#tailwind-config/theme/colors/slate' // alias
240246
```
241247

242248
::alert{type="warning"}

src/module.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface ModuleOptions {
4747
config: Config;
4848
viewer: boolean;
4949
exposeConfig: boolean;
50+
exposeLevel: number;
5051
injectPosition: InjectPosition;
5152
disableHmrHotfix: boolean;
5253
}
@@ -63,6 +64,7 @@ export default defineNuxtModule<ModuleOptions>({
6364
config: defaultTailwindConfig(),
6465
viewer: true,
6566
exposeConfig: false,
67+
exposeLevel: 2,
6668
injectPosition: 'first',
6769
disableHmrHotfix: false
6870
}),
@@ -149,10 +151,45 @@ export default defineNuxtModule<ModuleOptions>({
149151
// Expose resolved tailwind config as an alias
150152
// https://tailwindcss.com/docs/configuration/#referencing-in-javascript
151153
if (moduleOptions.exposeConfig) {
154+
/**
155+
* Creates MJS exports for properties of the config
156+
*
157+
* @param obj config
158+
* @param path parent properties trace
159+
* @param level level of object depth
160+
* @param maxLevel maximum level of depth
161+
*/
162+
const populateMap = (obj: any, path: string[] = [], level = 1, maxLevel = moduleOptions.exposeLevel) => {
163+
Object.entries(obj).forEach(([key, value = {}]) => {
164+
if (
165+
level >= maxLevel || // if recursive call is more than desired
166+
typeof value !== 'object' || // if its not an object, no more recursion required
167+
Array.isArray(value) || // arrays are objects in JS, but we can't break it down
168+
Object.keys(value).find(k => !k.match(/^[0-9a-z]+$/i)) // object has non-alphanumeric property (unsafe var name)
169+
) {
170+
addTemplate({
171+
filename: `tailwind.config/${path.concat(key).join('/')}.mjs`,
172+
getContents: () => `export default ${JSON.stringify(value, null, 2)}`
173+
})
174+
} else {
175+
// recurse through nested objects
176+
populateMap(value, path.concat(key), level + 1, maxLevel)
177+
178+
const values = Object.keys(value)
179+
addTemplate({
180+
filename: `tailwind.config/${path.concat(key).join('/')}.mjs`,
181+
getContents: () => `${Object.keys(value).map(v => `import _${v} from "./${key}/${v}.mjs"`).join('\n')}\nconst config = { ${values.map(k => `"${k}": _${k}`).join(', ')} }\nexport { config as default${values.length > 0 ? ', _' : ''}${values.join(', _')} }`
182+
})
183+
}
184+
})
185+
}
186+
187+
populateMap(resolvedConfig)
188+
152189
const configOptions = Object.keys(resolvedConfig)
153190
const template = addTemplate({
154191
filename: 'tailwind.config.mjs',
155-
getContents: () => `${Object.entries(resolvedConfig).map(([k, v]) => `export const ${k} = ${JSON.stringify(v, null, 2)}`).join('\n')}\nexport default { ${configOptions.join(', ')} }`
192+
getContents: () => `${configOptions.map(v => `import ${v} from "./tailwind.config/${v}.mjs"`).join('\n')}\nconst config = { ${configOptions.join(', ')} }\nexport { config as default, ${configOptions.join(', ')} }`
156193
})
157194
addTemplate({
158195
filename: 'tailwind.config.d.ts',

test/basic.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('tailwindcss module', async () => {
1313

1414
await setupNuxtTailwind({
1515
exposeConfig: true,
16+
exposeLevel: 2,
1617
cssPath: r('tailwind.css')
1718
})
1819

@@ -53,4 +54,11 @@ describe('tailwindcss module', async () => {
5354
// expect(logger.info).toHaveBeenNthCalledWith(2, `Merging Tailwind config from ~/${relative(rootDir, nuxt.options.tailwindcss.configPath)}`)
5455
// })
5556
//
57+
58+
test('expose config', () => {
59+
const nuxt = useTestContext().nuxt
60+
const vfsKey = Object.keys(nuxt.vfs).find(k => k.includes('tailwind.config/theme/animation.'))
61+
// check default tailwind default animation exists
62+
expect(nuxt.vfs[vfsKey]).contains('"pulse": "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite"')
63+
})
5664
})

0 commit comments

Comments
 (0)