@woodsportal/hubspot-kit 1.0.5 → 1.0.26

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +4 -2
  3. package/dist/{chunk-JDGIAYHX.js → chunk-GTTIJVAR.js} +43 -3
  4. package/dist/chunk-GTTIJVAR.js.map +1 -0
  5. package/dist/cli.js +1378 -339
  6. package/dist/cli.js.map +1 -1
  7. package/dist/exec-U2OJATR3.js +12 -0
  8. package/dist/index.d.ts +20 -0
  9. package/dist/index.js +8 -1
  10. package/dist/index.js.map +1 -1
  11. package/examples/greenfield/module-hybrid-cdn/README.md +40 -3
  12. package/examples/greenfield/module-hybrid-cdn/public/fields.json +44 -1
  13. package/examples/greenfield/module-hybrid-cdn/src/app.tsx +8 -0
  14. package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
  15. package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
  16. package/examples/greenfield/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
  17. package/examples/greenfield/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
  18. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
  19. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
  20. package/examples/greenfield/module-hybrid-cdn/src/config/router.ts +4 -0
  21. package/examples/greenfield/module-hybrid-cdn/src/config/routes.ts +5 -0
  22. package/examples/greenfield/module-hybrid-cdn/src/defaultData.ts +2 -7
  23. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
  24. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
  25. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
  26. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
  27. package/examples/greenfield/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
  28. package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +47 -0
  29. package/examples/greenfield/module-hybrid-cdn/src/features/starter/index.ts +6 -0
  30. package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
  31. package/examples/greenfield/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
  32. package/examples/greenfield/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
  33. package/examples/greenfield/module-hybrid-cdn/src/main.tsx +4 -3
  34. package/examples/greenfield/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
  35. package/examples/greenfield/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
  36. package/examples/greenfield/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
  37. package/examples/greenfield/module-hybrid-cdn/src/routes/__root.tsx +15 -0
  38. package/examples/greenfield/module-hybrid-cdn/src/routes/guide.tsx +7 -0
  39. package/examples/greenfield/module-hybrid-cdn/src/routes/index.tsx +7 -0
  40. package/examples/greenfield/module-hybrid-cdn/tsconfig.json +20 -0
  41. package/examples/greenfield/module-monolith/public/fields.json +44 -1
  42. package/examples/greenfield/module-monolith/src/app.tsx +8 -0
  43. package/examples/greenfield/module-monolith/src/assets/css/main.dev.css +8 -0
  44. package/examples/greenfield/module-monolith/src/components/layout/section-card.tsx +20 -0
  45. package/examples/greenfield/module-monolith/src/components/layout/starter-nav.tsx +28 -0
  46. package/examples/greenfield/module-monolith/src/components/layout/starter-shell.tsx +18 -0
  47. package/examples/greenfield/module-monolith/src/config/router.ts +4 -0
  48. package/examples/greenfield/module-monolith/src/config/routes.ts +5 -0
  49. package/examples/greenfield/module-monolith/src/defaultData.ts +2 -7
  50. package/examples/greenfield/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
  51. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
  52. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
  53. package/examples/greenfield/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
  54. package/examples/greenfield/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
  55. package/examples/greenfield/module-monolith/src/features/starter/hooks/use-starter-preview.ts +47 -0
  56. package/examples/greenfield/module-monolith/src/features/starter/index.ts +6 -0
  57. package/examples/greenfield/module-monolith/src/hubspot/default-data.ts +7 -0
  58. package/examples/greenfield/module-monolith/src/hubspot/types.ts +17 -0
  59. package/examples/greenfield/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
  60. package/examples/greenfield/module-monolith/src/main.tsx +5 -0
  61. package/examples/greenfield/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
  62. package/examples/greenfield/module-monolith/src/portal/mount-portal.tsx +12 -0
  63. package/examples/greenfield/module-monolith/src/routeTree.gen.ts +77 -0
  64. package/examples/greenfield/module-monolith/src/routes/__root.tsx +15 -0
  65. package/examples/greenfield/module-monolith/src/routes/guide.tsx +7 -0
  66. package/examples/greenfield/module-monolith/src/routes/index.tsx +7 -0
  67. package/examples/greenfield/module-monolith/tsconfig.json +20 -0
  68. package/package.json +1 -1
  69. package/scripts/check-hubspot-bundle-size.mjs +5 -2
  70. package/scripts/ensure-wphs-workspace.mjs +2 -1
  71. package/scripts/hubspot-module-postbuild.mjs +1 -1
  72. package/scripts/read-woodsportal-config.mjs +4 -1
  73. package/scripts/run-hubspot-module-postbuild.mjs +9 -4
  74. package/scripts/vite/bootstrap-shim-resolver.js +47 -0
  75. package/scripts/vite/ensure-dev-kit-shims.js +7 -1
  76. package/scripts/vite/react-dev-plugin-options.js +2 -1
  77. package/scripts/watch-hubspot.mjs +107 -17
  78. package/scripts/wphs-paths.mjs +1 -0
  79. package/scripts/write-audit-artifacts.mjs +2 -1
  80. package/src/dev/bootstrap-greenfield.tsx +17 -9
  81. package/src/dev-module-config/dev-module-display-name.ts +5 -0
  82. package/src/dev-module-config/ui/ModuleConfigPersistenceStatus.tsx +5 -5
  83. package/src/dev-module-config/ui/ModuleConfigShell.tsx +2 -1
  84. package/src/dev-module-config/ui/module-config-theme.css +6 -0
  85. package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +11 -5
  86. package/templates/module-hybrid-cdn/README.md +53 -3
  87. package/templates/module-hybrid-cdn/package.json +6 -2
  88. package/templates/module-hybrid-cdn/public/fields.json +44 -1
  89. package/templates/module-hybrid-cdn/src/app.tsx +8 -0
  90. package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
  91. package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
  92. package/templates/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
  93. package/templates/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
  94. package/templates/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
  95. package/templates/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
  96. package/templates/module-hybrid-cdn/src/config/router.ts +4 -0
  97. package/templates/module-hybrid-cdn/src/config/routes.ts +5 -0
  98. package/templates/module-hybrid-cdn/src/defaultData.ts +2 -7
  99. package/templates/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
  100. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
  101. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
  102. package/templates/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
  103. package/templates/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
  104. package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +42 -0
  105. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
  106. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +30 -0
  107. package/templates/module-hybrid-cdn/src/features/starter/index.ts +6 -0
  108. package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
  109. package/templates/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
  110. package/templates/module-hybrid-cdn/src/hubspot/module-entry.tsx +9 -0
  111. package/templates/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
  112. package/templates/module-hybrid-cdn/src/main.tsx +4 -3
  113. package/templates/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
  114. package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
  115. package/templates/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
  116. package/templates/module-hybrid-cdn/src/routes/__root.tsx +15 -0
  117. package/templates/module-hybrid-cdn/src/routes/guide.tsx +7 -0
  118. package/templates/module-hybrid-cdn/src/routes/index.tsx +7 -0
  119. package/templates/module-hybrid-cdn/tsconfig.json +20 -0
  120. package/templates/module-hybrid-cdn/vite.config.monolith.mjs +8 -0
  121. package/templates/module-monolith/README.md +59 -0
  122. package/templates/module-monolith/index.html +1 -1
  123. package/templates/module-monolith/package.json +10 -3
  124. package/templates/module-monolith/public/fields.json +44 -1
  125. package/templates/module-monolith/public/module.html +1 -0
  126. package/templates/module-monolith/src/app.tsx +8 -0
  127. package/templates/module-monolith/src/assets/css/main.dev.css +6 -1
  128. package/templates/module-monolith/src/components/layout/section-card.tsx +20 -0
  129. package/templates/module-monolith/src/components/layout/starter-nav.tsx +28 -0
  130. package/templates/module-monolith/src/components/layout/starter-shell.tsx +18 -0
  131. package/templates/module-monolith/src/config/router.ts +4 -0
  132. package/templates/module-monolith/src/config/routes.ts +5 -0
  133. package/templates/module-monolith/src/defaultData.ts +2 -7
  134. package/templates/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
  135. package/templates/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
  136. package/templates/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
  137. package/templates/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
  138. package/templates/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
  139. package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +42 -0
  140. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
  141. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.ts +30 -0
  142. package/templates/module-monolith/src/features/starter/index.ts +6 -0
  143. package/templates/module-monolith/src/hubspot/default-data.ts +7 -0
  144. package/templates/module-monolith/src/hubspot/module-entry.tsx +9 -0
  145. package/templates/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
  146. package/templates/module-monolith/src/main.tsx +4 -3
  147. package/templates/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
  148. package/templates/module-monolith/src/portal/mount-portal.tsx +12 -0
  149. package/templates/module-monolith/src/routeTree.gen.ts +77 -0
  150. package/templates/module-monolith/src/routes/__root.tsx +15 -0
  151. package/templates/module-monolith/src/routes/guide.tsx +7 -0
  152. package/templates/module-monolith/src/routes/index.tsx +7 -0
  153. package/templates/module-monolith/tsconfig.json +20 -0
  154. package/templates/module-monolith/vite.config.monolith.mjs +8 -0
  155. package/templates/module-monolith/woodscli.json +1 -0
  156. package/templates/shared/.env.dev.example +5 -0
  157. package/templates/shared/.env.prod.example +5 -0
  158. package/templates/shared/.env.stg.example +3 -0
  159. package/templates/shared/portal.cdn.json.example +8 -0
  160. package/templates/theme-starter/package.json +5 -2
  161. package/vite/vite.config.dev.mjs +9 -1
  162. package/vite/vite.config.monolith.mjs +22 -17
  163. package/dist/chunk-JDGIAYHX.js.map +0 -1
  164. package/dist/exec-K5C6LZP4.js +0 -10
  165. /package/dist/{exec-K5C6LZP4.js.map → exec-U2OJATR3.js.map} +0 -0
@@ -3,15 +3,21 @@
3
3
  * HubSpot CMS watch — uses woodscli.json upload paths.
4
4
  */
5
5
  import { spawn, spawnSync } from 'node:child_process'
6
- import { watch } from 'node:fs'
7
- import { resolve } from 'node:path'
6
+ import { existsSync, watch } from 'node:fs'
7
+ import { dirname, resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
8
9
 
9
10
  import { moduleOutDir, moduleUploadPath, readWoodsportalConfig, themeUploadPath } from './read-woodsportal-config.mjs'
10
11
 
12
+ const kitScriptsDir = dirname(fileURLToPath(import.meta.url))
13
+ const kitRoot = resolve(kitScriptsDir, '..')
14
+ const kitCli = resolve(kitRoot, 'dist/cli.js')
15
+
16
+ const argv = process.argv.slice(2)
17
+ const rebuild = argv.includes('--rebuild')
18
+ const tier = parseTier(argv)
19
+
11
20
  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
21
 
16
22
  const isTheme = config?.projectType === 'theme-starter'
17
23
  const hubspotModulePath = moduleOutDir(root)
@@ -22,13 +28,85 @@ const watchRoots = isTheme
22
28
  ? [theme.srcDir]
23
29
  : ['src', 'public'].map((dir) => resolve(root, dir))
24
30
 
31
+ /** @type {import('node:child_process').ChildProcess | null} */
32
+ let cmsChild = null
33
+ /** @type {ReturnType<typeof setTimeout> | null} */
34
+ let debounceTimer = null
35
+ /** @type {import('node:fs').FSWatcher[]} */
36
+ const fsWatchers = []
37
+ let shuttingDown = false
38
+
39
+ function parseTier(args) {
40
+ const idx = args.indexOf('--tier')
41
+ if (idx >= 0 && args[idx + 1]) {
42
+ const value = args[idx + 1]
43
+ if (value === 'dev' || value === 'stg' || value === 'prod') return value
44
+ }
45
+ if (args.includes('--prod')) return 'prod'
46
+ if (args.includes('--stg')) return 'stg'
47
+ return 'dev'
48
+ }
49
+
25
50
  function log(msg) {
26
51
  console.log(`[watch] ${msg}`)
27
52
  }
28
53
 
29
- function runYarn(script) {
30
- const result = spawnSync('yarn', [script], { cwd: root, stdio: 'inherit' })
31
- if (result.status !== 0) process.exit(result.status ?? 1)
54
+ function shutdown(signal) {
55
+ if (shuttingDown) return
56
+ shuttingDown = true
57
+
58
+ if (debounceTimer) {
59
+ clearTimeout(debounceTimer)
60
+ debounceTimer = null
61
+ }
62
+
63
+ for (const watcher of fsWatchers) {
64
+ try {
65
+ watcher.close()
66
+ } catch {
67
+ // ignore
68
+ }
69
+ }
70
+
71
+ if (cmsChild && !cmsChild.killed) {
72
+ cmsChild.kill(signal)
73
+ return
74
+ }
75
+
76
+ process.exit(signal === 'SIGINT' ? 130 : 143)
77
+ }
78
+
79
+ process.on('SIGINT', () => shutdown('SIGINT'))
80
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
81
+
82
+ /**
83
+ * Run wp build from the same @woodsportal/hubspot-kit package as this script.
84
+ * Avoids consumer node_modules/.bin/wp (often an older pin with wrong outDir).
85
+ */
86
+ function runWpBuild() {
87
+ const env = { ...process.env, WP_HS_PROJECT_ROOT: root }
88
+
89
+ let command = process.execPath
90
+ let args = []
91
+
92
+ if (existsSync(kitCli)) {
93
+ args = [kitCli, 'build', '--tier', tier, '--quiet']
94
+ } else {
95
+ const localWp = resolve(root, 'node_modules', '.bin', 'wp')
96
+ command = existsSync(localWp) ? localWp : 'wp'
97
+ args = ['build', '--tier', tier, '--quiet']
98
+ }
99
+
100
+ log(`Running wp build --tier ${tier} …`)
101
+ const result = spawnSync(command, args, {
102
+ cwd: root,
103
+ stdio: 'inherit',
104
+ env,
105
+ shell: command === 'wp',
106
+ })
107
+ if (result.status !== 0) {
108
+ process.exit(result.status ?? 1)
109
+ }
32
110
  }
33
111
 
34
112
  function runBuild() {
@@ -36,19 +114,22 @@ function runBuild() {
36
114
  log('Theme project — no module build step')
37
115
  return
38
116
  }
39
- log(`Running ${buildScript} …`)
40
- runYarn(buildScript)
117
+ runWpBuild()
41
118
  }
42
119
 
43
120
  function startCmsWatch() {
44
121
  const src = isTheme ? theme.srcDir : hubspotModulePath
45
122
  const target = isTheme ? theme.uploadPath : hubspotUploadTarget
46
123
  log(`Starting hs cms watch ${src} → ${target}`)
47
- return spawn('hs', ['cms', 'watch', src, target], { cwd: root, stdio: 'inherit' })
124
+ cmsChild = spawn('hs', ['cms', 'watch', src, target], { cwd: root, stdio: 'inherit' })
125
+ cmsChild.on('exit', (code) => {
126
+ if (shuttingDown) return
127
+ process.exit(code ?? 0)
128
+ })
129
+ return cmsChild
48
130
  }
49
131
 
50
132
  function startRebuildWatcher() {
51
- let debounceTimer = null
52
133
  let building = false
53
134
  let pending = false
54
135
 
@@ -75,12 +156,21 @@ function startRebuildWatcher() {
75
156
  }
76
157
 
77
158
  for (const watchRoot of watchRoots) {
78
- watch(watchRoot, { recursive: true }, () => scheduleBuild())
159
+ const watcher = watch(watchRoot, { recursive: true }, () => scheduleBuild())
160
+ fsWatchers.push(watcher)
161
+ log(`Watching ${watchRoot}`)
79
162
  }
80
163
  }
81
164
 
82
- if (!isTheme) runBuild()
83
- const cms = startCmsWatch()
84
- if (rebuild && !isTheme) startRebuildWatcher()
165
+ if (!isTheme) {
166
+ const moduleJs = resolve(hubspotModulePath, 'module.js')
167
+ if (!existsSync(moduleJs)) {
168
+ log('Module dist missing — building before watch')
169
+ runBuild()
170
+ } else if (rebuild) {
171
+ runBuild()
172
+ }
173
+ }
85
174
 
86
- cms.on('exit', (code) => process.exit(code ?? 0))
175
+ startCmsWatch()
176
+ if (rebuild && !isTheme) startRebuildWatcher()
@@ -68,6 +68,7 @@ export function wphsPaths(projectRoot) {
68
68
  stateDir: join(workspaceRoot, 'state'),
69
69
  kitModuleConfigLink: join(workspaceRoot, '.kit-module-config'),
70
70
  kitBootstrapLink: join(workspaceRoot, '.kit-bootstrap.tsx'),
71
+ kitBootstrapGreenfieldLink: join(workspaceRoot, '.kit-bootstrap-greenfield.tsx'),
71
72
  readmePath: join(workspaceRoot, 'README.md'),
72
73
  brandingAssetsUrlPrefix: `/${workspaceDir}/assets/branding/`,
73
74
  legacyBrandingAssetsUrlPrefixes: [
@@ -6,6 +6,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
6
6
  import { resolve, dirname } from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
8
  import { findProjectRoot } from './project-root.mjs'
9
+ import { moduleOutDir } from './read-woodsportal-config.mjs'
9
10
  import { gzipSync } from 'node:zlib'
10
11
 
11
12
  const root = findProjectRoot()
@@ -33,7 +34,7 @@ const report = {
33
34
  cdn: {},
34
35
  }
35
36
 
36
- const moduleDir = resolve(root, 'dist/WoodsPortal.Module')
37
+ const moduleDir = moduleOutDir(root)
37
38
  for (const name of ['module.js', 'module.css', 'module.html', 'fields.json']) {
38
39
  const path = resolve(moduleDir, name)
39
40
  if (existsSync(path)) {
@@ -3,6 +3,7 @@
3
3
  * Use after `wp init`; adopt `dev/bootstrap` when wiring a real WoodsPortal consumer app.
4
4
  */
5
5
  import { createRoot } from 'react-dom/client'
6
+ import type { ReactNode } from 'react'
6
7
 
7
8
  import {
8
9
  bindModuleConfigToHubSpotData,
@@ -16,7 +17,12 @@ import {
16
17
  } from '@/dev/module-config/ui/ModuleConfigOverlay'
17
18
  import wphsAdapter from '@wphs/adapter'
18
19
 
19
- export async function mountGreenfieldDev(): Promise<void> {
20
+ export type MountGreenfieldDevOptions = {
21
+ /** Scaffold-owned preview (e.g. templates/src/app/StarterPage.tsx). */
22
+ preview?: ReactNode
23
+ }
24
+
25
+ export async function mountGreenfieldDev(options: MountGreenfieldDevOptions = {}): Promise<void> {
20
26
  const rootElement = document.getElementById('app')
21
27
 
22
28
  try {
@@ -31,15 +37,17 @@ export async function mountGreenfieldDev(): Promise<void> {
31
37
  }
32
38
 
33
39
  createRoot(rootElement).render(
34
- <>
40
+ <div>
35
41
  <ModuleConfigOverlay />
36
- <main style={{ padding: '1.5rem', fontFamily: 'system-ui, sans-serif' }}>
37
- <h1>WoodsCLI dev</h1>
38
- <p>
39
- Edit HubSpot module fields with <kbd>Ctrl+Shift+Z</kbd> (Mac: <kbd>⌃⇧Z</kbd>).
40
- </p>
41
- </main>
42
- </>
42
+ {options.preview ?? (
43
+ <main style={{ padding: '1.5rem', fontFamily: 'system-ui, sans-serif' }}>
44
+ <h1>WoodsCLI dev</h1>
45
+ <p>
46
+ Edit HubSpot module fields with <kbd>Ctrl+Shift+Z</kbd> (Mac: <kbd>⌃⇧Z</kbd>).
47
+ </p>
48
+ </main>
49
+ )}
50
+ </div>
43
51
  )
44
52
  } catch (error) {
45
53
  const message = error instanceof Error ? error.message : String(error)
@@ -0,0 +1,5 @@
1
+ /** Module slug from woodscli.json — injected by Vite in local dev. */
2
+ export function devModuleDisplayName(): string {
3
+ const name = import.meta.env.VITE_WPHS_MODULE_NAME
4
+ return typeof name === 'string' && name.trim() ? name.trim() : 'module'
5
+ }
@@ -8,7 +8,7 @@ type ModuleConfigPersistenceStatusProps = {
8
8
  function StatusIcon({ variant }: { variant: 'dirty' | 'saved' | 'info' }) {
9
9
  if (variant === 'dirty') {
10
10
  return (
11
- <svg viewBox="0 0 16 16" className="h-4 w-4 shrink-0" fill="currentColor" aria-hidden>
11
+ <svg viewBox="0 0 16 16" className="mc-status-icon" fill="currentColor" aria-hidden>
12
12
  <circle cx="8" cy="8" r="3.5" />
13
13
  </svg>
14
14
  )
@@ -16,7 +16,7 @@ function StatusIcon({ variant }: { variant: 'dirty' | 'saved' | 'info' }) {
16
16
 
17
17
  if (variant === 'saved') {
18
18
  return (
19
- <svg viewBox="0 0 16 16" className="h-4 w-4 shrink-0" fill="none" aria-hidden>
19
+ <svg viewBox="0 0 16 16" className="mc-status-icon" fill="none" aria-hidden>
20
20
  <path
21
21
  d="M3.5 8.5 6.5 11.5 12.5 4.5"
22
22
  stroke="currentColor"
@@ -29,7 +29,7 @@ function StatusIcon({ variant }: { variant: 'dirty' | 'saved' | 'info' }) {
29
29
  }
30
30
 
31
31
  return (
32
- <svg viewBox="0 0 16 16" className="h-4 w-4 shrink-0" fill="none" aria-hidden>
32
+ <svg viewBox="0 0 16 16" className="mc-status-icon" fill="none" aria-hidden>
33
33
  <circle cx="8" cy="8" r="6.25" stroke="currentColor" strokeWidth="1.5" />
34
34
  <path d="M8 7v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
35
35
  <circle cx="8" cy="4.75" r="0.85" fill="currentColor" />
@@ -59,7 +59,7 @@ export function ModuleConfigPersistenceStatus({
59
59
  <StatusIcon variant="saved" />
60
60
  <div className="mc-persistence-status__body">
61
61
  <span className="mc-persistence-status__label">Saved locally</span>
62
- <code className="mc-persistence-status__path">.dev/module-config.local.json</code>
62
+ <code className="mc-persistence-status__path">.wphs/config/module-config.local.json</code>
63
63
  </div>
64
64
  </div>
65
65
  )
@@ -71,7 +71,7 @@ export function ModuleConfigPersistenceStatus({
71
71
  <StatusIcon variant="info" />
72
72
  <div className="mc-persistence-status__body">
73
73
  <span className="mc-persistence-status__label">Using example config</span>
74
- <code className="mc-persistence-status__path">.dev/module-config.example.json</code>
74
+ <code className="mc-persistence-status__path">.wphs/config/module-config.example.json</code>
75
75
  </div>
76
76
  </div>
77
77
  )
@@ -25,6 +25,7 @@ import { mcSurface } from './module-config-surfaces'
25
25
  import { ModuleConfigThemeToggle } from './ModuleConfigThemeToggle'
26
26
  import { ModuleConfigDockSideIcon } from './ModuleConfigDockSideIcon'
27
27
  import { ModuleConfigPersistenceStatus } from './ModuleConfigPersistenceStatus'
28
+ import { devModuleDisplayName } from '../dev-module-display-name'
28
29
 
29
30
  type Tab = 'CONTENT' | 'STYLE'
30
31
 
@@ -179,7 +180,7 @@ function ModuleConfigShellInner({
179
180
  ))}
180
181
  </nav>
181
182
  <div className="flex items-start justify-between gap-2">
182
- <h2 className={hsTypography.moduleTitle}>woodsportal-module</h2>
183
+ <h2 className={hsTypography.moduleTitle}>{devModuleDisplayName()}</h2>
183
184
  <div className="flex shrink-0 items-center gap-1">
184
185
  <ModuleConfigThemeToggle theme={theme} onToggle={toggleTheme} />
185
186
  {compact && onToggleOverlaySide ? (
@@ -277,6 +277,12 @@
277
277
  padding: 0.625rem 0.75rem;
278
278
  }
279
279
 
280
+ .mc-status-icon {
281
+ height: 1rem;
282
+ width: 1rem;
283
+ flex-shrink: 0;
284
+ }
285
+
280
286
  .mc-persistence-status__body {
281
287
  min-width: 0;
282
288
  flex: 1 1 0%;
@@ -7,6 +7,8 @@ import {
7
7
  type ReactNode,
8
8
  } from 'react'
9
9
 
10
+ import { devModuleDisplayName } from '../../dev-module-display-name'
11
+
10
12
  export type BreadcrumbSegment = {
11
13
  id: string
12
14
  label: string
@@ -27,6 +29,14 @@ type ModuleConfigNavContextValue = {
27
29
 
28
30
  const ROOT_SEGMENT: BreadcrumbSegment = { id: 'contents', label: 'Contents' }
29
31
 
32
+ function moduleTitleSegment(stackLength: number, popTo: (id: string) => void): BreadcrumbSegment {
33
+ return {
34
+ id: 'module',
35
+ label: devModuleDisplayName(),
36
+ onClick: stackLength > 1 ? () => popTo('module') : undefined,
37
+ }
38
+ }
39
+
30
40
  const ModuleConfigNavContext = createContext<ModuleConfigNavContextValue | null>(null)
31
41
 
32
42
  export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
@@ -124,11 +134,7 @@ export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
124
134
  }, [clearAllDrillDownPanels])
125
135
 
126
136
  const segments = useMemo(() => {
127
- const moduleTitle: BreadcrumbSegment = {
128
- id: 'module',
129
- label: 'woodsportal-module',
130
- onClick: stack.length > 1 ? () => popTo('module') : undefined,
131
- }
137
+ const moduleTitle = moduleTitleSegment(stack.length, popTo)
132
138
 
133
139
  if (stack.length <= 1) {
134
140
  return [ROOT_SEGMENT, moduleTitle]
@@ -1,9 +1,59 @@
1
- # HubSpot portal module (hybrid CDN)
1
+ # Getting started
2
2
 
3
- Scaffolded by `wp init --template module-hybrid-cdn`.
3
+ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
4
+
5
+ ## Daily dev
4
6
 
5
7
  ```bash
6
- npm install
8
+ yarn install
7
9
  wp doctor
8
10
  wp dev
9
11
  ```
12
+
13
+ Open the local URL, then **Ctrl+Shift+Z** (Mac **⌃⇧Z**) to edit module fields. Click **Apply changes** to refresh the starter preview.
14
+
15
+ ## Routing (`wp init`)
16
+
17
+ Default is **hash history** — same as `woodsportal-client-frontend` (`#/guide` URLs work in HubSpot CMS).
18
+
19
+ | `wp init --router` | Behavior |
20
+ |--------------------|----------|
21
+ | `hash` (default) | `createHashHistory()` — production-safe in HubSpot embeds |
22
+ | `browser` | Clean `/guide` URLs for local dev only |
23
+ | `none` | Single-page starter, no TanStack Router |
24
+
25
+ Recorded in `woodscli.json` → `scaffold.router`.
26
+
27
+ ## `src/` layout
28
+
29
+ | Path | Purpose |
30
+ |------|---------|
31
+ | `main.tsx` | Dev entry — loads styles and `mountDevPreview()` |
32
+ | `app.tsx` | `RouterProvider` shell (client-frontend pattern) |
33
+ | `config/router.ts` | Hash vs browser history |
34
+ | `portal/` | Dev vs production mount (`mount-dev-preview`, `mount-portal`) |
35
+ | `routes/` | TanStack file routes — keep thin; import from `features/` |
36
+ | `features/starter/` | Demo home + developer guide (replace with your app) |
37
+ | `components/` | Shared layout and UI |
38
+ | `integrations/` | Router and third-party wiring |
39
+ | `hubspot/` | HubSpot types and dev fixtures |
40
+ | `config/` | Route constants and app config |
41
+ | `assets/css/` | Tailwind entry for local dev |
42
+
43
+ Kit runtime expects `src/defaultData.ts` (re-exports `hubspot/default-data.ts`).
44
+
45
+ ## Docs (in the kit package)
46
+
47
+ After `npm install`, see:
48
+
49
+ - `node_modules/@woodsportal/hubspot-kit/docs/CLI-REFERENCE.md`
50
+ - `node_modules/@woodsportal/hubspot-kit/docs/MODULES.md`
51
+ - `node_modules/@woodsportal/hubspot-kit/docs/HUBSPOT-CLI-SETUP.md`
52
+
53
+ ## HubSpot upload
54
+
55
+ ```bash
56
+ cp hubspot.config.yml.example hubspot.config.yml
57
+ # add portal + personal access key
58
+ wp upload
59
+ ```
@@ -9,11 +9,15 @@
9
9
  "deploy:prod": "wp deploy --tier prod"
10
10
  },
11
11
  "devDependencies": {
12
- "@woodsportal/hubspot-kit": "^1.0.5",
12
+ "@woodsportal/hubspot-kit": "^1.0.11",
13
+ "@dnd-kit/core": "^6.3.1",
14
+ "@dnd-kit/sortable": "^10.0.0",
15
+ "@dnd-kit/utilities": "^3.2.2",
13
16
  "@tailwindcss/vite": "^4.0.6",
17
+ "@tanstack/react-router": "^1.121.2",
14
18
  "@tanstack/router-plugin": "^1.121.2",
15
19
  "@vitejs/plugin-react": "^4.3.4",
16
- "babel-plugin-react-compiler": "^1.0.0",
20
+ "babel-plugin-react-compiler": "^1.0.11",
17
21
  "react": "^19.0.0",
18
22
  "react-dom": "^19.0.0",
19
23
  "tailwindcss": "^4.0.6",
@@ -1 +1,44 @@
1
- [{"name":"title","label":"Title","type":"text","default":"WoodsCLI module"}]
1
+ [
2
+ {
3
+ "name": "title",
4
+ "label": "Portal title",
5
+ "type": "text",
6
+ "tab": "CONTENT",
7
+ "default": "My HubSpot Portal"
8
+ },
9
+ {
10
+ "name": "tagline",
11
+ "label": "Tagline",
12
+ "type": "text",
13
+ "tab": "CONTENT",
14
+ "help_text": "Subtitle shown under the hero title in the local dev preview.",
15
+ "default": "Customize this preview with Ctrl+Shift+Z, then replace StarterPage with your portal app."
16
+ },
17
+ {
18
+ "name": "show_feature_cards",
19
+ "label": "Show feature cards",
20
+ "type": "boolean",
21
+ "display": "toggle",
22
+ "tab": "CONTENT",
23
+ "default": true
24
+ },
25
+ {
26
+ "name": "accent_color",
27
+ "label": "Accent color",
28
+ "type": "color",
29
+ "tab": "STYLE",
30
+ "default": { "color": "#0091ae", "opacity": 100 }
31
+ },
32
+ {
33
+ "name": "hero_style",
34
+ "label": "Hero style",
35
+ "type": "choice",
36
+ "tab": "STYLE",
37
+ "choices": [
38
+ ["gradient", "Gradient"],
39
+ ["minimal", "Minimal"],
40
+ ["bold", "Bold"]
41
+ ],
42
+ "default": "gradient"
43
+ }
44
+ ]
@@ -0,0 +1,8 @@
1
+ import { RouterProvider } from '@tanstack/react-router'
2
+
3
+ import { router } from '@/integrations/tanstack-router/router'
4
+
5
+ /** Root app shell — same pattern as woodsportal-client-frontend `App`. */
6
+ export function App() {
7
+ return <RouterProvider router={router} />
8
+ }
@@ -1,3 +1,8 @@
1
1
  @import 'tailwindcss';
2
2
 
3
- @source '../../.wphs/**/*.{js,jsx,ts,tsx}';
3
+ /* Kit dev-module-config — paths relative to this file (src/assets/css/) */
4
+ @source '../../../.wphs/.kit-module-config/**/*.{ts,tsx}';
5
+ @source '../../../.wphs/.kit-bootstrap.tsx';
6
+ @source '../../../.wphs/.kit-bootstrap-greenfield.tsx';
7
+ @source '../../../src/**/*.{ts,tsx}';
8
+ @source '../../../node_modules/@woodsportal/hubspot-kit/src/dev-module-config/**/*.{ts,tsx}';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * CDN application chunk — mounts after vendor.js (HubSpot require_js order).
3
+ */
4
+ import { mountPortal } from '@/portal/mount-portal'
5
+
6
+ declare const __PORTAL_BUILD_ID__: string | undefined
7
+
8
+ declare global {
9
+ interface Window {
10
+ WoodsPortal?: {
11
+ mount: typeof mountPortal
12
+ version: string
13
+ }
14
+ }
15
+ }
16
+
17
+ const buildId =
18
+ typeof __PORTAL_BUILD_ID__ !== 'undefined' ? String(__PORTAL_BUILD_ID__) : 'dev'
19
+
20
+ window.WoodsPortal = {
21
+ mount: mountPortal,
22
+ version: buildId,
23
+ }
24
+
25
+ async function autoMount() {
26
+ if (!document.getElementById('app')) {
27
+ return
28
+ }
29
+ await mountPortal()
30
+ }
31
+
32
+ void autoMount()
@@ -0,0 +1,28 @@
1
+ /**
2
+ * CDN vendor chunk — React + ReactDOM on `window` for the app IIFE externals.
3
+ */
4
+ import React from 'react'
5
+ import * as ReactDOM from 'react-dom'
6
+ import * as ReactDOMClient from 'react-dom/client'
7
+ import * as jsxRuntime from 'react/jsx-runtime'
8
+ import * as jsxDevRuntime from 'react/jsx-dev-runtime'
9
+
10
+ declare global {
11
+ interface Window {
12
+ WoodsPortalVendor?: {
13
+ React: typeof React
14
+ ReactDOM: typeof ReactDOM
15
+ ReactDOMClient: typeof ReactDOMClient
16
+ jsxRuntime: typeof jsxRuntime
17
+ jsxDevRuntime: typeof jsxDevRuntime
18
+ }
19
+ }
20
+ }
21
+
22
+ window.WoodsPortalVendor = {
23
+ React,
24
+ ReactDOM,
25
+ ReactDOMClient,
26
+ jsxRuntime,
27
+ jsxDevRuntime,
28
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export function SectionCard({
4
+ title,
5
+ children,
6
+ className = '',
7
+ }: {
8
+ title: string
9
+ children: ReactNode
10
+ className?: string
11
+ }) {
12
+ return (
13
+ <section
14
+ className={`rounded-lg border border-[#cbd6e2] bg-white p-6 shadow-sm ${className}`}
15
+ >
16
+ <h2 className="text-base font-semibold text-[#33475b]">{title}</h2>
17
+ <div className="mt-4">{children}</div>
18
+ </section>
19
+ )
20
+ }
@@ -0,0 +1,28 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ import { ROUTES } from '@/config/routes'
4
+
5
+ export function StarterNav() {
6
+ return (
7
+ <header className="border-b border-[#cbd6e2] bg-white/90 backdrop-blur">
8
+ <div className="mx-auto flex max-w-4xl items-center justify-between gap-4 px-6 py-3 sm:px-10">
9
+ <span className="text-sm font-semibold text-[#0091ae]">WoodsCLI starter</span>
10
+ <nav className="flex gap-4 text-sm font-medium">
11
+ <Link
12
+ to={ROUTES.home}
13
+ className="text-[#516f90] hover:text-[#0091ae] [&.active]:text-[#0091ae]"
14
+ activeOptions={{ exact: true }}
15
+ >
16
+ Home
17
+ </Link>
18
+ <Link
19
+ to={ROUTES.guide}
20
+ className="text-[#516f90] hover:text-[#0091ae] [&.active]:text-[#0091ae]"
21
+ >
22
+ Developer guide
23
+ </Link>
24
+ </nav>
25
+ </div>
26
+ </header>
27
+ )
28
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { StarterNav } from './starter-nav'
4
+
5
+ export function StarterShell({
6
+ children,
7
+ showNav = true,
8
+ }: {
9
+ children: ReactNode
10
+ showNav?: boolean
11
+ }) {
12
+ return (
13
+ <div className="min-h-screen bg-[#eaf0f6] text-[#33475b]">
14
+ {showNav ? <StarterNav /> : null}
15
+ {children}
16
+ </div>
17
+ )
18
+ }
@@ -0,0 +1,4 @@
1
+ /** Router history — set by `wp init`. Hash matches woodsportal-client-frontend (HubSpot CMS embeds). */
2
+ export type RouterHistoryMode = 'hash' | 'browser'
3
+
4
+ export const ROUTER_HISTORY: RouterHistoryMode = 'hash'
@@ -0,0 +1,5 @@
1
+ /** Route paths — keep in sync with `src/routes/` file routes. */
2
+ export const ROUTES = {
3
+ home: '/',
4
+ guide: '/guide',
5
+ } as const
@@ -1,7 +1,2 @@
1
- /** Local dev fixtures for module-config runtime (greenfield wp init). */
2
- export const defaultData = {
3
- dataSourceSet: [],
4
- hubId: 'dev',
5
- userData: {},
6
- objectList: [] as unknown[],
7
- }
1
+ /** Kit runtime imports `@/defaultData` — re-export from the hubspot slice. */
2
+ export { devDefaultData as defaultData } from './hubspot/default-data'