@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,49 @@
1
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import { moduleConfigPaths } from '../module-config-dir.mjs'
5
+
6
+ function ensureSymlink(shimPath, target, { directory }) {
7
+ try {
8
+ if (existsSync(shimPath)) {
9
+ const stat = lstatSync(shimPath)
10
+ if (stat.isSymbolicLink()) {
11
+ return shimPath
12
+ }
13
+ rmSync(shimPath, { recursive: !directory, force: true })
14
+ }
15
+
16
+ mkdirSync(resolve(shimPath, '..'), { recursive: true })
17
+ symlinkSync(target, shimPath, directory ? 'dir' : 'file')
18
+ return shimPath
19
+ } catch {
20
+ return target
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Symlink kit dev sources into the consumer repo so Vite serves them in-root
26
+ * (avoids @fs/ + broken react/jsx-dev-runtime / react-dom CJS interop).
27
+ */
28
+ export function ensureDevKitShims(projectRoot, kitRoot) {
29
+ const { devDir } = moduleConfigPaths(projectRoot)
30
+
31
+ const moduleConfigRoot = ensureSymlink(
32
+ resolve(devDir, '.kit-module-config'),
33
+ resolve(kitRoot, 'src/dev-module-config'),
34
+ { directory: true },
35
+ )
36
+
37
+ const bootstrapShim = ensureSymlink(
38
+ resolve(devDir, '.kit-bootstrap.tsx'),
39
+ resolve(kitRoot, 'src/dev/bootstrap.tsx'),
40
+ { directory: false },
41
+ )
42
+
43
+ return { moduleConfigRoot, bootstrapShim }
44
+ }
45
+
46
+ /** @deprecated use ensureDevKitShims */
47
+ export function ensureDevModuleConfigShim(projectRoot, kitRoot) {
48
+ return ensureDevKitShims(projectRoot, kitRoot).moduleConfigRoot
49
+ }
@@ -0,0 +1 @@
1
+ export { ensureDevKitShims, ensureDevModuleConfigShim } from './ensure-dev-kit-shims.js'
@@ -0,0 +1,242 @@
1
+ /** @typedef {import('vite').Plugin} Plugin */
2
+
3
+ import fs from 'node:fs'
4
+ import fsp from 'node:fs/promises'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import { moduleConfigPaths } from '../module-config-dir.mjs'
9
+
10
+ const CONFIG_API = '/__dev/module-config'
11
+ const BRANDING_API = '/__dev/branding-asset'
12
+ const MAX_BODY_BYTES = 5 * 1024 * 1024
13
+
14
+ /**
15
+ * Dev-only Vite middleware: persist module config + branding assets under dev.moduleConfigDir.
16
+ *
17
+ * @param {string} [projectRoot]
18
+ * @returns {Plugin}
19
+ */
20
+ export default function moduleConfigStore(projectRoot) {
21
+ const root = projectRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
22
+ const {
23
+ moduleConfigDir,
24
+ devDir,
25
+ configDir,
26
+ localConfigPath,
27
+ exampleConfigPath,
28
+ assetsDir,
29
+ brandingAssetsUrlPrefix,
30
+ legacyBrandingAssetsUrlPrefixes = [],
31
+ } = moduleConfigPaths(root)
32
+
33
+ const legacyPrefixes = Array.isArray(legacyBrandingAssetsUrlPrefixes)
34
+ ? legacyBrandingAssetsUrlPrefixes
35
+ : []
36
+
37
+ return {
38
+ name: 'woodsportal-module-config-store',
39
+ apply: 'serve',
40
+ configureServer(server) {
41
+ ensureDirSync(devDir)
42
+ ensureDirSync(configDir)
43
+ ensureDirSync(assetsDir)
44
+
45
+ server.middlewares.use((req, res, next) => {
46
+ const url = req.url ?? ''
47
+ const assetsPrefix = [brandingAssetsUrlPrefix, ...legacyPrefixes].find((prefix) =>
48
+ url.startsWith(prefix),
49
+ )
50
+ if (!assetsPrefix) {
51
+ next()
52
+ return
53
+ }
54
+
55
+ const relative = decodeURIComponent(url.slice(assetsPrefix.length).split('?')[0] ?? '')
56
+ if (!relative || relative.includes('..')) {
57
+ res.statusCode = 400
58
+ res.end('Invalid path')
59
+ return
60
+ }
61
+
62
+ const filePath = path.join(assetsDir, relative)
63
+ if (!filePath.startsWith(assetsDir) || !fs.existsSync(filePath)) {
64
+ res.statusCode = 404
65
+ res.end('Not found')
66
+ return
67
+ }
68
+
69
+ const ext = path.extname(filePath).toLowerCase()
70
+ const types = {
71
+ '.png': 'image/png',
72
+ '.jpg': 'image/jpeg',
73
+ '.jpeg': 'image/jpeg',
74
+ '.gif': 'image/gif',
75
+ '.webp': 'image/webp',
76
+ '.svg': 'image/svg+xml',
77
+ }
78
+ res.setHeader('Content-Type', types[ext] ?? 'application/octet-stream')
79
+ fs.createReadStream(filePath).pipe(res)
80
+ })
81
+
82
+ server.middlewares.use(BRANDING_API, (req, res, next) => {
83
+ if (req.method !== 'POST') {
84
+ next()
85
+ return
86
+ }
87
+
88
+ readBody(req, res, async (body) => {
89
+ try {
90
+ const payload = JSON.parse(body)
91
+ const filename = sanitizeFilename(String(payload.filename ?? 'upload.bin'))
92
+ const data = String(payload.data ?? '')
93
+ const buffer = Buffer.from(data, 'base64')
94
+ if (buffer.length > MAX_BODY_BYTES) {
95
+ res.statusCode = 413
96
+ res.end('Payload too large')
97
+ return
98
+ }
99
+
100
+ const filePath = path.join(assetsDir, filename)
101
+ const tmpPath = `${filePath}.tmp`
102
+ await fsp.writeFile(tmpPath, buffer)
103
+ await fsp.rename(tmpPath, filePath)
104
+
105
+ writeJson(res, 200, {
106
+ src: `${brandingAssetsUrlPrefix}${encodeURIComponent(filename)}`,
107
+ filename,
108
+ })
109
+ } catch {
110
+ res.statusCode = 400
111
+ res.end('Invalid JSON')
112
+ }
113
+ })
114
+ })
115
+
116
+ server.middlewares.use(CONFIG_API, (req, res, next) => {
117
+ if (req.method === 'GET') {
118
+ void (async () => {
119
+ const local = await readConfigRecord(localConfigPath, 'local')
120
+ if (local) {
121
+ writeJson(res, 200, local)
122
+ return
123
+ }
124
+ const example = await readConfigRecord(exampleConfigPath, 'example')
125
+ if (example) {
126
+ writeJson(res, 200, example)
127
+ return
128
+ }
129
+ writeJson(res, 200, { version: 1, module: {}, updatedAt: null, source: 'empty' })
130
+ })()
131
+ return
132
+ }
133
+
134
+ if (req.method === 'PUT') {
135
+ readBody(req, res, (body) => {
136
+ void (async () => {
137
+ try {
138
+ const parsed = JSON.parse(body)
139
+ const record = {
140
+ version: Number(parsed.version ?? 1),
141
+ module: parsed.module ?? {},
142
+ updatedAt: new Date().toISOString(),
143
+ source: 'local',
144
+ }
145
+ await writeJsonAtomic(localConfigPath, record)
146
+ writeJson(res, 200, record)
147
+ } catch {
148
+ res.statusCode = 400
149
+ res.end('Invalid JSON')
150
+ }
151
+ })()
152
+ })
153
+ return
154
+ }
155
+
156
+ if (req.method === 'DELETE') {
157
+ void (async () => {
158
+ try {
159
+ if (fs.existsSync(localConfigPath)) {
160
+ await fsp.unlink(localConfigPath)
161
+ }
162
+ res.statusCode = 204
163
+ res.end()
164
+ } catch {
165
+ res.statusCode = 500
166
+ res.end('Delete failed')
167
+ }
168
+ })()
169
+ return
170
+ }
171
+
172
+ next()
173
+ })
174
+
175
+ console.log(
176
+ `\n\x1b[90m[module-config-store]\x1b[0m ${moduleConfigDir}/config/module-config.local.json + ${moduleConfigDir}/assets/branding/\n`,
177
+ )
178
+ },
179
+ }
180
+ }
181
+
182
+ function writeJson(res, status, payload) {
183
+ res.statusCode = status
184
+ res.setHeader('Content-Type', 'application/json')
185
+ res.end(JSON.stringify(payload))
186
+ }
187
+
188
+ function ensureDirSync(dir) {
189
+ if (!fs.existsSync(dir)) {
190
+ fs.mkdirSync(dir, { recursive: true })
191
+ }
192
+ }
193
+
194
+ function sanitizeFilename(name) {
195
+ const base = path.basename(name).replace(/[^a-zA-Z0-9._-]/g, '-')
196
+ return base || 'upload.bin'
197
+ }
198
+
199
+ function readBody(req, res, onEnd) {
200
+ const chunks = []
201
+ let size = 0
202
+ req.on('data', (chunk) => {
203
+ size += chunk.length
204
+ if (size > MAX_BODY_BYTES) {
205
+ res.statusCode = 413
206
+ res.end('Payload too large')
207
+ req.destroy()
208
+ return
209
+ }
210
+ chunks.push(chunk)
211
+ })
212
+ req.on('end', () => {
213
+ onEnd(Buffer.concat(chunks).toString('utf8'))
214
+ })
215
+ req.on('error', () => {
216
+ res.statusCode = 500
217
+ res.end('Request error')
218
+ })
219
+ }
220
+
221
+ async function readConfigRecord(filePath, source) {
222
+ try {
223
+ const raw = await fsp.readFile(filePath, 'utf8')
224
+ const parsed = JSON.parse(raw)
225
+ return {
226
+ version: Number(parsed.version ?? 1),
227
+ module: parsed.module ?? {},
228
+ updatedAt: parsed.updatedAt ?? null,
229
+ source,
230
+ }
231
+ } catch {
232
+ return null
233
+ }
234
+ }
235
+
236
+ async function writeJsonAtomic(filePath, record) {
237
+ const dir = path.dirname(filePath)
238
+ ensureDirSync(dir)
239
+ const tmpPath = `${filePath}.tmp`
240
+ await fsp.writeFile(tmpPath, JSON.stringify(record, null, 2), 'utf8')
241
+ await fsp.rename(tmpPath, filePath)
242
+ }
@@ -0,0 +1,9 @@
1
+ declare module './scripts/vite/sdk-log-bridge.js' {
2
+ import type { Plugin } from 'vite'
3
+ export default function sdkLogBridge(): Plugin
4
+ }
5
+
6
+ declare module './scripts/vite/module-config-store.js' {
7
+ import type { Plugin } from 'vite'
8
+ export default function moduleConfigStore(projectRoot?: string): Plugin
9
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Shared Vite options for HubSpot portal builds (CDN + module).
3
+ */
4
+ import { dirname, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import tailwindPrefixPlugin from '../tailwind/prefix-plugin.js'
7
+ import TailwindContentPlugin from '../tailwind/tailwind-content-plugin.js'
8
+ import isPrefix from '../tailwind/prefix.env.js'
9
+ import { portalBuildId as resolveBuildId } from '../resolve-portal-build-id.mjs'
10
+ import { nodeEnvForPortalMode } from '../portal-env-mode.mjs'
11
+ import { findProjectRoot } from '../project-root.mjs'
12
+ import { importConsumerDep } from '../resolve-consumer-dep.mjs'
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url))
15
+ const root = findProjectRoot()
16
+
17
+ const [{ default: viteReact }, { TanStackRouterVite }, { default: tailwindcss }] =
18
+ await Promise.all([
19
+ importConsumerDep('@vitejs/plugin-react'),
20
+ importConsumerDep('@tanstack/router-plugin/vite'),
21
+ importConsumerDep('@tailwindcss/vite'),
22
+ ])
23
+
24
+ export function portalBuildId() {
25
+ return resolveBuildId()
26
+ }
27
+
28
+ export function portalDefine(mode) {
29
+ return {
30
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnvForPortalMode(mode)),
31
+ 'process.env': {},
32
+ process: JSON.stringify({ env: {} }),
33
+ __PORTAL_BUILD_ID__: JSON.stringify(portalBuildId()),
34
+ }
35
+ }
36
+
37
+ function prodDevModuleConfigRouteStub(mode) {
38
+ if (!isPrefix(mode)) {
39
+ return null
40
+ }
41
+ const stub = resolve(root, 'src/routes/dev.module-config.prod-stub.tsx')
42
+ return {
43
+ name: 'woodsportal-prod-dev-route-stub',
44
+ enforce: 'pre',
45
+ resolveId(source) {
46
+ if (
47
+ source === './routes/dev.module-config' ||
48
+ source.endsWith('/routes/dev.module-config') ||
49
+ source.endsWith('/routes/dev.module-config.tsx')
50
+ ) {
51
+ return stub
52
+ }
53
+ return null
54
+ },
55
+ }
56
+ }
57
+
58
+ export function portalPlugins(mode, { codeSplitting = false, routeFileIgnorePattern } = {}) {
59
+ const routeIgnore = routeFileIgnorePattern ?? (isPrefix(mode) ? 'dev\\.' : undefined)
60
+ return [
61
+ prodDevModuleConfigRouteStub(mode),
62
+ TanStackRouterVite({
63
+ autoCodeSplitting: codeSplitting,
64
+ ...(routeIgnore ? { routeFileIgnorePattern: routeIgnore } : {}),
65
+ }),
66
+ viteReact({
67
+ babel: {
68
+ plugins: ['babel-plugin-react-compiler'],
69
+ },
70
+ }),
71
+ tailwindcss(),
72
+ ...(isPrefix(mode)
73
+ ? [tailwindPrefixPlugin(), TailwindContentPlugin()]
74
+ : []),
75
+ ]
76
+ }
77
+
78
+ export function portalResolve(mode) {
79
+ const kitRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
80
+ /** @type {import('vite').AliasOptions} */
81
+ const alias = [
82
+ {
83
+ find: '@wphs/adapter',
84
+ replacement: resolve(root, './wphs.adapter.ts'),
85
+ },
86
+ {
87
+ find: '@/dev/module-config',
88
+ replacement: resolve(kitRoot, 'src/dev-module-config'),
89
+ },
90
+ {
91
+ find: '@',
92
+ replacement: resolve(root, './src'),
93
+ },
94
+ ]
95
+
96
+ const rumEnabled =
97
+ process.env.VITE_DATADOG_RUM_ENABLED === 'true' ||
98
+ process.env.VITE_DATADOG_RUM_ENABLED === '1'
99
+
100
+ if (!rumEnabled) {
101
+ alias.push({
102
+ find: '@datadog/browser-rum',
103
+ replacement: resolve(root, 'src/integrations/datadog-browser-rum-stub.ts'),
104
+ })
105
+ }
106
+
107
+ return { alias }
108
+ }
109
+
110
+ export { root, __dirname }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared @vitejs/plugin-react options for local dev.
3
+ * Kit dev-module-config must skip react-compiler (CJS/ESM interop when loaded from kit path)
4
+ * but still use the React plugin's Babel JSX pipeline — do not use plugin `exclude`.
5
+ */
6
+ const DEV_KIT_UI_RE = /[/\\]dev-module-config[/\\]|[/\\]\.kit-bootstrap\.tsx$|[/\\]dev[/\\]bootstrap\.tsx$/
7
+
8
+ export function createDevReactPluginOptions() {
9
+ return {
10
+ babel: {
11
+ plugins: [
12
+ [
13
+ 'babel-plugin-react-compiler',
14
+ {
15
+ sources: (filename) => !DEV_KIT_UI_RE.test(filename),
16
+ },
17
+ ],
18
+ ],
19
+ },
20
+ }
21
+ }
@@ -0,0 +1,31 @@
1
+ import { createRequire } from 'node:module'
2
+ import { resolve } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ /**
6
+ * When Vite transforms files under @woodsportal/hubspot-kit, bare imports must
7
+ * resolve from the consumer project's node_modules (not the kit package root).
8
+ */
9
+ export default function resolveFromProjectRoot(projectRoot, kitRoot) {
10
+ const require = createRequire(pathToFileURL(resolve(projectRoot, 'package.json')))
11
+ const kitRootNorm = resolve(kitRoot)
12
+
13
+ return {
14
+ name: 'wp-resolve-from-project-root',
15
+ enforce: 'pre',
16
+ resolveId(source, importer) {
17
+ if (!importer) return null
18
+ if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0')) {
19
+ return null
20
+ }
21
+ if (!resolve(importer).startsWith(kitRootNorm)) {
22
+ return null
23
+ }
24
+ try {
25
+ return require.resolve(source)
26
+ } catch {
27
+ return null
28
+ }
29
+ },
30
+ }
31
+ }
@@ -0,0 +1,104 @@
1
+ /** @typedef {import('vite').Plugin} Plugin */
2
+
3
+ const SDK_LOG_PATH = '/__sdk-log'
4
+
5
+ const LEVEL_STYLE = {
6
+ debug: { color: '\x1b[90m', label: 'DEBUG' },
7
+ info: { color: '\x1b[36m', label: 'INFO ' },
8
+ warn: { color: '\x1b[33m', label: 'WARN ' },
9
+ error: { color: '\x1b[31m', label: 'ERROR' },
10
+ }
11
+
12
+ /**
13
+ * Dev-only Vite middleware: browser SDK logs → Vite terminal.
14
+ * Active when running `yarn dev` / `vite serve` in development mode.
15
+ *
16
+ * @returns {Plugin}
17
+ */
18
+ export default function sdkLogBridge() {
19
+ return {
20
+ name: 'woodsportal-sdk-log-bridge',
21
+ apply: 'serve',
22
+ configureServer(server) {
23
+ server.middlewares.use(SDK_LOG_PATH, (req, res, next) => {
24
+ if (req.method !== 'POST') {
25
+ if (req.method === 'GET' || req.method === 'HEAD') {
26
+ res.statusCode = 405
27
+ res.setHeader('Allow', 'POST')
28
+ res.end('Method Not Allowed')
29
+ return
30
+ }
31
+ next()
32
+ return
33
+ }
34
+
35
+ const chunks = []
36
+ req.on('data', (chunk) => chunks.push(chunk))
37
+ req.on('end', () => {
38
+ try {
39
+ const body = Buffer.concat(chunks).toString('utf8')
40
+ const entry = JSON.parse(body)
41
+ printSdkLog(entry)
42
+ res.statusCode = 204
43
+ res.end()
44
+ } catch {
45
+ res.statusCode = 400
46
+ res.end('Invalid JSON')
47
+ }
48
+ })
49
+ req.on('error', () => {
50
+ res.statusCode = 500
51
+ res.end('Request error')
52
+ })
53
+ })
54
+
55
+ console.log(
56
+ '\n\x1b[90m[sdk-log-bridge]\x1b[0m WoodsPortal SDK runtime logs → this terminal\n',
57
+ )
58
+ },
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @param {{ level?: string, context?: string, message?: string, meta?: unknown, error?: unknown }} entry
64
+ */
65
+ function printSdkLog(entry) {
66
+ const level = entry.level ?? 'info'
67
+ const style = LEVEL_STYLE[level] ?? LEVEL_STYLE.info
68
+ const context = entry.context ?? 'sdk'
69
+ const reset = '\x1b[0m'
70
+ const dim = '\x1b[90m'
71
+ const head = `${style.color}${style.label}${reset} ${dim}[woodsportal-sdk]${reset} ${context}`
72
+
73
+ if (level === 'error') {
74
+ const parts = [head]
75
+ if (entry.error != null) {
76
+ parts.push(formatPayload(entry.error))
77
+ }
78
+ if (entry.meta != null && Object.keys(entry.meta).length > 0) {
79
+ parts.push(formatPayload(entry.meta))
80
+ }
81
+ console.error(parts.join(' '))
82
+ return
83
+ }
84
+
85
+ const line = [head]
86
+ if (entry.message) {
87
+ line.push(entry.message)
88
+ }
89
+ if (entry.meta != null && Object.keys(entry.meta).length > 0) {
90
+ line.push(formatPayload(entry.meta))
91
+ }
92
+ console.log(line.join(' '))
93
+ }
94
+
95
+ function formatPayload(value) {
96
+ if (typeof value === 'string') {
97
+ return value
98
+ }
99
+ try {
100
+ return JSON.stringify(value)
101
+ } catch {
102
+ return String(value)
103
+ }
104
+ }
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HubSpot CMS watch — uses woodscli.json upload paths.
4
+ */
5
+ import { spawn, spawnSync } from 'node:child_process'
6
+ import { watch } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+
9
+ import { moduleOutDir, moduleUploadPath, readWoodsportalConfig, themeUploadPath } from './read-woodsportal-config.mjs'
10
+
11
+ const { root, config } = readWoodsportalConfig()
12
+ const rebuild = process.argv.includes('--rebuild')
13
+ const isProd = process.argv.includes('--prod')
14
+ const buildScript = isProd ? 'build:prod' : 'build:dev'
15
+
16
+ const isTheme = config?.projectType === 'theme-starter'
17
+ const hubspotModulePath = moduleOutDir(root)
18
+ const hubspotUploadTarget = moduleUploadPath(root)
19
+ const theme = themeUploadPath(root)
20
+
21
+ const watchRoots = isTheme
22
+ ? [theme.srcDir]
23
+ : ['src', 'public'].map((dir) => resolve(root, dir))
24
+
25
+ function log(msg) {
26
+ console.log(`[watch] ${msg}`)
27
+ }
28
+
29
+ function runYarn(script) {
30
+ const result = spawnSync('yarn', [script], { cwd: root, stdio: 'inherit' })
31
+ if (result.status !== 0) process.exit(result.status ?? 1)
32
+ }
33
+
34
+ function runBuild() {
35
+ if (isTheme) {
36
+ log('Theme project — no module build step')
37
+ return
38
+ }
39
+ log(`Running ${buildScript} …`)
40
+ runYarn(buildScript)
41
+ }
42
+
43
+ function startCmsWatch() {
44
+ const src = isTheme ? theme.srcDir : hubspotModulePath
45
+ const target = isTheme ? theme.uploadPath : hubspotUploadTarget
46
+ log(`Starting hs cms watch ${src} → ${target}`)
47
+ return spawn('hs', ['cms', 'watch', src, target], { cwd: root, stdio: 'inherit' })
48
+ }
49
+
50
+ function startRebuildWatcher() {
51
+ let debounceTimer = null
52
+ let building = false
53
+ let pending = false
54
+
55
+ const triggerBuild = () => {
56
+ if (building) {
57
+ pending = true
58
+ return
59
+ }
60
+ building = true
61
+ try {
62
+ runBuild()
63
+ } finally {
64
+ building = false
65
+ if (pending) {
66
+ pending = false
67
+ triggerBuild()
68
+ }
69
+ }
70
+ }
71
+
72
+ const scheduleBuild = () => {
73
+ if (debounceTimer) clearTimeout(debounceTimer)
74
+ debounceTimer = setTimeout(triggerBuild, 500)
75
+ }
76
+
77
+ for (const watchRoot of watchRoots) {
78
+ watch(watchRoot, { recursive: true }, () => scheduleBuild())
79
+ }
80
+ }
81
+
82
+ if (!isTheme) runBuild()
83
+ const cms = startCmsWatch()
84
+ if (rebuild && !isTheme) startRebuildWatcher()
85
+
86
+ cms.on('exit', (code) => process.exit(code ?? 0))