@woodsportal/hubspot-kit 5.0.0-beta.0 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/scripts/tailwind/prefix-plugin.js +131 -182
- package/scripts/tailwind/prefix-plugin.test.mjs +110 -0
- package/scripts/tailwind/tailwind-content.js +29 -28
- package/scripts/vite/create-dev-server-config.mjs +14 -3
- package/scripts/vite/portal-build-shared.js +5 -3
- package/src/dev-module-config/__tests__/build-api-routes-custom-menu.test.ts +32 -0
- package/src/dev-module-config/__tests__/custom-menu.test.ts +77 -0
- package/src/dev-module-config/__tests__/drill-down-panels.test.ts +65 -0
- package/src/dev-module-config/__tests__/fixtures/golden-module.fixture.ts +2 -0
- package/src/dev-module-config/__tests__/flatten-visible-fields.test.ts +34 -1
- package/src/dev-module-config/__tests__/module-config-transform.test.ts +27 -0
- package/src/dev-module-config/runtime/module-config-runtime.ts +3 -1
- package/src/dev-module-config/schema/custom-menu.ts +124 -0
- package/src/dev-module-config/schema/validate-module-config.ts +2 -0
- package/src/dev-module-config/transform/build-hubspot-data.ts +14 -0
- package/src/dev-module-config/ui/FieldRenderer.tsx +2 -0
- package/src/dev-module-config/ui/ModuleConfigAnimatedPresence.tsx +4 -2
- package/src/dev-module-config/ui/ModuleConfigShell.tsx +16 -4
- package/src/dev-module-config/ui/editors/MenuOptionsEditor.tsx +34 -0
- package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +16 -11
- package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +27 -23
- package/src/dev-module-config/ui/navigation/drill-down-panels.ts +48 -0
- package/src/routing/build-api-routes.ts +45 -15
- package/src/routing/custom-menu-path.ts +14 -0
- package/templates/module-hybrid-cdn/src/assets/css/main.prod.css +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### CSS prefix
|
|
6
|
+
- **Auto-prefix** — class string bindings ending in `_CLASS` (e.g. `CARDS_GRID_CLASS`) are prefixed the same as `*DynamicClassName*`
|
|
7
|
+
- **Auto-prefix** — JSX props ending in `ClassName` (`containerClassName`, `pageLinkClassName`, …) and helpers ending in `Class` (`paginationItemBaseClass`, `navBtnClass`) are prefixed
|
|
8
|
+
|
|
3
9
|
## [1.0.44] - 2026-08-06
|
|
4
10
|
|
|
5
11
|
### Portal auth scaffold
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woodsportal/hubspot-kit",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.2",
|
|
4
4
|
"packageManager": "pnpm@10.12.1",
|
|
5
5
|
"description": "WoodsCLI — HubSpot CMS dev kit by WoodsPortal. wp CLI, Vite presets, module-config overlay, and theme/module build pipelines.",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"build": "tsup",
|
|
53
53
|
"dev": "tsup --watch",
|
|
54
54
|
"typecheck": "tsc --noEmit",
|
|
55
|
-
"test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/cli/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' 'scripts/portal-build-id.test.mjs' 'scripts/resolve-consumer-dep.test.mjs' 'scripts/wp-cli-smoke.test.mjs' 'scripts/vite/ensure-dev-kit-shims.test.mjs' 'scripts/vite/consumer-react-aliases.test.mjs' 'scripts/vite/resolve-from-project-root.test.mjs' && vitest run src/dev-module-config/__tests__/normalize-repeater-items.test.ts src/dev-module-config/__tests__/module-config-transform.test.ts src/dev-module-config/__tests__/flatten-visible-fields.test.ts",
|
|
55
|
+
"test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/cli/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' 'scripts/portal-build-id.test.mjs' 'scripts/resolve-consumer-dep.test.mjs' 'scripts/wp-cli-smoke.test.mjs' 'scripts/vite/ensure-dev-kit-shims.test.mjs' 'scripts/vite/consumer-react-aliases.test.mjs' 'scripts/vite/resolve-from-project-root.test.mjs' 'scripts/tailwind/prefix-plugin.test.mjs' && vitest run src/dev-module-config/__tests__/normalize-repeater-items.test.ts src/dev-module-config/__tests__/module-config-transform.test.ts src/dev-module-config/__tests__/flatten-visible-fields.test.ts src/dev-module-config/__tests__/drill-down-panels.test.ts",
|
|
56
56
|
"test:examples": "node examples/smoke/run-matrix.mjs --tier static",
|
|
57
57
|
"test:dist-pack": "node --test scripts/dist-cli-pack-smoke.test.mjs",
|
|
58
58
|
"lint": "eslint src",
|
|
@@ -7,15 +7,49 @@ import { prefix } from './prefix.env.js'
|
|
|
7
7
|
const prefix1 = `${prefix}:`
|
|
8
8
|
const prefix2 = `${prefix}\:`
|
|
9
9
|
|
|
10
|
+
/** Bindings rewritten even when not in `className=`: `*DynamicClassName*`, `_CLASS`, `*ClassName`, `*Class`. */
|
|
11
|
+
export function isClassBindingName(name) {
|
|
12
|
+
if (!name) return false
|
|
13
|
+
return (
|
|
14
|
+
name.includes('DynamicClassName') ||
|
|
15
|
+
name.endsWith('_CLASS') ||
|
|
16
|
+
/(?:ClassName|Class)$/.test(name)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** JSX / object keys: `className`, `containerClassName`, `pageLinkClassName`, … */
|
|
21
|
+
export function isClassAttrName(name) {
|
|
22
|
+
if (!name) return false
|
|
23
|
+
return name === 'className' || name.endsWith('ClassName')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function jsxAttrName(attr) {
|
|
27
|
+
if (t.isJSXIdentifier(attr.name)) return attr.name.name
|
|
28
|
+
return ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function objectKeyName(key) {
|
|
32
|
+
if (t.isIdentifier(key)) return key.name
|
|
33
|
+
if (t.isStringLiteral(key)) return key.value
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Vite serve ids often include `?t=` / `?v=` — strip those before extension checks. */
|
|
38
|
+
function filePathFromId(id) {
|
|
39
|
+
return (id.split('?')[0] ?? id).replaceAll('\\', '/')
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
export default function TailwindPrefixPlugin() {
|
|
11
43
|
return {
|
|
12
44
|
name: 'vite-tailwind-prefix',
|
|
13
45
|
enforce: 'pre',
|
|
14
46
|
transform(code, id) {
|
|
47
|
+
const filename = filePathFromId(id)
|
|
48
|
+
|
|
15
49
|
// Handle CSS files: prefix custom class selectors safely
|
|
16
|
-
if (
|
|
50
|
+
if (filename.endsWith('.css')) {
|
|
17
51
|
// don't need to add prefix to override.style.css
|
|
18
|
-
if (
|
|
52
|
+
if (filename.endsWith('override.style.css')) return code;
|
|
19
53
|
|
|
20
54
|
|
|
21
55
|
const lines = code.split('\n')
|
|
@@ -61,7 +95,7 @@ export default function TailwindPrefixPlugin() {
|
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
|
|
64
|
-
if (!
|
|
98
|
+
if (!filename.endsWith('.tsx') && !filename.endsWith('.ts')) return null
|
|
65
99
|
|
|
66
100
|
try {
|
|
67
101
|
const ast = parse(code, {
|
|
@@ -95,23 +129,84 @@ export default function TailwindPrefixPlugin() {
|
|
|
95
129
|
.join(' ')
|
|
96
130
|
}
|
|
97
131
|
|
|
98
|
-
const
|
|
132
|
+
const prefixExpr = (expr) => {
|
|
133
|
+
if (!expr) return expr
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
t.isTSAsExpression(expr) ||
|
|
137
|
+
t.isTSTypeAssertion(expr) ||
|
|
138
|
+
t.isTSSatisfiesExpression(expr) ||
|
|
139
|
+
t.isTSNonNullExpression(expr) ||
|
|
140
|
+
t.isParenthesizedExpression(expr)
|
|
141
|
+
) {
|
|
142
|
+
expr.expression = prefixExpr(expr.expression)
|
|
143
|
+
return expr
|
|
144
|
+
}
|
|
145
|
+
|
|
99
146
|
if (t.isStringLiteral(expr)) {
|
|
100
147
|
return t.stringLiteral(prefixClasses(expr.value))
|
|
101
148
|
}
|
|
149
|
+
|
|
150
|
+
if (t.isTemplateLiteral(expr)) {
|
|
151
|
+
const quasis = expr.quasis.map((quasi, i) => {
|
|
152
|
+
let classes = prefixClasses(quasi.value.raw)
|
|
153
|
+
if (i < expr.expressions.length) classes += ' '
|
|
154
|
+
if (i > 0 && !classes.startsWith(' ')) classes = ' ' + classes
|
|
155
|
+
return t.templateElement({ raw: classes, cooked: classes })
|
|
156
|
+
})
|
|
157
|
+
const expressions = expr.expressions.map((inner) => prefixExpr(inner))
|
|
158
|
+
return t.templateLiteral(quasis, expressions)
|
|
159
|
+
}
|
|
160
|
+
|
|
102
161
|
if (t.isConditionalExpression(expr)) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
expr.consequent = prefixExpr(expr.consequent)
|
|
163
|
+
expr.alternate = prefixExpr(expr.alternate)
|
|
164
|
+
return expr
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (t.isLogicalExpression(expr)) {
|
|
168
|
+
if (t.isStringLiteral(expr.left) || t.isTemplateLiteral(expr.left)) {
|
|
169
|
+
expr.left = prefixExpr(expr.left)
|
|
170
|
+
}
|
|
171
|
+
expr.right = prefixExpr(expr.right)
|
|
172
|
+
return expr
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (t.isObjectExpression(expr)) {
|
|
176
|
+
expr.properties.forEach((prop) => {
|
|
177
|
+
if (!t.isObjectProperty(prop) || prop.computed) return
|
|
178
|
+
prop.value = prefixExpr(prop.value)
|
|
179
|
+
})
|
|
180
|
+
return expr
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (t.isArrayExpression(expr)) {
|
|
184
|
+
expr.elements = expr.elements.map((el) => {
|
|
185
|
+
if (!el || t.isSpreadElement(el)) return el
|
|
186
|
+
return prefixExpr(el)
|
|
187
|
+
})
|
|
188
|
+
return expr
|
|
108
189
|
}
|
|
190
|
+
|
|
191
|
+
if (t.isCallExpression(expr)) {
|
|
192
|
+
expr.arguments = expr.arguments.map((arg) => {
|
|
193
|
+
if (t.isSpreadElement(arg)) return arg
|
|
194
|
+
return prefixExpr(arg)
|
|
195
|
+
})
|
|
196
|
+
return expr
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
|
|
200
|
+
if (!t.isBlockStatement(expr.body)) {
|
|
201
|
+
expr.body = prefixExpr(expr.body)
|
|
202
|
+
}
|
|
203
|
+
return expr
|
|
204
|
+
}
|
|
205
|
+
|
|
109
206
|
return expr
|
|
110
207
|
}
|
|
111
208
|
|
|
112
209
|
traverse.default(ast, {
|
|
113
|
-
// ✅ Handle variables whose name includes "DynamicClassName"
|
|
114
|
-
|
|
115
210
|
ReturnStatement(path) {
|
|
116
211
|
const fn = path.getFunctionParent()
|
|
117
212
|
if (!fn) return
|
|
@@ -127,186 +222,37 @@ export default function TailwindPrefixPlugin() {
|
|
|
127
222
|
fnName = parent.id.name
|
|
128
223
|
}
|
|
129
224
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!isClassHelper) return
|
|
134
|
-
|
|
135
|
-
if (t.isStringLiteral(path.node.argument)) {
|
|
136
|
-
path.node.argument = t.stringLiteral(prefixClasses(path.node.argument.value))
|
|
137
|
-
} else if (t.isConditionalExpression(path.node.argument)) {
|
|
138
|
-
path.node.argument = prefixStringExpr(path.node.argument)
|
|
225
|
+
if (!isClassBindingName(fnName)) return
|
|
226
|
+
if (path.node.argument) {
|
|
227
|
+
path.node.argument = prefixExpr(path.node.argument)
|
|
139
228
|
}
|
|
140
229
|
},
|
|
141
230
|
|
|
142
231
|
JSXAttribute(path) {
|
|
143
|
-
if (path.node
|
|
144
|
-
|
|
232
|
+
if (!isClassAttrName(jsxAttrName(path.node))) return
|
|
145
233
|
const value = path.node.value
|
|
146
|
-
|
|
147
|
-
// String literal: className="..."
|
|
148
234
|
if (t.isStringLiteral(value)) {
|
|
149
235
|
path.node.value = t.stringLiteral(prefixClasses(value.value))
|
|
236
|
+
return
|
|
150
237
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
t.isJSXExpressionContainer(value) &&
|
|
154
|
-
t.isTemplateLiteral(value.expression)
|
|
155
|
-
) {
|
|
156
|
-
const quasis = value.expression.quasis.map((quasi, i) => {
|
|
157
|
-
let classes = prefixClasses(quasi.value.raw)
|
|
158
|
-
|
|
159
|
-
if (i < value.expression.expressions.length) classes += ' '
|
|
160
|
-
if (i > 0 && !classes.startsWith(' ')) classes = ' ' + classes
|
|
161
|
-
|
|
162
|
-
return t.templateElement({ raw: classes, cooked: classes })
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// 🔥 also fix expressions inside ${ ... }
|
|
166
|
-
const expressions = value.expression.expressions.map((expr) => {
|
|
167
|
-
if (t.isStringLiteral(expr)) {
|
|
168
|
-
return t.stringLiteral(prefixClasses(expr.value))
|
|
169
|
-
}
|
|
170
|
-
if (t.isLogicalExpression(expr)) {
|
|
171
|
-
if (t.isStringLiteral(expr.right)) {
|
|
172
|
-
return t.logicalExpression(
|
|
173
|
-
expr.operator,
|
|
174
|
-
expr.left,
|
|
175
|
-
t.stringLiteral(prefixClasses(expr.right.value)),
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (t.isConditionalExpression(expr)) {
|
|
180
|
-
const newConsequent = t.isStringLiteral(expr.consequent)
|
|
181
|
-
? t.stringLiteral(prefixClasses(expr.consequent.value))
|
|
182
|
-
: expr.consequent
|
|
183
|
-
const newAlternate = t.isStringLiteral(expr.alternate)
|
|
184
|
-
? t.stringLiteral(prefixClasses(expr.alternate.value))
|
|
185
|
-
: expr.alternate
|
|
186
|
-
return t.conditionalExpression(
|
|
187
|
-
expr.test,
|
|
188
|
-
newConsequent,
|
|
189
|
-
newAlternate,
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
return expr
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
path.node.value = t.jsxExpressionContainer(
|
|
196
|
-
t.templateLiteral(quasis, expressions),
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// className={condition ? "..." : "..."}
|
|
201
|
-
else if (
|
|
202
|
-
t.isJSXExpressionContainer(value) &&
|
|
203
|
-
t.isConditionalExpression(value.expression)
|
|
204
|
-
) {
|
|
205
|
-
const expr = value.expression
|
|
206
|
-
path.node.value = t.jsxExpressionContainer(
|
|
207
|
-
t.conditionalExpression(
|
|
208
|
-
expr.test,
|
|
209
|
-
prefixStringExpr(expr.consequent),
|
|
210
|
-
prefixStringExpr(expr.alternate),
|
|
211
|
-
),
|
|
212
|
-
)
|
|
238
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
239
|
+
value.expression = prefixExpr(value.expression)
|
|
213
240
|
}
|
|
241
|
+
},
|
|
214
242
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
)
|
|
220
|
-
const args = value.expression.arguments.map((arg) => {
|
|
221
|
-
if (t.isStringLiteral(arg))
|
|
222
|
-
return t.stringLiteral(prefixClasses(arg.value))
|
|
223
|
-
return arg
|
|
224
|
-
})
|
|
225
|
-
path.node.value = t.jsxExpressionContainer(
|
|
226
|
-
t.callExpression(value.expression.callee, args),
|
|
227
|
-
)
|
|
228
|
-
}
|
|
243
|
+
ObjectProperty(path) {
|
|
244
|
+
if (path.node.computed) return
|
|
245
|
+
const keyName = objectKeyName(path.node.key)
|
|
246
|
+
if (!isClassAttrName(keyName) && !isClassBindingName(keyName)) return
|
|
247
|
+
path.node.value = prefixExpr(path.node.value)
|
|
229
248
|
},
|
|
249
|
+
|
|
230
250
|
VariableDeclarator(path) {
|
|
231
251
|
const id = path.node.id
|
|
232
252
|
const init = path.node.init
|
|
233
|
-
if (!t.isIdentifier(id)) return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!id.name.includes('DynamicClassName')) return
|
|
237
|
-
if (!init) return
|
|
238
|
-
|
|
239
|
-
const prefixString = (expr) =>
|
|
240
|
-
t.isStringLiteral(expr)
|
|
241
|
-
? t.stringLiteral(prefixClasses(expr.value))
|
|
242
|
-
: expr
|
|
243
|
-
|
|
244
|
-
const prefixObject = (objExpr) => {
|
|
245
|
-
if (!t.isObjectExpression(objExpr)) return objExpr
|
|
246
|
-
objExpr.properties.forEach((prop) => {
|
|
247
|
-
if (t.isObjectProperty(prop)) {
|
|
248
|
-
if (t.isStringLiteral(prop.value)) {
|
|
249
|
-
prop.value = t.stringLiteral(
|
|
250
|
-
prefixClasses(prop.value.value),
|
|
251
|
-
)
|
|
252
|
-
} else if (t.isTemplateLiteral(prop.value)) {
|
|
253
|
-
const quasis = prop.value.quasis.map((quasi) => {
|
|
254
|
-
const raw = prefixClasses(quasi.value.raw)
|
|
255
|
-
return t.templateElement({ raw, cooked: raw })
|
|
256
|
-
})
|
|
257
|
-
prop.value = t.templateLiteral(
|
|
258
|
-
quasis,
|
|
259
|
-
prop.value.expressions,
|
|
260
|
-
)
|
|
261
|
-
} else if (t.isObjectExpression(prop.value)) {
|
|
262
|
-
prop.value = prefixObject(prop.value) // recurse
|
|
263
|
-
} else if (t.isConditionalExpression(prop.value)) {
|
|
264
|
-
prop.value = t.conditionalExpression(
|
|
265
|
-
prop.value.test,
|
|
266
|
-
prefixString(prop.value.consequent),
|
|
267
|
-
prefixString(prop.value.alternate),
|
|
268
|
-
)
|
|
269
|
-
} else if (t.isLogicalExpression(prop.value)) {
|
|
270
|
-
const right = t.isStringLiteral(prop.value.right)
|
|
271
|
-
? t.stringLiteral(prefixClasses(prop.value.right.value))
|
|
272
|
-
: prop.value.right
|
|
273
|
-
prop.value = t.logicalExpression(
|
|
274
|
-
prop.value.operator,
|
|
275
|
-
prop.value.left,
|
|
276
|
-
right,
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
})
|
|
281
|
-
return objExpr
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (t.isStringLiteral(init)) {
|
|
285
|
-
path.node.init = t.stringLiteral(prefixClasses(init.value))
|
|
286
|
-
} else if (t.isTemplateLiteral(init)) {
|
|
287
|
-
const quasis = init.quasis.map((quasi) => {
|
|
288
|
-
const raw = prefixClasses(quasi.value.raw)
|
|
289
|
-
return t.templateElement({ raw, cooked: raw })
|
|
290
|
-
})
|
|
291
|
-
path.node.init = t.templateLiteral(quasis, init.expressions)
|
|
292
|
-
} else if (t.isObjectExpression(init)) {
|
|
293
|
-
path.node.init = prefixObject(init)
|
|
294
|
-
} else if (t.isConditionalExpression(init)) {
|
|
295
|
-
path.node.init = t.conditionalExpression(
|
|
296
|
-
init.test,
|
|
297
|
-
prefixString(init.consequent),
|
|
298
|
-
prefixString(init.alternate),
|
|
299
|
-
)
|
|
300
|
-
} else if (t.isLogicalExpression(init)) {
|
|
301
|
-
const right = t.isStringLiteral(init.right)
|
|
302
|
-
? t.stringLiteral(prefixClasses(init.right.value))
|
|
303
|
-
: init.right
|
|
304
|
-
path.node.init = t.logicalExpression(
|
|
305
|
-
init.operator,
|
|
306
|
-
init.left,
|
|
307
|
-
right,
|
|
308
|
-
)
|
|
309
|
-
}
|
|
253
|
+
if (!t.isIdentifier(id) || !init) return
|
|
254
|
+
if (!isClassBindingName(id.name)) return
|
|
255
|
+
path.node.init = prefixExpr(init)
|
|
310
256
|
},
|
|
311
257
|
})
|
|
312
258
|
|
|
@@ -322,8 +268,11 @@ export default function TailwindPrefixPlugin() {
|
|
|
322
268
|
|
|
323
269
|
// Handles:
|
|
324
270
|
// ✅ className="..."
|
|
325
|
-
// ✅
|
|
326
|
-
// ✅className={
|
|
327
|
-
// ✅className={cond
|
|
328
|
-
// ✅className={
|
|
329
|
-
// ✅
|
|
271
|
+
// ✅ containerClassName / *ClassName props
|
|
272
|
+
// ✅ className={`...`} with ${"foo"} inside
|
|
273
|
+
// ✅ className={cond ? "a" : "b"}
|
|
274
|
+
// ✅ className={cond && "foo"}
|
|
275
|
+
// ✅ className={clsx("foo", cond && "bar")}
|
|
276
|
+
// ✅ const fooDynamicClassName = "..." | {...} | cond ? ... : ...
|
|
277
|
+
// ✅ const BUTTON_CLASS / CARDS_GRID_CLASS = "..."
|
|
278
|
+
// ✅ const paginationItemBaseClass / navBtnClass = `...`
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import TailwindPrefixPlugin, {
|
|
5
|
+
isClassAttrName,
|
|
6
|
+
isClassBindingName,
|
|
7
|
+
} from './prefix-plugin.js'
|
|
8
|
+
|
|
9
|
+
function transform(code, id = 'src/Button.tsx') {
|
|
10
|
+
return TailwindPrefixPlugin().transform(code, id)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('isClassBindingName', () => {
|
|
14
|
+
it('matches DynamicClassName, _CLASS, ClassName, and Class suffixes', () => {
|
|
15
|
+
assert.equal(isClassBindingName('fooDynamicClassName'), true)
|
|
16
|
+
assert.equal(isClassBindingName('CARDS_GRID_CLASS'), true)
|
|
17
|
+
assert.equal(isClassBindingName('paginationItemBaseClass'), true)
|
|
18
|
+
assert.equal(isClassBindingName('navBtnClass'), true)
|
|
19
|
+
assert.equal(isClassBindingName('containerClassName'), true)
|
|
20
|
+
assert.equal(isClassBindingName('LABEL'), false)
|
|
21
|
+
assert.equal(isClassBindingName('FOO_CLASS_NAME'), false)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('isClassAttrName', () => {
|
|
26
|
+
it('matches className and *ClassName props', () => {
|
|
27
|
+
assert.equal(isClassAttrName('className'), true)
|
|
28
|
+
assert.equal(isClassAttrName('containerClassName'), true)
|
|
29
|
+
assert.equal(isClassAttrName('pageLinkClassName'), true)
|
|
30
|
+
assert.equal(isClassAttrName('id'), false)
|
|
31
|
+
assert.equal(isClassAttrName('classNames'), false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('vite-tailwind-prefix VariableDeclarator', () => {
|
|
36
|
+
it('prefixes DynamicClassName string bindings', () => {
|
|
37
|
+
const result = transform('const fooDynamicClassName = "flex p-4"')
|
|
38
|
+
assert.match(result.code, /dw:!flex/)
|
|
39
|
+
assert.match(result.code, /dw:!p-4/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('prefixes export const *_CLASS strings (no semicolon)', () => {
|
|
43
|
+
const result = transform(
|
|
44
|
+
"export const CARDS_GRID_CLASS = 'grid grid-cols-12 md:gap-4 gap-3'\n",
|
|
45
|
+
'src/dashboard-layout.constants.ts',
|
|
46
|
+
)
|
|
47
|
+
assert.match(result.code, /dw:!grid/)
|
|
48
|
+
assert.match(result.code, /dw:!grid-cols-12/)
|
|
49
|
+
assert.match(result.code, /dw:md:!gap-4/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('prefixes *_CLASS object bindings', () => {
|
|
53
|
+
const result = transform('const CARD_CLASS = { base: "flex gap-2" }')
|
|
54
|
+
assert.match(result.code, /dw:!flex/)
|
|
55
|
+
assert.match(result.code, /dw:!gap-2/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('prefixes paginationItemBaseClass strings', () => {
|
|
59
|
+
const result = transform(
|
|
60
|
+
"const paginationItemBaseClass = 'inline-flex shrink-0 items-center'",
|
|
61
|
+
)
|
|
62
|
+
assert.match(result.code, /dw:!inline-flex/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('prefixes navBtnClass template returns', () => {
|
|
66
|
+
const result = transform(
|
|
67
|
+
'const navBtnClass = (disabled: boolean) => `inline-flex ${disabled ? "opacity-40" : "cursor-pointer"}`',
|
|
68
|
+
)
|
|
69
|
+
assert.match(result.code, /dw:!inline-flex/)
|
|
70
|
+
assert.match(result.code, /dw:!opacity-40/)
|
|
71
|
+
assert.match(result.code, /dw:!cursor-pointer/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('does not prefix unrelated constants', () => {
|
|
75
|
+
const result = transform('const LABEL = "flex p-4"')
|
|
76
|
+
assert.match(result.code, /"flex p-4"/)
|
|
77
|
+
assert.doesNotMatch(result.code, /dw:/)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('vite-tailwind-prefix JSX *ClassName props', () => {
|
|
82
|
+
it('prefixes className string attributes', () => {
|
|
83
|
+
const result = transform('export const A = () => <div className="flex p-4" />')
|
|
84
|
+
assert.match(result.code, /dw:!flex/)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('prefixes containerClassName and other *ClassName props', () => {
|
|
88
|
+
const result = transform(
|
|
89
|
+
'export const A = () => <ReactPaginate containerClassName="w-max inline-flex items-center gap-1" pageClassName="list-none" />',
|
|
90
|
+
)
|
|
91
|
+
assert.match(result.code, /dw:!w-max/)
|
|
92
|
+
assert.match(result.code, /dw:!inline-flex/)
|
|
93
|
+
assert.match(result.code, /dw:!list-none/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('prefixes pageLinkClassName template literals', () => {
|
|
97
|
+
const result = transform(
|
|
98
|
+
'export const A = () => <ReactPaginate pageLinkClassName={`${paginationItemBaseClass} cursor-pointer hover:bg-secondary/5`} />',
|
|
99
|
+
)
|
|
100
|
+
assert.match(result.code, /dw:!cursor-pointer/)
|
|
101
|
+
assert.match(result.code, /dw:hover:!bg-secondary\/5/)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('vite-tailwind-prefix return helpers', () => {
|
|
106
|
+
it('prefixes functions named *_CLASS', () => {
|
|
107
|
+
const result = transform('function getButton_CLASS() { return "flex" }')
|
|
108
|
+
assert.match(result.code, /dw:!flex/)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -115,9 +115,9 @@ function prefixQuotedStringsInExpression(expr) {
|
|
|
115
115
|
|
|
116
116
|
// Handle className
|
|
117
117
|
function prefixClassesInCode(code) {
|
|
118
|
-
// 1) Plain string: className="..."
|
|
119
|
-
code = code.replace(/
|
|
120
|
-
return
|
|
118
|
+
// 1) Plain string: className="..." / containerClassName="..." / *ClassName="..."
|
|
119
|
+
code = code.replace(/([A-Za-z0-9_]*[Cc]lassName)\s*=\s*["']([^"']*)["']/g, (_, attr, classStr) => {
|
|
120
|
+
return `${attr}="${prefixStaticClasses(classStr)}"`
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
// code = code.replace(/className\s*=\s*["']([^"']*)["']/g, (_, classStr) => {
|
|
@@ -128,8 +128,8 @@ function prefixClassesInCode(code) {
|
|
|
128
128
|
// return `className="${prefixStaticClasses(classStr)}"`
|
|
129
129
|
// })
|
|
130
130
|
|
|
131
|
-
// 2) Template literal: className={`...`}
|
|
132
|
-
code = code.replace(/
|
|
131
|
+
// 2) Template literal: className={`...`} / *ClassName={`...`}
|
|
132
|
+
code = code.replace(/([A-Za-z0-9_]*[Cc]lassName)\s*=\s*{\s*`([\s\S]*?)`\s*}/g, (_, attr, tplBody) => {
|
|
133
133
|
const parts = tplBody.split(/(\$\{[\s\S]*?\})/g)
|
|
134
134
|
|
|
135
135
|
// const rebuilt = parts
|
|
@@ -161,13 +161,13 @@ function prefixClassesInCode(code) {
|
|
|
161
161
|
.join(' ')
|
|
162
162
|
.replace(/\s+/g, ' ') // collapse multiple spaces
|
|
163
163
|
|
|
164
|
-
return
|
|
164
|
+
return `${attr}={\`${rebuilt.trim()}\`}`
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
// 3) Ternary expressions: className={cond ? "..." : "..."}
|
|
167
|
+
// 3) Ternary expressions: className={cond ? "..." : "..."} / *ClassName={...}
|
|
168
168
|
code = code.replace(
|
|
169
|
-
/
|
|
170
|
-
(match, inner) => {
|
|
169
|
+
/([A-Za-z0-9_]*[Cc]lassName)\s*=\s*{([^{};]+?\?[^:]+:[^}]+)}/g,
|
|
170
|
+
(match, attr, inner) => {
|
|
171
171
|
const ternaryMatch = inner.match(
|
|
172
172
|
/^(.*?\?)\s*(['"][^'"]*['"])\s*:\s*(['"][^'"]*['"])\s*$/,
|
|
173
173
|
)
|
|
@@ -183,42 +183,43 @@ function prefixClassesInCode(code) {
|
|
|
183
183
|
(_, q, cls) => `${q}${prefixStaticClasses(cls)}${q}`,
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
return
|
|
186
|
+
return `${attr}={${condition} ${prefixedTrue} : ${prefixedFalse}}`
|
|
187
187
|
},
|
|
188
188
|
)
|
|
189
189
|
|
|
190
|
-
// 4) Variables with "DynamicClassName"
|
|
190
|
+
// 4) Variables with "DynamicClassName" or suffix `_CLASS` (quoted; no semicolon required)
|
|
191
191
|
code = code.replace(
|
|
192
|
-
/(const|let|var)\s+([A-Za-z0-9_]*DynamicClassName[A-Za-z0-9_]
|
|
192
|
+
/(const|let|var)\s+([A-Za-z0-9_]*(?:DynamicClassName[A-Za-z0-9_]*|_CLASS))\s*(?::\s*([^=]+))?\s*=\s*(['"`])([\s\S]*?)\4/g,
|
|
193
|
+
(match, decl, varName, typeAnnotation, quote, value) => {
|
|
194
|
+
const typeSuffix = typeAnnotation ? `: ${typeAnnotation.trim()}` : ''
|
|
195
|
+
if (quote === '`') {
|
|
196
|
+
return `${decl} ${varName}${typeSuffix} = \`${prefixTemplateLiteralBody(value)}\``
|
|
197
|
+
}
|
|
198
|
+
return `${decl} ${varName}${typeSuffix} = ${quote}${prefixStaticClasses(value)}${quote}`
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// 4b) DynamicClassName / _CLASS object or ternary values (semicolon-terminated)
|
|
203
|
+
code = code.replace(
|
|
204
|
+
/(const|let|var)\s+([A-Za-z0-9_]*(?:DynamicClassName[A-Za-z0-9_]*|_CLASS))\s*(?::\s*([^=]+))?\s*=\s*(\{[\s\S]*?\}|.+\?.+:.+);/g,
|
|
193
205
|
(match, decl, varName, typeAnnotation, value) => {
|
|
194
206
|
const typeSuffix = typeAnnotation ? `: ${typeAnnotation.trim()}` : ''
|
|
195
207
|
const trimmed = value.trim()
|
|
196
|
-
if (/^\{[\s\S]*\}$/.test(trimmed)) {
|
|
197
|
-
return `${decl} ${varName}${typeSuffix} = ${prefixValueStrings(value)};`
|
|
198
|
-
}
|
|
199
|
-
if (/\?.*:/.test(trimmed)) {
|
|
208
|
+
if (/^\{[\s\S]*\}$/.test(trimmed) || /\?.*:/.test(trimmed)) {
|
|
200
209
|
return `${decl} ${varName}${typeSuffix} = ${prefixValueStrings(value)};`
|
|
201
210
|
}
|
|
202
|
-
if (/^["'`]/.test(trimmed)) {
|
|
203
|
-
if (trimmed.startsWith('`')) {
|
|
204
|
-
const body = trimmed.slice(1, -1)
|
|
205
|
-
return `${decl} ${varName}${typeSuffix} = \`${prefixTemplateLiteralBody(body)}\`;`
|
|
206
|
-
}
|
|
207
|
-
const cls = trimmed.slice(1, -1)
|
|
208
|
-
return `${decl} ${varName}${typeSuffix} = "${prefixStaticClasses(cls)}";`
|
|
209
|
-
}
|
|
210
211
|
return match
|
|
211
212
|
},
|
|
212
213
|
)
|
|
213
214
|
|
|
214
|
-
// 5) Handle classNames(...) or clsx(...)
|
|
215
|
+
// 5) Handle classNames(...) or clsx(...) on *ClassName props
|
|
215
216
|
code = code.replace(
|
|
216
|
-
/
|
|
217
|
-
(match, fnName, args) => {
|
|
217
|
+
/([A-Za-z0-9_]*[Cc]lassName)\s*=\s*{?\s*(classNames|clsx)\(([\s\S]*?)\)}?/g,
|
|
218
|
+
(match, attr, fnName, args) => {
|
|
218
219
|
const transformedArgs = args.replace(/(['"])(.*?)\1/g, (_, q, cls) => {
|
|
219
220
|
return `${q}${prefixStaticClasses(cls)}${q}`
|
|
220
221
|
})
|
|
221
|
-
return
|
|
222
|
+
return `${attr}={${fnName}(${transformedArgs})}`
|
|
222
223
|
},
|
|
223
224
|
)
|
|
224
225
|
|
|
@@ -48,7 +48,8 @@ export async function createDevServerViteConfig(options = {}) {
|
|
|
48
48
|
])
|
|
49
49
|
|
|
50
50
|
return defineConfig(async ({ mode, command }) => {
|
|
51
|
-
const
|
|
51
|
+
const usePrefix = isPrefix(mode)
|
|
52
|
+
const prefixPlugins = usePrefix
|
|
52
53
|
? [
|
|
53
54
|
(await import('../tailwind/prefix-plugin.js')).default(),
|
|
54
55
|
(await import('../tailwind/tailwind-content-plugin.js')).default(),
|
|
@@ -61,10 +62,20 @@ export async function createDevServerViteConfig(options = {}) {
|
|
|
61
62
|
bootstrapShimResolver({ bootstrapShim, bootstrapGreenfieldShim, kitRoot }),
|
|
62
63
|
resolveFromProjectRoot(projectRoot, kitRoot),
|
|
63
64
|
...(command === 'serve' ? [sdkLogBridge(), moduleConfigStore(projectRoot)] : []),
|
|
65
|
+
// Prefix must run before React / compiler so JSX className is still in the source.
|
|
66
|
+
...prefixPlugins,
|
|
64
67
|
...(hasTanStackRoutes ? [TanStackRouterVite({ autoCodeSplitting: false })] : []),
|
|
65
|
-
viteReact(
|
|
68
|
+
viteReact(
|
|
69
|
+
usePrefix
|
|
70
|
+
? {
|
|
71
|
+
babel: {
|
|
72
|
+
// Match production: compiler rewrites className after the prefix pass.
|
|
73
|
+
plugins: [],
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
: createDevReactPluginOptions(),
|
|
77
|
+
),
|
|
66
78
|
tailwindcss(),
|
|
67
|
-
...prefixPlugins,
|
|
68
79
|
],
|
|
69
80
|
resolve: {
|
|
70
81
|
dedupe: [
|