@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,273 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Publish dist/cdn to the public GitHub CDN repo and tag for jsDelivr.
4
+ *
5
+ * Auth (pick one):
6
+ * PORTAL_CDN_PUBLISH_TOKEN — GitHub PAT with repo push on the CDN repo
7
+ * GITHUB_TOKEN — same, common in CI
8
+ * (none) — uses your local git credentials (ssh/https)
9
+ *
10
+ * Usage:
11
+ * yarn build:cdn:prod && yarn publish:cdn:prod
12
+ * yarn publish:cdn --dry-run
13
+ * yarn publish:cdn --skip-jsdelivr-check
14
+ */
15
+ import { spawnSync } from 'node:child_process'
16
+ import {
17
+ cpSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ readFileSync,
21
+ readdirSync,
22
+ rmSync,
23
+ writeFileSync,
24
+ } from 'node:fs'
25
+ import { dirname, join, resolve } from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ import { loadPortalCdnConfig, jsDelivrBaseUrl, root } from './load-portal-cdn-config.mjs'
29
+ import { resolvePortalBuildId } from './resolve-portal-build-id.mjs'
30
+
31
+ const args = new Set(process.argv.slice(2))
32
+ const dryRun = args.has('--dry-run')
33
+ const skipJsDelivrCheck = args.has('--skip-jsdelivr-check')
34
+
35
+ function log(msg) {
36
+ console.log(`[publish:cdn] ${msg}`)
37
+ }
38
+
39
+ function fail(msg) {
40
+ console.error(`[publish:cdn] ERROR: ${msg}`)
41
+ process.exit(1)
42
+ }
43
+
44
+ function run(cmd, cmdArgs, options = {}) {
45
+ const result = spawnSync(cmd, cmdArgs, {
46
+ stdio: 'inherit',
47
+ ...options,
48
+ })
49
+ if (result.status !== 0) {
50
+ fail(`Command failed: ${cmd} ${cmdArgs.join(' ')}`)
51
+ }
52
+ }
53
+
54
+ function runCapture(cmd, cmdArgs, options = {}) {
55
+ const result = spawnSync(cmd, cmdArgs, {
56
+ encoding: 'utf8',
57
+ ...options,
58
+ })
59
+ if (result.status !== 0) {
60
+ fail(`Command failed: ${cmd} ${cmdArgs.join(' ')}\n${result.stderr ?? ''}`)
61
+ }
62
+ return (result.stdout ?? '').trim()
63
+ }
64
+
65
+ function authRemote(remote, token) {
66
+ if (!token?.trim()) {
67
+ return remote
68
+ }
69
+ const t = token.trim()
70
+ if (remote.startsWith('https://github.com/')) {
71
+ return remote.replace('https://github.com/', `https://x-access-token:${t}@github.com/`)
72
+ }
73
+ if (remote.startsWith('git@github.com:')) {
74
+ log('Note: PORTAL_CDN_PUBLISH_TOKEN set but remote is SSH — using local SSH auth.')
75
+ }
76
+ return remote
77
+ }
78
+
79
+ function copyCdnArtifacts(sourceDir, targetDir) {
80
+ mkdirSync(targetDir, { recursive: true })
81
+ for (const entry of readdirSync(sourceDir)) {
82
+ const src = join(sourceDir, entry)
83
+ const dest = join(targetDir, entry)
84
+ rmSync(dest, { recursive: true, force: true })
85
+ cpSync(src, dest, { recursive: true })
86
+ }
87
+ }
88
+
89
+ function writeCdnReadme(repoRoot, buildId, config) {
90
+ const readmePath = join(repoRoot, 'README.md')
91
+ const base = jsDelivrBaseUrl(config.github, buildId, config.cdnPathInRepo)
92
+ const body = `# woodsportal-client-frontend-cdn
93
+
94
+ Public CDN assets for the WoodsPortal HubSpot module (**Strategy B — Hybrid CDN**).
95
+
96
+ **Do not edit manually.** Files are published from \`digitalwoods.io-woodsportal-client-frontend\` via \`yarn publish:cdn\`.
97
+
98
+ | | |
99
+ |---|---|
100
+ | Latest tag | \`${buildId}\` |
101
+ | jsDelivr base | ${base} |
102
+ | Source repo | [digitalwoods.io-woodsportal-client-frontend](https://github.com/Digital-Woods/digitalwoods.io-woodsportal-client-frontend) (private) |
103
+
104
+ ## jsDelivr URLs (tag \`${buildId}\`)
105
+
106
+ - \`${base}/vendor.${buildId}.js\`
107
+ - \`${base}/app.${buildId}.js\`
108
+ - \`${base}/app.${buildId}.css\`
109
+
110
+ Published at ${new Date().toISOString()}.
111
+ `
112
+ writeFileSync(readmePath, body, 'utf8')
113
+ }
114
+
115
+ async function checkJsDelivr(config, buildId, vendorFile, appFile, appCssFile) {
116
+ const base = jsDelivrBaseUrl(config.github, buildId, config.cdnPathInRepo)
117
+ for (const file of [appCssFile, vendorFile, appFile]) {
118
+ const url = `${base}/${file}`
119
+ log(`Checking jsDelivr (may take a minute on first publish): ${url}`)
120
+ try {
121
+ const res = await fetch(url, { method: 'HEAD' })
122
+ if (res.ok) {
123
+ log(` OK ${res.status} ${file}`)
124
+ } else {
125
+ log(` ${file} → HTTP ${res.status} (jsDelivr may still be propagating — wait 5–15 min)`)
126
+ }
127
+ } catch (err) {
128
+ log(` Could not reach jsDelivr for ${file}: ${err.message}`)
129
+ }
130
+ }
131
+ }
132
+
133
+ const config = loadPortalCdnConfig()
134
+ const buildId = resolvePortalBuildId()
135
+ const sourceCdn = resolve(root, 'dist/cdn')
136
+ const token = process.env.PORTAL_CDN_PUBLISH_TOKEN ?? process.env.GITHUB_TOKEN
137
+
138
+ if (!existsSync(sourceCdn)) {
139
+ fail('dist/cdn missing — run `yarn build:cdn` first')
140
+ }
141
+
142
+ const cdnFiles = readdirSync(sourceCdn)
143
+ const jsFiles = cdnFiles.filter((f) => f.endsWith('.js'))
144
+ const cssFiles = cdnFiles.filter((f) => f.endsWith('.css'))
145
+ const vendorFile = jsFiles.find((f) => f.startsWith('vendor.'))
146
+ const appFile = jsFiles.find((f) => f.startsWith('app.'))
147
+ const appCssFile = cssFiles.find((f) => f.startsWith('app.'))
148
+
149
+ if (!vendorFile || !appFile) {
150
+ fail(`dist/cdn must contain vendor.*.js and app.*.js — found: ${jsFiles.join(', ') || '(none)'}`)
151
+ }
152
+
153
+ if (!appCssFile) {
154
+ fail(`dist/cdn must contain app.*.css — run yarn build:cdn (component styles load via require_css)`)
155
+ }
156
+
157
+ if (!vendorFile.includes(buildId) || !appFile.includes(buildId) || !appCssFile.includes(buildId)) {
158
+ fail(
159
+ `Build id mismatch: .portal-build-id is "${buildId}" but chunks are ${vendorFile}, ${appFile}, ${appCssFile}. ` +
160
+ 'Update .portal-build-id and rebuild with `yarn build:cdn:prod` or `yarn build:cdn:dev`.',
161
+ )
162
+ }
163
+
164
+ const jsdelivrBase = jsDelivrBaseUrl(config.github, buildId, config.cdnPathInRepo)
165
+
166
+ const appSource = readFileSync(join(sourceCdn, appFile), 'utf8')
167
+ const bakedApis = [
168
+ ...new Set(
169
+ [...appSource.matchAll(/https:\/\/api(?:\.dev)?\.woodsportal\.com/g)].map((m) => m[0]),
170
+ ),
171
+ ]
172
+ log(` baked API URL(s): ${bakedApis.join(', ') || '(none found)'}`)
173
+ if (bakedApis.length === 1 && bakedApis[0] === 'https://api.woodsportal.com') {
174
+ log(' hint: prod API in app chunk — use `yarn deploy:prod` (not deploy:dev)')
175
+ } else if (bakedApis.length === 1 && bakedApis[0] === 'https://api.dev.woodsportal.com') {
176
+ log(' hint: dev API in app chunk — use `yarn deploy:dev` for HubSpot test portals')
177
+ }
178
+
179
+ log('Configuration')
180
+ log(` config file: ${config.configPath ?? '(defaults)'}`)
181
+ log(` build id / tag: ${buildId}`)
182
+ log(` GitHub (jsDelivr): ${config.github}`)
183
+ log(` git remote: ${config.gitRemote}`)
184
+ log(` target path: ${config.cdnPathInRepo}/`)
185
+ log(` jsDelivr base: ${jsdelivrBase}`)
186
+
187
+ if (dryRun) {
188
+ log('Dry run — would copy:')
189
+ for (const f of readdirSync(sourceCdn)) {
190
+ log(` dist/cdn/${f} → ${config.cdnPathInRepo}/${f}`)
191
+ }
192
+ log(`Would commit, tag ${buildId}, and push to ${config.defaultBranch}`)
193
+ process.exit(0)
194
+ }
195
+
196
+ const workDir = resolve(root, '.cache/portal-cdn-repo')
197
+ rmSync(workDir, { recursive: true, force: true })
198
+ mkdirSync(workDir, { recursive: true })
199
+
200
+ const remote = authRemote(config.gitRemote, token)
201
+ const branch = config.defaultBranch
202
+
203
+ log(`Cloning ${config.gitRemote} …`)
204
+ const cloneResult = spawnSync(
205
+ 'git',
206
+ ['clone', '--depth', '1', '--branch', branch, remote, workDir],
207
+ { stdio: 'pipe', encoding: 'utf8' },
208
+ )
209
+
210
+ if (cloneResult.status !== 0) {
211
+ log('Repo empty or branch missing — initializing a fresh checkout.')
212
+ rmSync(workDir, { recursive: true, force: true })
213
+ const bareClone = spawnSync('git', ['clone', remote, workDir], {
214
+ stdio: 'pipe',
215
+ encoding: 'utf8',
216
+ })
217
+ if (bareClone.status !== 0) {
218
+ mkdirSync(workDir, { recursive: true })
219
+ run('git', ['init'], { cwd: workDir })
220
+ run('git', ['remote', 'add', 'origin', remote], { cwd: workDir })
221
+ }
222
+ run('git', ['checkout', '-B', branch], { cwd: workDir })
223
+ }
224
+
225
+ const targetCdnDir = resolve(workDir, config.cdnPathInRepo)
226
+ copyCdnArtifacts(sourceCdn, targetCdnDir)
227
+ writeCdnReadme(workDir, buildId, config)
228
+
229
+ run('git', ['add', '-A'], { cwd: workDir })
230
+
231
+ const status = runCapture('git', ['status', '--porcelain'], { cwd: workDir })
232
+ if (!status) {
233
+ log('No file changes in CDN repo — tag may already exist. Attempting tag push only.')
234
+ } else {
235
+ run(
236
+ 'git',
237
+ ['commit', '-m', `release: ${buildId} portal CDN assets`],
238
+ { cwd: workDir },
239
+ )
240
+ }
241
+
242
+ const existingTag = spawnSync('git', ['rev-parse', buildId], {
243
+ cwd: workDir,
244
+ stdio: 'pipe',
245
+ encoding: 'utf8',
246
+ })
247
+
248
+ if (existingTag.status === 0) {
249
+ log(`Tag ${buildId} already exists locally — deleting and recreating on current commit.`)
250
+ run('git', ['tag', '-d', buildId], { cwd: workDir })
251
+ }
252
+
253
+ run('git', ['tag', '-a', buildId, '-m', `WoodsPortal CDN ${buildId}`], { cwd: workDir })
254
+ run('git', ['push', '-u', 'origin', branch], { cwd: workDir })
255
+ run('git', ['push', 'origin', `refs/tags/${buildId}`, '--force'], { cwd: workDir })
256
+
257
+ log('')
258
+ log('Published successfully.')
259
+ log(` GitHub: https://github.com/${config.github}/tree/${buildId}/${config.cdnPathInRepo}`)
260
+ log(` jsDelivr vendor: ${jsdelivrBase}/${vendorFile}`)
261
+ log(` jsDelivr app: ${jsdelivrBase}/${appFile}`)
262
+ log(` jsDelivr css: ${jsdelivrBase}/${appCssFile}`)
263
+ log('')
264
+ log('Next steps:')
265
+ log(' 1. Wait ~5–15 minutes for jsDelivr to index the new tag (first publish can be slower).')
266
+ log(' 2. yarn build:hubspot:dev # dev API + jsDelivr URLs')
267
+ log(' yarn build:hubspot:prod # prod API')
268
+ log(' 3. yarn upload:hubspot')
269
+ log(' Or: yarn deploy:dev | yarn deploy:prod')
270
+
271
+ if (!skipJsDelivrCheck) {
272
+ await checkJsDelivr(config, buildId, vendorFile, appFile, appCssFile)
273
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Read project config JSON for build/watch scripts (no zod — scripts only).
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
6
+
7
+ import { findProjectRootWithConfig, resolveProjectConfigPath } from './config-files.mjs'
8
+ import { wphsPaths } from './wphs-paths.mjs'
9
+
10
+ /**
11
+ * @param {string} [projectRoot]
12
+ */
13
+ export function readWoodsportalConfig(projectRoot) {
14
+ const root = projectRoot ?? findProjectRootWithConfig()
15
+ const configPath = resolveProjectConfigPath(root)
16
+ if (!configPath || !existsSync(configPath)) {
17
+ return { root, configPath: null, config: null, paths: wphsPaths(root) }
18
+ }
19
+ const config = JSON.parse(readFileSync(configPath, 'utf8'))
20
+ return { root, configPath, config, paths: wphsPaths(root) }
21
+ }
22
+
23
+ export function moduleOutDir(projectRoot) {
24
+ const { root, config } = readWoodsportalConfig(projectRoot)
25
+ return resolve(root, config?.module?.outDir ?? 'dist/WoodsPortal.Module')
26
+ }
27
+
28
+ export function moduleUploadPath(projectRoot) {
29
+ const { config } = readWoodsportalConfig(projectRoot)
30
+ return config?.module?.uploadPath ?? 'dw-modules/woodsportal-module.module'
31
+ }
32
+
33
+ export function themeUploadPath(projectRoot) {
34
+ const { root, config } = readWoodsportalConfig(projectRoot)
35
+ const srcDir = config?.theme?.srcDir ?? 'src/theme'
36
+ return {
37
+ srcDir: resolve(root, srcDir),
38
+ uploadPath: config?.theme?.uploadPath ?? 'themes/my-theme',
39
+ }
40
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolve CDN base URL from env (jsDelivr GitHub, custom, or local).
3
+ */
4
+ import { loadPortalCdnConfig } from './load-portal-cdn-config.mjs'
5
+ import { portalBuildId, resolvePortalBuildId } from './resolve-portal-build-id.mjs'
6
+
7
+ export { portalBuildId, resolvePortalBuildId }
8
+
9
+ export function resolveCdnBaseUrl(env = process.env) {
10
+ const config = loadPortalCdnConfig(env)
11
+ const explicit = env.VITE_PORTAL_CDN_BASE_URL?.replace(/\/$/, '')
12
+ if (explicit) {
13
+ return explicit
14
+ }
15
+
16
+ const mode = config.publishMode
17
+ const buildId = resolvePortalBuildId(env)
18
+
19
+ if (mode === 'local') {
20
+ const port = env.VITE_PORTAL_CDN_LOCAL_PORT ?? '4173'
21
+ return `http://localhost:${port}/dist/cdn`
22
+ }
23
+
24
+ if (mode === 'custom') {
25
+ throw new Error(
26
+ 'VITE_PORTAL_CDN_PUBLISH_MODE=custom requires VITE_PORTAL_CDN_BASE_URL',
27
+ )
28
+ }
29
+
30
+ const cdnPath = config.cdnPathInRepo.replace(/^\//, '').replace(/\/$/, '')
31
+ return `https://cdn.jsdelivr.net/gh/${config.github}@${buildId}/${cdnPath}`
32
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Resolve and import packages from the consumer project (woodscli.json root).
3
+ * Kit scripts live under @woodsportal/hubspot-kit; Vite/React plugins are consumer deps.
4
+ */
5
+ import { createRequire } from 'node:module'
6
+ import { resolve } from 'node:path'
7
+ import { pathToFileURL } from 'node:url'
8
+ import { findProjectRoot } from './project-root.mjs'
9
+
10
+ /**
11
+ * @param {string} specifier - package name or subpath export
12
+ */
13
+ export function resolveConsumerDep(specifier) {
14
+ const root = findProjectRoot()
15
+ const require = createRequire(resolve(root, 'package.json'))
16
+ return require.resolve(specifier)
17
+ }
18
+
19
+ /**
20
+ * @param {string} specifier
21
+ */
22
+ export async function importConsumerDep(specifier) {
23
+ const resolved = resolveConsumerDep(specifier)
24
+ return import(pathToFileURL(resolved).href)
25
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Single source of truth for PORTAL_BUILD_ID (CDN chunk filenames + jsDelivr pin).
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
6
+
7
+ import { findProjectRoot } from './project-root.mjs'
8
+ import { resolveProjectConfigPath } from './config-files.mjs'
9
+
10
+ let cached
11
+
12
+ function getBuildIdFilePath(projectRoot) {
13
+ const configPath = resolveProjectConfigPath(projectRoot)
14
+ if (configPath) {
15
+ try {
16
+ const config = JSON.parse(readFileSync(configPath, 'utf8'))
17
+ if (config.cdn?.buildIdFile) {
18
+ return resolve(projectRoot, config.cdn.buildIdFile)
19
+ }
20
+ } catch {
21
+ // ignore invalid config
22
+ }
23
+ }
24
+ return resolve(projectRoot, '.portal-build-id')
25
+ }
26
+
27
+ function readBuildIdFile(projectRoot) {
28
+ const buildIdFile = getBuildIdFilePath(projectRoot)
29
+ if (!existsSync(buildIdFile)) {
30
+ return undefined
31
+ }
32
+ const line = readFileSync(buildIdFile, 'utf8')
33
+ .split('\n')
34
+ .map((l) => l.trim())
35
+ .find((l) => l.length > 0 && !l.startsWith('#'))
36
+ return line || undefined
37
+ }
38
+
39
+ /**
40
+ * @param {NodeJS.ProcessEnv} [env]
41
+ */
42
+ export function resolvePortalBuildId(env = process.env) {
43
+ if (env.PORTAL_BUILD_ID?.trim()) {
44
+ return env.PORTAL_BUILD_ID.trim()
45
+ }
46
+ if (env.GITHUB_REF_NAME?.trim()) {
47
+ return env.GITHUB_REF_NAME.trim()
48
+ }
49
+ const projectRoot = findProjectRoot()
50
+ return readBuildIdFile(projectRoot) ?? 'dev'
51
+ }
52
+
53
+ /** Cached build id for the current Node process (file + env read once). */
54
+ export function portalBuildId() {
55
+ if (cached === undefined) {
56
+ cached = resolvePortalBuildId()
57
+ }
58
+ return cached
59
+ }
60
+
61
+ /** Inject resolved id into env for child processes (vite, etc.). */
62
+ export function envWithPortalBuildId(baseEnv = process.env) {
63
+ const buildId = resolvePortalBuildId(baseEnv)
64
+ return { ...baseEnv, PORTAL_BUILD_ID: buildId }
65
+ }
66
+
67
+ export { getBuildIdFilePath as buildIdFile }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local dev server — loads Vite and plugins from the consumer project's node_modules
4
+ * (configFile: false) so the kit does not need its own vite install.
5
+ */
6
+ import { createRequire } from 'node:module'
7
+ import { dirname, resolve } from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+
10
+ import { findProjectRoot, getKitRoot } from './project-root.mjs'
11
+
12
+ const projectRoot = findProjectRoot()
13
+ const kitRoot = getKitRoot()
14
+ const require = createRequire(pathToFileURL(resolve(projectRoot, 'package.json')))
15
+
16
+ const port = Number(process.env.WP_HS_DEV_PORT ?? process.argv[2] ?? 3000)
17
+
18
+ const { createServer } = require('vite')
19
+ const viteReact = require('@vitejs/plugin-react')
20
+ const { TanStackRouterVite } = require('@tanstack/router-plugin/vite')
21
+ const tailwindcss = require('@tailwindcss/vite')
22
+
23
+ const tailwindPrefixPlugin = (
24
+ await import(pathToFileURL(resolve(kitRoot, 'scripts/tailwind/prefix-plugin.js')).href)
25
+ ).default
26
+ const TailwindContentPlugin = (
27
+ await import(pathToFileURL(resolve(kitRoot, 'scripts/tailwind/tailwind-content-plugin.js')).href)
28
+ ).default
29
+ const sdkLogBridge = (
30
+ await import(pathToFileURL(resolve(kitRoot, 'scripts/vite/sdk-log-bridge.js')).href)
31
+ ).default
32
+ const moduleConfigStore = (
33
+ await import(pathToFileURL(resolve(kitRoot, 'scripts/vite/module-config-store.js')).href)
34
+ ).default
35
+ const resolveFromProjectRoot = (
36
+ await import(pathToFileURL(resolve(kitRoot, 'scripts/vite/resolve-from-project-root.js')).href)
37
+ ).default
38
+ const { createDevReactPluginOptions } = await import(
39
+ pathToFileURL(resolve(kitRoot, 'scripts/vite/react-dev-plugin-options.js')).href
40
+ )
41
+ const { ensureDevKitShims } = await import(
42
+ pathToFileURL(resolve(kitRoot, 'scripts/vite/ensure-dev-kit-shims.js')).href
43
+ )
44
+ const isPrefix = (await import(pathToFileURL(resolve(kitRoot, 'scripts/tailwind/prefix.env.js')).href))
45
+ .default
46
+
47
+ const { moduleConfigRoot, bootstrapShim } = ensureDevKitShims(projectRoot, kitRoot)
48
+
49
+ const server = await createServer({
50
+ root: projectRoot,
51
+ configFile: false,
52
+ mode: 'dev',
53
+ plugins: [
54
+ resolveFromProjectRoot(projectRoot, kitRoot),
55
+ sdkLogBridge(),
56
+ moduleConfigStore(projectRoot),
57
+ TanStackRouterVite({ autoCodeSplitting: false }),
58
+ viteReact.default?.(createDevReactPluginOptions()) ?? viteReact(createDevReactPluginOptions()),
59
+ tailwindcss.default?.() ?? tailwindcss(),
60
+ ...(isPrefix('dev') ? [tailwindPrefixPlugin(), TailwindContentPlugin()] : []),
61
+ ],
62
+ resolve: {
63
+ preserveSymlinks: true,
64
+ alias: {
65
+ '@/dev/module-config': moduleConfigRoot,
66
+ '@woodsportal/hubspot-kit/dev/bootstrap': bootstrapShim,
67
+ '@': resolve(projectRoot, './src'),
68
+ },
69
+ },
70
+ optimizeDeps: {
71
+ include: [
72
+ 'react/compiler-runtime',
73
+ 'react/jsx-dev-runtime',
74
+ 'react/jsx-runtime',
75
+ 'react-dom/client',
76
+ ],
77
+ exclude: ['woodsportal-client-sdk', 'woodsportal-client-sdk/auth', 'woodsportal-client-sdk/crm'],
78
+ },
79
+ define: {
80
+ 'process.env.NODE_ENV': JSON.stringify('development'),
81
+ 'process.env': {},
82
+ process: JSON.stringify({ env: {} }),
83
+ },
84
+ server: {
85
+ port,
86
+ fs: {
87
+ allow: [projectRoot, kitRoot],
88
+ },
89
+ },
90
+ })
91
+
92
+ await server.listen()
93
+ server.printUrls()
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HubSpot module post-build runner — invoked after `vite build` completes.
4
+ * Kept separate from Vite closeBundle to avoid hook-order races on CI.
5
+ */
6
+ import { fileURLToPath } from 'node:url'
7
+ import { findProjectRoot } from './project-root.mjs'
8
+ import { dirname, resolve } from 'node:path'
9
+ import { finalizeHubSpotModuleDist } from './hubspot-module-postbuild.mjs'
10
+
11
+ const root = findProjectRoot()
12
+ const { jsBytes, cssBytes } = finalizeHubSpotModuleDist(
13
+ resolve(root, 'dist/WoodsPortal.Module'),
14
+ resolve(root, 'public'),
15
+ )
16
+
17
+ console.log(
18
+ `[hubspot-module] post-build OK — module.js ${(jsBytes / 1024 / 1024).toFixed(2)} MiB, module.css ${(cssBytes / 1024).toFixed(0)} KiB`,
19
+ )
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run `vite build` with portal env — `.env.stg` / `.env.prod` take precedence over shell.
4
+ *
5
+ * Usage: node scripts/run-vite-build.mjs <vite-config.mjs> [--stg]
6
+ */
7
+ import { spawnSync } from 'node:child_process'
8
+
9
+ import { defaultDatadogFlag, envForPortalBuild } from './portal-vite-env.mjs'
10
+ import { resolvePortalViteMode } from './portal-env-mode.mjs'
11
+ import { findProjectRoot } from './project-root.mjs'
12
+
13
+ const root = findProjectRoot()
14
+ const config = process.argv[2]
15
+ const mode = resolvePortalViteMode()
16
+
17
+ if (!config) {
18
+ console.error('Usage: node scripts/run-vite-build.mjs <vite-config.mjs> [--stg]')
19
+ process.exit(1)
20
+ }
21
+
22
+ const env = envForPortalBuild(mode, root, {
23
+ VITE_DATADOG_RUM_ENABLED: defaultDatadogFlag(mode),
24
+ WP_HS_PROJECT_ROOT: root,
25
+ })
26
+
27
+ const result = spawnSync(
28
+ 'npx',
29
+ ['vite', 'build', '--config', config, '--mode', mode],
30
+ { cwd: root, stdio: 'inherit', env, shell: true },
31
+ )
32
+
33
+ process.exit(result.status ?? 1)