@woodsportal/hubspot-kit 1.0.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -0
  4. package/SECURITY.md +27 -0
  5. package/dist/chunk-JDGIAYHX.js +87 -0
  6. package/dist/chunk-JDGIAYHX.js.map +1 -0
  7. package/dist/chunk-U4KFGTEW.js +136 -0
  8. package/dist/chunk-U4KFGTEW.js.map +1 -0
  9. package/dist/cli.js +1849 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/exec-K5C6LZP4.js +10 -0
  12. package/dist/exec-K5C6LZP4.js.map +1 -0
  13. package/dist/index.d.ts +228 -0
  14. package/dist/index.js +204 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/vite/index.d.ts +17 -0
  17. package/dist/vite/index.js +9 -0
  18. package/dist/vite/index.js.map +1 -0
  19. package/examples/README.md +12 -0
  20. package/examples/fixtures/adopted-module-hybrid/public/fields.json +1 -0
  21. package/examples/fixtures/adopted-module-hybrid/public/module.html +1 -0
  22. package/examples/fixtures/adopted-module-hybrid/woodscli.json +7 -0
  23. package/examples/fixtures/adopted-module-monolith/public/fields.json +1 -0
  24. package/examples/fixtures/adopted-module-monolith/woodscli.json +7 -0
  25. package/examples/fixtures/adopted-theme-minimal/src/theme/templates/page.html +1 -0
  26. package/examples/fixtures/adopted-theme-minimal/src/theme/theme.json +1 -0
  27. package/examples/fixtures/adopted-theme-minimal/woodscli.json +7 -0
  28. package/examples/fixtures/existing-module-hybrid/public/fields.json +1 -0
  29. package/examples/fixtures/existing-module-hybrid/public/module.html +1 -0
  30. package/examples/fixtures/existing-theme-minimal/templates/page.html +1 -0
  31. package/examples/fixtures/existing-theme-minimal/theme.json +1 -0
  32. package/examples/greenfield/module-hybrid-cdn/README.md +9 -0
  33. package/examples/greenfield/module-hybrid-cdn/index.html +1 -0
  34. package/examples/greenfield/module-hybrid-cdn/package.json +14 -0
  35. package/examples/greenfield/module-hybrid-cdn/public/fields.json +1 -0
  36. package/examples/greenfield/module-hybrid-cdn/public/module.html +1 -0
  37. package/examples/greenfield/module-hybrid-cdn/src/main.tsx +2 -0
  38. package/examples/greenfield/module-hybrid-cdn/vite.config.dev.mjs +1 -0
  39. package/examples/greenfield/module-hybrid-cdn/vite.config.hubspot.mjs +1 -0
  40. package/examples/greenfield/module-hybrid-cdn/woodscli.json +8 -0
  41. package/examples/greenfield/module-hybrid-cdn/wphs.adapter.ts +7 -0
  42. package/examples/greenfield/module-monolith/package.json +12 -0
  43. package/examples/greenfield/module-monolith/public/fields.json +1 -0
  44. package/examples/greenfield/module-monolith/vite.config.dev.mjs +1 -0
  45. package/examples/greenfield/module-monolith/vite.config.monolith.mjs +1 -0
  46. package/examples/greenfield/module-monolith/woodscli.json +25 -0
  47. package/examples/greenfield/module-monolith/wphs.adapter.ts +7 -0
  48. package/examples/greenfield/theme-starter/package.json +11 -0
  49. package/examples/greenfield/theme-starter/src/theme/css/main.css +1 -0
  50. package/examples/greenfield/theme-starter/src/theme/templates/base.html +1 -0
  51. package/examples/greenfield/theme-starter/src/theme/templates/page.html +1 -0
  52. package/examples/greenfield/theme-starter/src/theme/theme.json +1 -0
  53. package/examples/greenfield/theme-starter/theme.json +6 -0
  54. package/examples/greenfield/theme-starter/vite.config.dev.mjs +1 -0
  55. package/examples/greenfield/theme-starter/woodscli.json +7 -0
  56. package/examples/manifest.json +20 -0
  57. package/examples/smoke/run-matrix.mjs +100 -0
  58. package/package.json +94 -0
  59. package/scripts/audit-module-config-fields.mjs +73 -0
  60. package/scripts/audit-parity.mjs +22 -0
  61. package/scripts/audit-tdz-split.mjs +111 -0
  62. package/scripts/build-cdn.mjs +36 -0
  63. package/scripts/check-hubspot-bundle-size.mjs +58 -0
  64. package/scripts/config-files.mjs +53 -0
  65. package/scripts/detect-layout.mjs +70 -0
  66. package/scripts/ensure-wphs-workspace.mjs +53 -0
  67. package/scripts/generate-cdn-manifest.mjs +92 -0
  68. package/scripts/generate-module-config-example.mjs +57 -0
  69. package/scripts/hubspot-module-postbuild.mjs +151 -0
  70. package/scripts/load-portal-cdn-config.mjs +91 -0
  71. package/scripts/migrate-sdk-3-api.mjs +144 -0
  72. package/scripts/module-config-dir.mjs +12 -0
  73. package/scripts/portal-env-mode.mjs +24 -0
  74. package/scripts/portal-vite-env.mjs +39 -0
  75. package/scripts/project-root.mjs +30 -0
  76. package/scripts/publish-cdn-github.mjs +273 -0
  77. package/scripts/read-woodsportal-config.mjs +40 -0
  78. package/scripts/resolve-cdn-base-url.mjs +32 -0
  79. package/scripts/resolve-consumer-dep.mjs +25 -0
  80. package/scripts/resolve-portal-build-id.mjs +67 -0
  81. package/scripts/run-dev.mjs +93 -0
  82. package/scripts/run-hubspot-module-postbuild.mjs +19 -0
  83. package/scripts/run-vite-build.mjs +33 -0
  84. package/scripts/tailwind/prefix-plugin.js +291 -0
  85. package/scripts/tailwind/prefix.env.js +11 -0
  86. package/scripts/tailwind/tailwind-content-plugin.js +27 -0
  87. package/scripts/tailwind/tailwind-content.js +267 -0
  88. package/scripts/verify-cdn-api-endpoint.mjs +68 -0
  89. package/scripts/vite/ensure-dev-kit-shims.js +49 -0
  90. package/scripts/vite/ensure-dev-module-config-shim.js +1 -0
  91. package/scripts/vite/module-config-store.js +242 -0
  92. package/scripts/vite/plugins.d.ts +9 -0
  93. package/scripts/vite/portal-build-shared.js +110 -0
  94. package/scripts/vite/react-dev-plugin-options.js +21 -0
  95. package/scripts/vite/resolve-from-project-root.js +31 -0
  96. package/scripts/vite/sdk-log-bridge.js +104 -0
  97. package/scripts/watch-hubspot.mjs +86 -0
  98. package/scripts/wphs-paths.mjs +115 -0
  99. package/scripts/write-audit-artifacts.mjs +56 -0
  100. package/templates/hubspot.config.yml.example +10 -0
  101. package/templates/module-hybrid-cdn/README.md +9 -0
  102. package/templates/module-hybrid-cdn/hubspot.config.yml.example +10 -0
  103. package/templates/module-hybrid-cdn/index.html +1 -0
  104. package/templates/module-hybrid-cdn/package.json +14 -0
  105. package/templates/module-hybrid-cdn/public/fields.json +1 -0
  106. package/templates/module-hybrid-cdn/public/module.html +1 -0
  107. package/templates/module-hybrid-cdn/src/main.tsx +2 -0
  108. package/templates/module-hybrid-cdn/vite.config.dev.mjs +1 -0
  109. package/templates/module-hybrid-cdn/vite.config.hubspot.mjs +1 -0
  110. package/templates/module-hybrid-cdn/woodscli.json +8 -0
  111. package/templates/module-hybrid-cdn/wphs.adapter.ts +7 -0
  112. package/templates/module-monolith/hubspot.config.yml.example +10 -0
  113. package/templates/module-monolith/package.json +12 -0
  114. package/templates/module-monolith/public/fields.json +1 -0
  115. package/templates/module-monolith/vite.config.dev.mjs +1 -0
  116. package/templates/module-monolith/vite.config.monolith.mjs +1 -0
  117. package/templates/module-monolith/woodscli.json +25 -0
  118. package/templates/module-monolith/wphs.adapter.ts +7 -0
  119. package/templates/theme-starter/hubspot.config.yml.example +10 -0
  120. package/templates/theme-starter/package.json +11 -0
  121. package/templates/theme-starter/src/theme/css/main.css +1 -0
  122. package/templates/theme-starter/src/theme/templates/base.html +1 -0
  123. package/templates/theme-starter/src/theme/templates/page.html +1 -0
  124. package/templates/theme-starter/src/theme/theme.json +1 -0
  125. package/templates/theme-starter/theme.json +6 -0
  126. package/templates/theme-starter/vite.config.dev.mjs +1 -0
  127. package/templates/theme-starter/woodscli.json +7 -0
  128. package/vite/vite.config.audit.mjs +33 -0
  129. package/vite/vite.config.cdn-app.mjs +44 -0
  130. package/vite/vite.config.cdn-esm.mjs +35 -0
  131. package/vite/vite.config.cdn-vendor.mjs +33 -0
  132. package/vite/vite.config.dev.mjs +62 -0
  133. package/vite/vite.config.hubspot.mjs +33 -0
  134. package/vite/vite.config.monolith.mjs +55 -0
  135. package/vite/vite.config.tdz-audit.mjs +58 -0
@@ -0,0 +1,291 @@
1
+ import { parse } from '@babel/parser'
2
+ import traverse from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import generate from '@babel/generator'
5
+ import { prefix } from './prefix.env.js'
6
+
7
+ const prefix1 = `${prefix}:`
8
+ const prefix2 = `${prefix}\:`
9
+
10
+ export default function TailwindPrefixPlugin() {
11
+ return {
12
+ name: 'vite-tailwind-prefix',
13
+ enforce: 'pre',
14
+ transform(code, id) {
15
+ // Handle CSS files: prefix custom class selectors safely
16
+ if (id.endsWith('.css')) {
17
+ // don't need to add prefix to override.style.css
18
+ if (id.endsWith('override.style.css')) return code;
19
+
20
+
21
+ const lines = code.split('\n')
22
+ let braceDepth = 0
23
+
24
+ const prefixedLines = lines.map((line) => {
25
+ // Count { and } to track depth
26
+ const openBraces = (line.match(/{/g) || []).length
27
+ const closeBraces = (line.match(/}/g) || []).length
28
+
29
+ let newLine = line
30
+ if (braceDepth === 0 && line.includes('{')) {
31
+ const braceIndex = line.indexOf('{')
32
+ const selector = line.slice(0, braceIndex)
33
+ const rest = line.slice(braceIndex)
34
+
35
+ const prefixedSelector = selector.replace(
36
+ /(\.)([a-zA-Z0-9_-]+)/g,
37
+ (match, dot, cls) => {
38
+ // ✅ Skip ProseMirror-related, EditorView-related classes
39
+ if (
40
+ cls.startsWith('ProseMirror') ||
41
+ cls.startsWith('EditorView') ||
42
+ cls.startsWith('EmailView') ||
43
+ cls.startsWith('CUSTOM') ||
44
+ cls.startsWith(prefix1) ||
45
+ cls.startsWith(prefix2)
46
+ ) {
47
+ return match
48
+ }
49
+ return `${dot}${prefix2}${cls}`
50
+ },
51
+ )
52
+
53
+ newLine = prefixedSelector + rest
54
+ }
55
+
56
+ braceDepth += openBraces - closeBraces
57
+ return newLine
58
+ })
59
+
60
+ return { code: prefixedLines.join('\n'), map: null }
61
+ }
62
+
63
+
64
+ if (!id.endsWith('.tsx') && !id.endsWith('.ts')) return null
65
+
66
+ try {
67
+ const ast = parse(code, {
68
+ sourceType: 'module',
69
+ plugins: ['jsx', 'typescript'],
70
+ })
71
+
72
+ const addImportant = (c) => {
73
+ if (!c) return c
74
+ const parts = c.split(':')
75
+ const utility = parts.pop()
76
+ if (!utility || utility.startsWith('!')) {
77
+ return c
78
+ }
79
+ parts.push(`!${utility}`)
80
+ return parts.join(':')
81
+ }
82
+
83
+ const prefixClasses = (str) => {
84
+ return str
85
+ .split(/\s+/)
86
+ .filter(Boolean)
87
+ .map((c) => {
88
+ if (c.startsWith(prefix1) || c.startsWith('CUSTOM') || c.startsWith('ProseMirror') || c.startsWith('EmailView')) return c
89
+ return `${prefix1}${c}`
90
+ })
91
+ .map((c) => {
92
+ if (c.startsWith('CUSTOM') || c.startsWith('ProseMirror') || c.startsWith('EmailView')) return c
93
+ return addImportant(c)
94
+ })
95
+ .join(' ')
96
+ }
97
+
98
+ traverse.default(ast, {
99
+ // ✅ Handle variables whose name includes "DynamicClassName"
100
+
101
+ JSXAttribute(path) {
102
+ if (path.node.name.name !== 'className') return
103
+
104
+ const value = path.node.value
105
+
106
+ // String literal: className="..."
107
+ if (t.isStringLiteral(value)) {
108
+ path.node.value = t.stringLiteral(prefixClasses(value.value))
109
+ }
110
+ // Template literal: className={`...`}
111
+ else if (
112
+ t.isJSXExpressionContainer(value) &&
113
+ t.isTemplateLiteral(value.expression)
114
+ ) {
115
+ const quasis = value.expression.quasis.map((quasi, i) => {
116
+ let classes = prefixClasses(quasi.value.raw)
117
+
118
+ if (i < value.expression.expressions.length) classes += ' '
119
+ if (i > 0 && !classes.startsWith(' ')) classes = ' ' + classes
120
+
121
+ return t.templateElement({ raw: classes, cooked: classes })
122
+ })
123
+
124
+ // 🔥 also fix expressions inside ${ ... }
125
+ const expressions = value.expression.expressions.map((expr) => {
126
+ if (t.isStringLiteral(expr)) {
127
+ return t.stringLiteral(prefixClasses(expr.value))
128
+ }
129
+ if (t.isLogicalExpression(expr)) {
130
+ if (t.isStringLiteral(expr.right)) {
131
+ return t.logicalExpression(
132
+ expr.operator,
133
+ expr.left,
134
+ t.stringLiteral(prefixClasses(expr.right.value)),
135
+ )
136
+ }
137
+ }
138
+ if (t.isConditionalExpression(expr)) {
139
+ const newConsequent = t.isStringLiteral(expr.consequent)
140
+ ? t.stringLiteral(prefixClasses(expr.consequent.value))
141
+ : expr.consequent
142
+ const newAlternate = t.isStringLiteral(expr.alternate)
143
+ ? t.stringLiteral(prefixClasses(expr.alternate.value))
144
+ : expr.alternate
145
+ return t.conditionalExpression(
146
+ expr.test,
147
+ newConsequent,
148
+ newAlternate,
149
+ )
150
+ }
151
+ return expr
152
+ })
153
+
154
+ path.node.value = t.jsxExpressionContainer(
155
+ t.templateLiteral(quasis, expressions),
156
+ )
157
+ }
158
+
159
+ // className={condition ? "..." : "..."}
160
+ else if (
161
+ t.isJSXExpressionContainer(value) &&
162
+ t.isConditionalExpression(value.expression)
163
+ ) {
164
+ const expr = value.expression
165
+ const newConsequent = t.isStringLiteral(expr.consequent)
166
+ ? t.stringLiteral(prefixClasses(expr.consequent.value))
167
+ : expr.consequent
168
+ const newAlternate = t.isStringLiteral(expr.alternate)
169
+ ? t.stringLiteral(prefixClasses(expr.alternate.value))
170
+ : expr.alternate
171
+
172
+ path.node.value = t.jsxExpressionContainer(
173
+ t.conditionalExpression(expr.test, newConsequent, newAlternate),
174
+ )
175
+ }
176
+
177
+ // Other expressions (e.g., clsx())
178
+ else if (
179
+ t.isJSXExpressionContainer(value) &&
180
+ t.isCallExpression(value.expression)
181
+ ) {
182
+ const args = value.expression.arguments.map((arg) => {
183
+ if (t.isStringLiteral(arg))
184
+ return t.stringLiteral(prefixClasses(arg.value))
185
+ return arg
186
+ })
187
+ path.node.value = t.jsxExpressionContainer(
188
+ t.callExpression(value.expression.callee, args),
189
+ )
190
+ }
191
+ },
192
+ VariableDeclarator(path) {
193
+ const id = path.node.id
194
+ const init = path.node.init
195
+ if (!t.isIdentifier(id)) return
196
+
197
+ // 🔹 only affect DynamicClassName variables
198
+ if (!id.name.includes('DynamicClassName')) return
199
+ if (!init) return
200
+
201
+ const prefixString = (expr) =>
202
+ t.isStringLiteral(expr)
203
+ ? t.stringLiteral(prefixClasses(expr.value))
204
+ : expr
205
+
206
+ const prefixObject = (objExpr) => {
207
+ if (!t.isObjectExpression(objExpr)) return objExpr
208
+ objExpr.properties.forEach((prop) => {
209
+ if (t.isObjectProperty(prop)) {
210
+ if (t.isStringLiteral(prop.value)) {
211
+ prop.value = t.stringLiteral(
212
+ prefixClasses(prop.value.value),
213
+ )
214
+ } else if (t.isTemplateLiteral(prop.value)) {
215
+ const quasis = prop.value.quasis.map((quasi) => {
216
+ const raw = prefixClasses(quasi.value.raw)
217
+ return t.templateElement({ raw, cooked: raw })
218
+ })
219
+ prop.value = t.templateLiteral(
220
+ quasis,
221
+ prop.value.expressions,
222
+ )
223
+ } else if (t.isObjectExpression(prop.value)) {
224
+ prop.value = prefixObject(prop.value) // recurse
225
+ } else if (t.isConditionalExpression(prop.value)) {
226
+ prop.value = t.conditionalExpression(
227
+ prop.value.test,
228
+ prefixString(prop.value.consequent),
229
+ prefixString(prop.value.alternate),
230
+ )
231
+ } else if (t.isLogicalExpression(prop.value)) {
232
+ const right = t.isStringLiteral(prop.value.right)
233
+ ? t.stringLiteral(prefixClasses(prop.value.right.value))
234
+ : prop.value.right
235
+ prop.value = t.logicalExpression(
236
+ prop.value.operator,
237
+ prop.value.left,
238
+ right,
239
+ )
240
+ }
241
+ }
242
+ })
243
+ return objExpr
244
+ }
245
+
246
+ if (t.isStringLiteral(init)) {
247
+ path.node.init = t.stringLiteral(prefixClasses(init.value))
248
+ } else if (t.isTemplateLiteral(init)) {
249
+ const quasis = init.quasis.map((quasi) => {
250
+ const raw = prefixClasses(quasi.value.raw)
251
+ return t.templateElement({ raw, cooked: raw })
252
+ })
253
+ path.node.init = t.templateLiteral(quasis, init.expressions)
254
+ } else if (t.isObjectExpression(init)) {
255
+ path.node.init = prefixObject(init)
256
+ } else if (t.isConditionalExpression(init)) {
257
+ path.node.init = t.conditionalExpression(
258
+ init.test,
259
+ prefixString(init.consequent),
260
+ prefixString(init.alternate),
261
+ )
262
+ } else if (t.isLogicalExpression(init)) {
263
+ const right = t.isStringLiteral(init.right)
264
+ ? t.stringLiteral(prefixClasses(init.right.value))
265
+ : init.right
266
+ path.node.init = t.logicalExpression(
267
+ init.operator,
268
+ init.left,
269
+ right,
270
+ )
271
+ }
272
+ },
273
+ })
274
+
275
+ const output = generate.default(ast, {}, code)
276
+ return { code: output.code, map: output.map }
277
+ } catch (err) {
278
+ console.error(`Error transforming file ${id}:`, err)
279
+ return null
280
+ }
281
+ },
282
+ }
283
+ }
284
+
285
+ // Handles:
286
+ // ✅ className="..."
287
+ // ✅ className={\`...\`}with${"foo"}inside
288
+ // ✅className={cond ? "a" : "b"}
289
+ // ✅className={cond && "foo"}
290
+ // ✅className={clsx("foo", cond && "bar")}
291
+ // ✅const fooDynamicClassName = "..." | {...} | cond ? ... : ...`
@@ -0,0 +1,11 @@
1
+ import { isPrefixBuildMode } from '../portal-env-mode.mjs'
2
+
3
+ const forcePrefix = null // true, false, or null to auto-detect
4
+
5
+ export const prefix = 'dw'
6
+
7
+ const isPrefix = (env) => {
8
+ if (forcePrefix != null) return forcePrefix
9
+ return isPrefixBuildMode(env)
10
+ }
11
+ export default isPrefix
@@ -0,0 +1,27 @@
1
+ import tailwindContent from './tailwind-content.js'
2
+
3
+ export default function TailwindContentPlugin() {
4
+ return {
5
+ name: 'vite-tailwind-content',
6
+ apply: 'serve',
7
+
8
+ async buildStart() {
9
+ await tailwindContent()
10
+ },
11
+ async handleHotUpdate({ file }) {
12
+ // ❌ ignore changes inside .temp (to avoid infinite loop)
13
+ if (file.includes('.temp')) return
14
+
15
+ if (
16
+ /\.(tsx|jsx|css)$/.test(file) &&
17
+ (file.includes('/src/') ||
18
+ file.includes('/dev-module-config/') ||
19
+ file.includes('/.wphs/') ||
20
+ file.includes('/.kit-module-config/') ||
21
+ file.includes('/.dev/'))
22
+ ) {
23
+ await tailwindContent()
24
+ }
25
+ },
26
+ }
27
+ }
@@ -0,0 +1,267 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { prefix } from './prefix.env.js'
4
+ import { findProjectRoot, getKitRoot } from '../project-root.mjs'
5
+ import { moduleConfigPaths } from '../module-config-dir.mjs'
6
+
7
+ const prefix1 = `${prefix}:`
8
+
9
+ const tempDir = path.resolve('./.temp')
10
+
11
+ function resolveContentScanDirs() {
12
+ const dirs = [path.resolve('./src')]
13
+ try {
14
+ const projectRoot = findProjectRoot()
15
+ const { devDir } = moduleConfigPaths(projectRoot)
16
+ const kitModuleConfig = path.join(getKitRoot(), 'src/dev-module-config')
17
+ const shimModuleConfig = path.join(devDir, '.kit-module-config')
18
+
19
+ for (const dir of [kitModuleConfig, shimModuleConfig]) {
20
+ if (fs.existsSync(dir) && !dirs.includes(dir)) {
21
+ dirs.push(dir)
22
+ }
23
+ }
24
+ } catch {
25
+ // cwd may not be a woodsportal project (e.g. kit-only scripts)
26
+ }
27
+ return dirs
28
+ }
29
+
30
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true })
31
+
32
+ function addPrefix(token) {
33
+ if (!token) return token
34
+ if (token.startsWith(prefix1)) return token
35
+ return `${prefix1}${token}`
36
+ }
37
+
38
+ function addImportant(token) {
39
+ if (!token) return token
40
+ const parts = token.split(':')
41
+ const utility = parts.pop()
42
+ if (!utility || utility.startsWith('!')) {
43
+ return token
44
+ }
45
+ parts.push(`!${utility}`)
46
+ return parts.join(':')
47
+ }
48
+
49
+ // function prefixStaticClasses(str) {
50
+ // // Prefix whitespace-separated class tokens (outside ${ ... })
51
+ // return str.split(/\s+/).filter(Boolean).map(addPrefix).join(' ')
52
+ // }
53
+
54
+ // ✅ Safe prefixing for static classes
55
+ function prefixStaticClasses(str) {
56
+ return str
57
+ .split(/\s+/)
58
+ .filter(Boolean)
59
+ .map((token) => {
60
+ if (!token) return token
61
+ if (token.startsWith(prefix1)) return addImportant(token)
62
+
63
+ // ✅ Handle opening arbitrary value like "z-["
64
+ if (/^[a-z0-9-]+-\[$/i.test(token)) {
65
+ return addImportant(`${prefix1}${token}`)
66
+ }
67
+
68
+ // ✅ Leave closing bracket "]" untouched
69
+ if (token === ']') {
70
+ return token
71
+ }
72
+
73
+ // ✅ Handle complete arbitrary values like "z-[999]" or "top-[50%]"
74
+ // const match = token.match(/^([a-z0-9-]+(?:\/[a-z0-9-]+)?):?\[(.+)\]$/i)
75
+ // if (match) {
76
+ // const [, key, val] = match
77
+ // return `${prefix1}${key}-[${val}]`
78
+ // }
79
+
80
+ return addImportant(addPrefix(token))
81
+ })
82
+ .join(' ')
83
+ }
84
+
85
+ // Prefix static segments of a template literal body (leave ${...} untouched)
86
+ function prefixTemplateLiteralBody(body) {
87
+ const parts = body.split(/(\$\{[^}]*\})/g)
88
+ return parts
89
+ .map((part) => {
90
+ if (part.startsWith('${')) return part
91
+ if (!part.trim()) return part
92
+ const leading = part.match(/^\s*/)?.[0] ?? ''
93
+ const trailing = part.match(/\s*$/)?.[0] ?? ''
94
+ const core = part.trim()
95
+ if (!core) return part
96
+ return leading + prefixStaticClasses(core) + trailing
97
+ })
98
+ .join('')
99
+ }
100
+
101
+ // Prefix class strings inside object literals, quoted strings, and template literals
102
+ function prefixValueStrings(value) {
103
+ let result = value.replace(/"([^"]*)"/g, (_, cls) => `"${prefixStaticClasses(cls)}"`)
104
+ result = result.replace(/`([^`]*)`/g, (_, body) => `\`${prefixTemplateLiteralBody(body)}\``)
105
+ return result
106
+ }
107
+
108
+ // Prefix classes inside quoted strings ONLY, e.g. "...${cond ? 'lg:w-[...]' : 'lg:w-[...]'}..."
109
+ function prefixQuotedStringsInExpression(expr) {
110
+ // Replace '..."' or '...' with prefixed contents
111
+ return expr.replace(/(['"])((?:\\.|(?!\1).)*)\1/g, (_, quote, inner) => {
112
+ return `${quote}${prefixStaticClasses(inner)}${quote}`
113
+ })
114
+ }
115
+
116
+ // Handle className
117
+ function prefixClassesInCode(code) {
118
+ // 1) Plain string: className="..."
119
+ code = code.replace(/className\s*=\s*["']([^"']*)["']/g, (_, classStr) => {
120
+ return `className="${prefixStaticClasses(classStr)}"`
121
+ })
122
+
123
+ // code = code.replace(/className\s*=\s*["']([^"']*)["']/g, (_, classStr) => {
124
+ // // 🛑 Skip prefixing if contains "ProseMirror"
125
+ // if (/\bProseMirror\b/.test(classStr)) {
126
+ // return `className="${classStr}"`
127
+ // }
128
+ // return `className="${prefixStaticClasses(classStr)}"`
129
+ // })
130
+
131
+ // 2) Template literal: className={`...`}
132
+ code = code.replace(/className\s*=\s*{\s*`([\s\S]*?)`\s*}/g, (_, tplBody) => {
133
+ const parts = tplBody.split(/(\$\{[\s\S]*?\})/g)
134
+
135
+ // const rebuilt = parts
136
+ // .map((part) => {
137
+ // if (part.startsWith('${')) {
138
+ // return part.replace(
139
+ // /^\$\{([\s\S]*?)\}$/,
140
+ // (_m, inner) => '${' + prefixQuotedStringsInExpression(inner) + '}',
141
+ // )
142
+ // }
143
+ // return prefixStaticClasses(part)
144
+ // })
145
+ // .join('')
146
+
147
+
148
+ // return `className={\`${rebuilt}\`}`
149
+ const rebuilt = parts
150
+ .map((part, i) => {
151
+ if (part.startsWith('${')) {
152
+ // normalize: always prefix with one space
153
+ return ' ' + part.replace(
154
+ /^\$\{([\s\S]*?)\}$/,
155
+ (_m, inner) => '${' + prefixQuotedStringsInExpression(inner) + '}',
156
+ )
157
+ }
158
+ // static classes
159
+ return prefixStaticClasses(part).trim()
160
+ })
161
+ .join(' ')
162
+ .replace(/\s+/g, ' ') // collapse multiple spaces
163
+
164
+ return `className={\`${rebuilt.trim()}\`}`
165
+ })
166
+
167
+ // 3) Ternary expressions: className={cond ? "..." : "..."}
168
+ code = code.replace(
169
+ /className\s*=\s*{([^{};]+?\?[^:]+:[^}]+)}/g,
170
+ (match, inner) => {
171
+ const ternaryMatch = inner.match(
172
+ /^(.*?\?)\s*(['"][^'"]*['"])\s*:\s*(['"][^'"]*['"])\s*$/,
173
+ )
174
+ if (!ternaryMatch) return match
175
+
176
+ const [, condition, whenTrue, whenFalse] = ternaryMatch
177
+ const prefixedTrue = whenTrue.replace(
178
+ /(['"])(.*?)\1/,
179
+ (_, q, cls) => `${q}${prefixStaticClasses(cls)}${q}`,
180
+ )
181
+ const prefixedFalse = whenFalse.replace(
182
+ /(['"])(.*?)\1/,
183
+ (_, q, cls) => `${q}${prefixStaticClasses(cls)}${q}`,
184
+ )
185
+
186
+ return `className={${condition} ${prefixedTrue} : ${prefixedFalse}}`
187
+ },
188
+ )
189
+
190
+ // 4) Variables with "DynamicClassName" (supports TypeScript annotations like `: any`)
191
+ code = code.replace(
192
+ /(const|let|var)\s+([A-Za-z0-9_]*DynamicClassName[A-Za-z0-9_]*)\s*(?::\s*([^=]+))?\s*=\s*([\s\S]*?);/g,
193
+ (match, decl, varName, typeAnnotation, value) => {
194
+ const typeSuffix = typeAnnotation ? `: ${typeAnnotation.trim()}` : ''
195
+ const trimmed = value.trim()
196
+ if (/^\{[\s\S]*\}$/.test(trimmed)) {
197
+ return `${decl} ${varName}${typeSuffix} = ${prefixValueStrings(value)};`
198
+ }
199
+ if (/\?.*:/.test(trimmed)) {
200
+ return `${decl} ${varName}${typeSuffix} = ${prefixValueStrings(value)};`
201
+ }
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
+ return match
211
+ },
212
+ )
213
+
214
+ // 5) Handle classNames(...) or clsx(...)
215
+ code = code.replace(
216
+ /className\s*=\s*{?\s*(classNames|clsx)\(([\s\S]*?)\)}?/g,
217
+ (match, fnName, args) => {
218
+ const transformedArgs = args.replace(/(['"])(.*?)\1/g, (_, q, cls) => {
219
+ return `${q}${prefixStaticClasses(cls)}${q}`
220
+ })
221
+ return `className={${fnName}(${transformedArgs})}`
222
+ },
223
+ )
224
+
225
+ return code
226
+ }
227
+
228
+ // 🔥 helper: recursively collect all `.tsx` files under src
229
+ function getAllTsxFiles(dir, fileList = []) {
230
+ const files = fs.readdirSync(dir)
231
+ for (const file of files) {
232
+ const fullPath = path.join(dir, file)
233
+ const stat = fs.statSync(fullPath)
234
+ if (stat.isDirectory()) {
235
+ getAllTsxFiles(fullPath, fileList)
236
+ } else if (file.endsWith('.tsx')) {
237
+ fileList.push(fullPath)
238
+ }
239
+ }
240
+ return fileList
241
+ }
242
+
243
+ // Pre-build static file for Tailwind
244
+ export default async function tailwindContent() {
245
+ const srcFiles = resolveContentScanDirs().flatMap((dir) => getAllTsxFiles(dir))
246
+ let allPrefixedClasses = ''
247
+
248
+ for (const filePath of srcFiles) {
249
+ const code = fs.readFileSync(filePath, 'utf-8')
250
+ const transformed = prefixClassesInCode(code)
251
+ allPrefixedClasses += transformed + '\n'
252
+ }
253
+
254
+ const outputPath = path.resolve(tempDir, 'tailwind-classes.tsx')
255
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true })
256
+ fs.writeFileSync(outputPath, allPrefixedClasses, 'utf-8')
257
+
258
+ // Return the file as Tailwind content
259
+ return [
260
+ {
261
+ raw: allPrefixedClasses,
262
+ extension: 'tsx',
263
+ },
264
+ ]
265
+ }
266
+
267
+ tailwindContent()
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail if dist/cdn/app.*.js does not bake the API URL from .env.stg / .env.prod.
4
+ *
5
+ * Usage:
6
+ * node scripts/verify-cdn-api-endpoint.mjs --stg
7
+ * node scripts/verify-cdn-api-endpoint.mjs --prod
8
+ */
9
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
10
+ import { dirname, resolve } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+ import { findProjectRoot } from './project-root.mjs'
13
+ import { loadEnv } from 'vite'
14
+
15
+ import { resolvePortalBuildId } from './resolve-portal-build-id.mjs'
16
+ import { resolvePortalViteMode } from './portal-env-mode.mjs'
17
+
18
+ const root = findProjectRoot()
19
+
20
+ const stg = process.argv.includes('--stg') || process.argv.includes('--staging')
21
+ const prod = process.argv.includes('--prod') || process.argv.includes('--production')
22
+
23
+ if (stg === prod) {
24
+ console.error(
25
+ 'Usage: node scripts/verify-cdn-api-endpoint.mjs --stg | --prod',
26
+ )
27
+ process.exit(1)
28
+ }
29
+
30
+ const mode = resolvePortalViteMode()
31
+ const expected = loadEnv(mode, root, '').VITE_PUBLIC_REST_API_ENDPOINT?.replace(/\/$/, '')
32
+
33
+ if (!expected) {
34
+ console.error(`[verify-cdn-api] Missing VITE_PUBLIC_REST_API_ENDPOINT in .env.${mode}`)
35
+ process.exit(1)
36
+ }
37
+
38
+ const cdnDir = resolve(root, 'dist/cdn')
39
+ if (!existsSync(cdnDir)) {
40
+ console.error('[verify-cdn-api] dist/cdn missing — run yarn build:cdn:dev or yarn build:cdn:prod first')
41
+ process.exit(1)
42
+ }
43
+
44
+ const buildId = resolvePortalBuildId()
45
+ const appFile = readdirSync(cdnDir).find((f) => f === `app.${buildId}.js`)
46
+
47
+ if (!appFile) {
48
+ console.error(
49
+ `[verify-cdn-api] app.${buildId}.js not found in dist/cdn — rebuild with matching .portal-build-id`,
50
+ )
51
+ process.exit(1)
52
+ }
53
+
54
+ const source = readFileSync(resolve(cdnDir, appFile), 'utf8')
55
+ const found = [...source.matchAll(/https:\/\/api(?:\.dev)?\.woodsportal\.com/g)].map((m) => m[0])
56
+ const unique = [...new Set(found)]
57
+
58
+ if (!unique.includes(expected)) {
59
+ console.error('[verify-cdn-api] API URL mismatch in dist/cdn — refusing to publish.')
60
+ console.error(` expected (${mode}): ${expected}`)
61
+ console.error(` found in ${appFile}: ${unique.join(', ') || '(none)'}`)
62
+ console.error('')
63
+ console.error(' Dev API on HubSpot: yarn deploy:dev')
64
+ console.error(' Prod API on HubSpot: yarn deploy:prod')
65
+ process.exit(1)
66
+ }
67
+
68
+ console.log(`[verify-cdn-api] OK ${appFile} bakes ${expected} (${mode})`)