Skip to content

Commit 7fbee8a

Browse files
committed
feat(module): externalize format ; improve module stability
1 parent 234d112 commit 7fbee8a

File tree

3 files changed

+233
-185
lines changed

3 files changed

+233
-185
lines changed

src/formats.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
const DesignTokenType =
2+
`interface DesignToken {
3+
/* The raw value you specified in your token declaration. */
4+
value: any;
5+
/* CSS Variable reference that gets generated. */
6+
variable: string;
7+
name?: string;
8+
comment?: string;
9+
themeable?: boolean;
10+
attributes?: {
11+
category?: string;
12+
type?: string;
13+
item?: string;
14+
subitem?: string;
15+
state?: string;
16+
[key: string]: any;
17+
};
18+
[key: string]: any;
19+
}`
20+
21+
const DesignTokensType =
22+
`interface DesignTokens {
23+
[key: string]: DesignTokens | DesignToken;
24+
}`
25+
26+
export const treeWalker = (obj, typing: boolean = true) => {
27+
let type = Object.create(null)
28+
29+
const has = Object.prototype.hasOwnProperty.bind(obj)
30+
31+
if (has('value')) {
32+
// Transform name to CSS variable name
33+
obj.variable = `var(--${obj.name})`
34+
35+
// Toggle between type declaration and value
36+
type = typing ? 'DesignToken' : obj
37+
} else {
38+
for (const k in obj) {
39+
if (has(k)) {
40+
switch (typeof obj[k]) {
41+
case 'object':
42+
type[k] = treeWalker(obj[k], typing)
43+
}
44+
}
45+
}
46+
}
47+
48+
return type
49+
}
50+
51+
/**
52+
* Make a list of `get()` compatible paths for any object
53+
*/
54+
export const objectPaths = (data: any) => {
55+
const output: any = []
56+
function step (obj: any, prev?: string) {
57+
Object.keys(obj).forEach((key) => {
58+
const value = obj[key]
59+
const isarray = Array.isArray(value)
60+
const type = Object.prototype.toString.call(value)
61+
const isobject =
62+
type === '[object Object]' ||
63+
type === '[object Array]'
64+
65+
const newKey = prev
66+
? `${prev}.${key}`
67+
: key
68+
69+
if (!output.includes(newKey)) { output.push(newKey) }
70+
71+
if (!isarray && isobject && Object.keys(value).length) { return step(value, newKey) }
72+
})
73+
}
74+
step(data)
75+
return output
76+
}
77+
78+
/**
79+
* Formats
80+
*/
81+
82+
export const tsTypesDeclaration = ({ tokens }) => {
83+
const tokensObject = treeWalker(tokens)
84+
85+
let result = 'import type { Ref } from \'vue\'\n\n'
86+
87+
result = result + `export ${DesignTokenType}\n\n`
88+
89+
result = result + `export ${DesignTokensType}\n\n`
90+
91+
result = result + `export interface NuxtDesignTokens extends DesignTokens ${JSON.stringify(tokensObject, null, 2)}\n\n`
92+
93+
const tokensPaths = objectPaths(tokensObject)
94+
95+
if (tokensPaths.length) {
96+
result = result + `export type DesignTokensPaths = ${tokensPaths.map(path => (`'${path}'`)).join(' | \n')}\n\n`
97+
} else {
98+
result = result + 'export type DesignTokensPaths = \'no.tokens\'\n\n'
99+
}
100+
101+
result = result.replace(/"DesignToken"/g, 'DesignToken')
102+
103+
return result
104+
}
105+
106+
export const tsFull = ({ tokens }) => {
107+
let result = 'import get from \'lodash.get\'\n\n'
108+
109+
result = result + 'import type { NuxtDesignTokens, DesignTokensPaths, DesignToken } from \'./types.d\'\n\n'
110+
111+
result = result + 'export * from \'./types.d\'\n\n'
112+
113+
result = result + `export const designTokens: NuxtDesignTokens = ${JSON.stringify(treeWalker(tokens, false), null, 2)}\n`
114+
115+
result = result + `\n
116+
/**
117+
* Get a theme token by its path
118+
*/
119+
export const $tokens = (path: DesignTokensPaths, key: keyof DesignToken = 'variable', flatten: boolean = true) => {
120+
const token = get(designTokens, path)
121+
122+
if (key && token?.[key]) { return token[key] }
123+
124+
if (key && flatten && typeof token === 'object') {
125+
const flattened = {}
126+
127+
const flatten = (obj) => {
128+
Object.entries(obj).forEach(([objectKey, value]) => {
129+
if (value[key]) {
130+
flattened[objectKey] = value[key]
131+
return
132+
}
133+
134+
flatten(value)
135+
})
136+
}
137+
138+
flatten(token)
139+
140+
return flattened
141+
}
142+
143+
return token
144+
}\n\n`
145+
146+
result = result + 'export const $dt = $tokens\n\n'
147+
148+
return result
149+
}
150+
151+
export const jsFull = ({ tokens }) => {
152+
let result = 'import get from \'lodash.get\'\n\n'
153+
154+
result = result + `export const designTokens = ${JSON.stringify(treeWalker(tokens, false), null, 2)}\n`
155+
156+
result = result +
157+
`\n
158+
/**
159+
* Get a theme token by its path
160+
* @typedef {import('tokens-types').DesignTokensPaths} DesignTokensPaths
161+
* @typedef {import('tokens-types').DesignToken} DesignToken
162+
* @param {DesignTokensPaths} path The path to the theme token
163+
* @param {keyof DesignToken} variable Returns the variable if exists if true
164+
* @param {boolean} flatten If the path gives an object, returns a deeply flattened object with "key" used as values.
165+
*/
166+
export const $tokens = (path, key = 'variable', flatten: boolean = true) => {
167+
const token = get(designTokens, path)
168+
169+
if (key && token?.[key]) { return token[key] }
170+
171+
if (key && flatten && typeof token === 'object') {
172+
const flattened = {}
173+
174+
const flatten = (obj) => {
175+
Object.entries(obj).forEach(([objectKey, value]) => {
176+
if (value[key]) {
177+
flattened[objectKey] = value[key]
178+
return
179+
}
180+
181+
flatten(value)
182+
})
183+
}
184+
185+
flatten(token)
186+
187+
return flattened
188+
}
189+
190+
return token
191+
}\n\n`
192+
193+
result = result + 'export const $to = $tokens\n\n'
194+
195+
return result
196+
}

src/module.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ export default defineNuxtModule<ModuleOptions>({
3737
// `.nuxt/theme` resolver
3838
const tokensDir = withTrailingSlash(nuxt.options.buildDir + '/tokens')
3939

40-
// Refresh theme function
41-
const refreshTokens = async (nitro?: Nitro) => {
42-
await createTokensDir(tokensDir)
40+
// Init directory
41+
await createTokensDir(tokensDir)
4342

43+
// Refresh configuration
44+
const refreshTokens = async (nitro?: Nitro) => {
4445
// Resolve theme configuration from every layer
4546
const { tokensFilePaths, tokens } = resolveTokens(layers as NuxtLayer[])
4647

@@ -71,14 +72,15 @@ export default defineNuxtModule<ModuleOptions>({
7172
nuxt.options.build.transpile = nuxt.options.build.transpile || []
7273
nuxt.options.build.transpile.push(resolveRuntime('./runtime'))
7374

74-
// Set buildTokens to real function as the feature is enabled
75+
// Main function to build tokens (module-level)
7576
const buildTokens = async (nitro) => {
7677
try {
7778
const start = performance.now()
7879
const tokens = await nitro.storage.getItem('cache:design-tokens:tokens.json') as NuxtDesignTokens
7980
await generateTokens(tokens, tokensDir)
8081
const end = performance.now()
81-
logger.success(`Design Tokens built in ${start - end}ms`)
82+
const ms = Math.round(end - start)
83+
logger.success(`Design Tokens built${ms ? ' in ' + ms + 'ms' : ''}`)
8284
} catch (e) {
8385
logger.error('Could not build tokens!')
8486
logger.error(e.message)

0 commit comments

Comments
 (0)