@woodsportal/hubspot-kit 5.0.2 → 5.0.3-beta.1
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/build-cdn.mjs +30 -0
- package/scripts/cdn-release-artifacts.mjs +76 -0
- package/scripts/cdn-release-artifacts.test.mjs +20 -0
- package/scripts/generate-cdn-manifest.mjs +48 -15
- package/scripts/hubspot-module-postbuild.mjs +8 -20
- package/scripts/resolve-cdn-base-url.mjs +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 +2 -2
|
@@ -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: [
|
|
@@ -135,11 +135,16 @@ function prodDefaultDataStub(mode) {
|
|
|
135
135
|
|
|
136
136
|
export function portalPlugins(mode, { codeSplitting = false, routeFileIgnorePattern } = {}) {
|
|
137
137
|
const routeIgnore = routeFileIgnorePattern ?? (isPrefix(mode) ? 'dev\\.' : undefined)
|
|
138
|
+
const prefixPlugins = isPrefix(mode)
|
|
139
|
+
? [tailwindPrefixPlugin(), TailwindContentPlugin()]
|
|
140
|
+
: []
|
|
138
141
|
return [
|
|
139
142
|
prodDevModuleConfigRouteStub(mode),
|
|
140
143
|
prodDevHubSpotLiveStub(mode),
|
|
141
144
|
prodSdkTerminalLogSinksStub(mode),
|
|
142
145
|
prodDefaultDataStub(mode),
|
|
146
|
+
// Prefix must run before React / compiler so JSX className is still in the source.
|
|
147
|
+
...prefixPlugins,
|
|
143
148
|
TanStackRouterVite({
|
|
144
149
|
autoCodeSplitting: codeSplitting,
|
|
145
150
|
...(routeIgnore ? { routeFileIgnorePattern: routeIgnore } : {}),
|
|
@@ -152,9 +157,6 @@ export function portalPlugins(mode, { codeSplitting = false, routeFileIgnorePatt
|
|
|
152
157
|
},
|
|
153
158
|
}),
|
|
154
159
|
tailwindcss(),
|
|
155
|
-
...(isPrefix(mode)
|
|
156
|
-
? [tailwindPrefixPlugin(), TailwindContentPlugin()]
|
|
157
|
-
: []),
|
|
158
160
|
]
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { buildApiRoutes } from '../../routing/build-api-routes'
|
|
2
|
+
|
|
3
|
+
describe('buildApiRoutes custom menu', () => {
|
|
4
|
+
it('maps custom menu items to opaque paths without ids', () => {
|
|
5
|
+
const routes = buildApiRoutes([
|
|
6
|
+
{
|
|
7
|
+
label: 'Help Center',
|
|
8
|
+
menuUrl: 'https://help.example.com',
|
|
9
|
+
icon: '<svg></svg>',
|
|
10
|
+
selectOption: 'iframe',
|
|
11
|
+
objectType: 'CUSTOM_MENU',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
label: 'Contacts',
|
|
15
|
+
hubspotObjectTypeId: '0-1',
|
|
16
|
+
objectType: 'HUBSPOT_DEFINED',
|
|
17
|
+
},
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
expect(routes[0]).toMatchObject({
|
|
21
|
+
path: 'custom-menu:help-center',
|
|
22
|
+
title: 'Help Center',
|
|
23
|
+
isCustomMenu: true,
|
|
24
|
+
selectOption: 'iframe',
|
|
25
|
+
menuUrl: 'https://help.example.com',
|
|
26
|
+
isRequiredAuth: false,
|
|
27
|
+
})
|
|
28
|
+
expect(routes[0]).not.toHaveProperty('customMenuId')
|
|
29
|
+
expect(routes[1]?.path).toBe('/0-1')
|
|
30
|
+
expect(routes[1]?.isCustomMenu).toBeFalsy()
|
|
31
|
+
})
|
|
32
|
+
})
|