@woodsportal/hubspot-kit 1.0.41-dev.0 → 1.0.41-dev.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woodsportal/hubspot-kit",
3
- "version": "1.0.41-dev.0",
3
+ "version": "1.0.41-dev.2",
4
4
  "description": "WoodsCLI — HubSpot CMS dev kit by WoodsPortal. wp CLI, Vite presets, module-config overlay, and theme/module build pipelines.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -3,19 +3,25 @@
3
3
  * Usage: node scripts/audit-tdz-split.mjs
4
4
  */
5
5
  import { defineConfig, build } from 'vite'
6
- import viteReact from '@vitejs/plugin-react'
7
- import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
8
6
  import { resolve, dirname } from 'node:path'
9
7
  import { fileURLToPath } from 'node:url'
10
8
  import { execSync } from 'node:child_process'
11
9
  import { readFileSync, readdirSync, existsSync } from 'node:fs'
12
- import tailwindcss from '@tailwindcss/vite'
13
10
  import tailwindPrefixPlugin from './tailwind/prefix-plugin.js'
14
11
  import TailwindContentPlugin from './tailwind/tailwind-content-plugin.js'
15
12
  import isPrefix from './tailwind/prefix.env.js'
13
+ import { findProjectRoot } from './project-root.mjs'
14
+ import { importConsumerDep } from './resolve-consumer-dep.mjs'
15
+
16
+ const [{ default: viteReact }, { TanStackRouterVite }, { default: tailwindcss }] =
17
+ await Promise.all([
18
+ importConsumerDep('@vitejs/plugin-react'),
19
+ importConsumerDep('@tanstack/router-plugin/vite'),
20
+ importConsumerDep('@tailwindcss/vite'),
21
+ ])
16
22
 
17
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
18
- const root = resolve(__dirname, '..')
24
+ const root = findProjectRoot()
19
25
  const outDir = resolve(root, 'dist/TdzAudit')
20
26
  const mode = 'production'
21
27
 
@@ -42,7 +48,7 @@ const config = defineConfig({
42
48
  copyPublicDir: false,
43
49
  cssCodeSplit: true,
44
50
  lib: {
45
- entry: resolve(root, 'src/main.tsx'),
51
+ entry: resolve(root, 'src/cdn/app-entry.ts'),
46
52
  name: 'TdzAuditModule',
47
53
  fileName: () => 'module.js',
48
54
  formats: ['iife'],
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail when dist/cdn/app.*.js raw size grows more than PORTAL_CDN_APP_SIZE_REGRESSION_PCT vs baseline.
4
+ * Baseline: docs/audit/cdn-app-size-baseline.json (committed after intentional size changes).
5
+ */
6
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+ import { findProjectRoot } from './project-root.mjs'
9
+
10
+ const root = findProjectRoot()
11
+ const cdnDir = resolve(root, 'dist/cdn')
12
+ const baselinePath = resolve(root, 'docs/audit/cdn-app-size-baseline.json')
13
+ const pctLimit = Number(process.env.PORTAL_CDN_APP_SIZE_REGRESSION_PCT ?? 5)
14
+
15
+ function findAppJs() {
16
+ if (!existsSync(cdnDir)) return null
17
+ return readdirSync(cdnDir).find((f) => /^app\..*\.js$/.test(f)) ?? null
18
+ }
19
+
20
+ const appFile = findAppJs()
21
+ if (!appFile) {
22
+ console.error('[cdn-regression] dist/cdn/app.*.js missing — run yarn build:cdn first')
23
+ process.exit(1)
24
+ }
25
+
26
+ const rawBytes = readFileSync(resolve(cdnDir, appFile)).length
27
+
28
+ if (!existsSync(baselinePath)) {
29
+ const baseline = { file: appFile, rawBytes, updatedAt: new Date().toISOString() }
30
+ writeFileSync(baselinePath, JSON.stringify(baseline, null, 2))
31
+ console.log(`[cdn-regression] Wrote baseline ${(rawBytes / 1024).toFixed(1)} KiB → ${baselinePath}`)
32
+ process.exit(0)
33
+ }
34
+
35
+ const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'))
36
+ const growthPct = ((rawBytes - baseline.rawBytes) / baseline.rawBytes) * 100
37
+
38
+ console.log(
39
+ `[cdn-regression] app shell ${(rawBytes / 1024).toFixed(1)} KiB raw (baseline ${(baseline.rawBytes / 1024).toFixed(1)} KiB, ${growthPct >= 0 ? '+' : ''}${growthPct.toFixed(1)}%)`,
40
+ )
41
+
42
+ if (growthPct > pctLimit) {
43
+ console.error(
44
+ `[cdn-regression] FAIL: grew ${growthPct.toFixed(1)}% > ${pctLimit}% limit. Update baseline intentionally or reduce bundle.`,
45
+ )
46
+ process.exit(1)
47
+ }
48
+
49
+ console.log('[cdn-regression] OK')
@@ -17,9 +17,9 @@ const embedMode = isMonolith ? 'monolith' : (process.env.VITE_PORTAL_EMBED_MODE
17
17
  const HUBSPOT_FILE_LIMIT = Number(process.env.HUBSPOT_MODULE_RAW_LIMIT_BYTES ?? 1.5 * 1024 * 1024)
18
18
  const BOOTSTRAP_RAW_LIMIT = Number(process.env.HUBSPOT_BOOTSTRAP_RAW_LIMIT_BYTES ?? 200 * 1024)
19
19
  const CDN_CHUNK_RAW_LIMIT = Number(process.env.PORTAL_CDN_CHUNK_RAW_LIMIT_BYTES ?? 1.5 * 1024 * 1024)
20
- /** Phase 1 app chunk may exceed 1.5 MiB until ESM route splitting (Phase 2). */
20
+ /** App entry chunk stepped down as multi-chunk splits land (target 1.5 MiB). */
21
21
  const CDN_APP_RAW_LIMIT = Number(
22
- process.env.PORTAL_CDN_APP_RAW_LIMIT_BYTES ?? 2.3 * 1024 * 1024,
22
+ process.env.PORTAL_CDN_APP_RAW_LIMIT_BYTES ?? 2.0 * 1024 * 1024,
23
23
  )
24
24
 
25
25
  function checkFile(label, path, rawLimit) {
@@ -52,8 +52,22 @@ if (embedMode === 'monolith') {
52
52
  console.error('FAIL: dist/cdn missing — run yarn build:cdn')
53
53
  process.exit(1)
54
54
  }
55
- for (const file of readdirSync(cdnDir).filter((f) => f.endsWith('.js'))) {
56
- const limit = file.startsWith('app.') ? CDN_APP_RAW_LIMIT : CDN_CHUNK_RAW_LIMIT
55
+ function walkJsFiles(dir, prefix = '') {
56
+ const paths = []
57
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
58
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name
59
+ if (entry.isDirectory()) {
60
+ paths.push(...walkJsFiles(resolve(dir, entry.name), rel))
61
+ } else if (entry.name.endsWith('.js')) {
62
+ paths.push(rel)
63
+ }
64
+ }
65
+ return paths
66
+ }
67
+
68
+ for (const file of walkJsFiles(cdnDir)) {
69
+ const base = file.split('/').pop() ?? file
70
+ const limit = base.startsWith('app.') ? CDN_APP_RAW_LIMIT : CDN_CHUNK_RAW_LIMIT
57
71
  checkFile(`cdn/${file}`, resolve(cdnDir, file), limit)
58
72
  }
59
73
  }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Write docs/audit/CDN-BUNDLE-BREAKDOWN.md from cdn-bundle-visualizer.json (rollup-plugin-visualizer v2).
4
+ */
5
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
6
+ import { resolve } from 'node:path'
7
+ import { findProjectRoot } from './project-root.mjs'
8
+
9
+ const root = findProjectRoot()
10
+ const jsonPath = resolve(root, 'docs/audit/cdn-bundle-visualizer.json')
11
+ const outPath = resolve(root, 'docs/audit/CDN-BUNDLE-BREAKDOWN.md')
12
+
13
+ if (!existsSync(jsonPath)) {
14
+ console.error(
15
+ `[cdn-breakdown] Missing ${jsonPath} — run: yarn audit:cdn-bundle`,
16
+ )
17
+ process.exit(1)
18
+ }
19
+
20
+ const data = JSON.parse(readFileSync(jsonPath, 'utf8'))
21
+ const { nodeParts = {}, nodeMetas = {} } = data
22
+
23
+ const leaves = Object.entries(nodeParts)
24
+ .map(([uid, part]) => {
25
+ const meta = nodeMetas[uid] ?? {}
26
+ return {
27
+ name: meta.id ?? uid,
28
+ gzipSize: part.gzipSize ?? 0,
29
+ renderedLength: part.renderedLength ?? 0,
30
+ }
31
+ })
32
+ .filter((n) => n.renderedLength > 0)
33
+ .sort((a, b) => b.gzipSize - a.gzipSize)
34
+
35
+ const top = leaves.slice(0, 15)
36
+ const generatedAt = new Date().toISOString()
37
+
38
+ const lines = [
39
+ '# CDN app bundle breakdown',
40
+ '',
41
+ `Generated: ${generatedAt}`,
42
+ '',
43
+ 'Source: `yarn audit:cdn-bundle` → `docs/audit/cdn-bundle-visualizer.json`',
44
+ '',
45
+ 'Top 15 modules by gzip size (CDN app graph):',
46
+ '',
47
+ '| Rank | Module | Gzip (KiB) | Raw (KiB) |',
48
+ '|------|--------|------------|-----------|',
49
+ ]
50
+
51
+ top.forEach((node, i) => {
52
+ const gzip = node.gzipSize / 1024
53
+ const raw = node.renderedLength / 1024
54
+ const name = String(node.name).replace(/\|/g, '\\|')
55
+ lines.push(`| ${i + 1} | \`${name}\` | ${gzip.toFixed(1)} | ${raw.toFixed(1)} |`)
56
+ })
57
+
58
+ lines.push('', 'See also: [BUNDLE-STRATEGY.md](../BUNDLE-STRATEGY.md), [artifact-sizes.json](./artifact-sizes.json).')
59
+
60
+ writeFileSync(outPath, `${lines.join('\n')}\n`)
61
+ console.log(`[cdn-breakdown] wrote ${outPath}`)
@@ -15,7 +15,7 @@ const cdnDir = resolve(root, 'dist/cdn')
15
15
  const buildId = portalBuildId()
16
16
 
17
17
  function fileMetrics(file) {
18
- const path = resolve(cdnDir, file)
18
+ const path = resolve(cdnDir, file.replace(/^\//, ''))
19
19
  const raw = readFileSync(path)
20
20
  return {
21
21
  file,
@@ -24,17 +24,31 @@ function fileMetrics(file) {
24
24
  }
25
25
  }
26
26
 
27
- const jsFiles = readdirSync(cdnDir)
28
- .filter((f) => f.endsWith('.js'))
29
- .map((file) => {
27
+ function scriptEntry(file) {
28
+ if (file.startsWith('vendor.')) {
29
+ return { name: 'vendor', order: 1, preload: true }
30
+ }
31
+ if (file.startsWith('app.') && !file.includes('/')) {
32
+ return { name: 'app', order: 2, preload: true }
33
+ }
34
+ if (file.startsWith('chunks/')) {
35
+ return { name: file.replace(/^chunks\//, ''), order: 99, preload: false }
36
+ }
37
+ return { name: file, order: 99, preload: false }
38
+ }
39
+
40
+ const jsFiles = readdirSync(cdnDir, { recursive: true })
41
+ .filter((f) => String(f).endsWith('.js'))
42
+ .map((rel) => {
43
+ const file = String(rel)
30
44
  const metrics = fileMetrics(file)
45
+ const entry = scriptEntry(file)
31
46
  return {
32
47
  ...metrics,
33
- name: file.startsWith('vendor.') ? 'vendor' : file.startsWith('app.') ? 'app' : file,
34
- order: file.startsWith('vendor.') ? 1 : file.startsWith('app.') ? 2 : 99,
48
+ ...entry,
35
49
  }
36
50
  })
37
- .sort((a, b) => a.order - b.order)
51
+ .sort((a, b) => a.order - b.order || a.file.localeCompare(b.file))
38
52
 
39
53
  const cssFiles = readdirSync(cdnDir)
40
54
  .filter((f) => f.endsWith('.css'))
@@ -64,10 +78,11 @@ const manifest = {
64
78
  buildId,
65
79
  baseUrl: resolveCdnBaseUrl(),
66
80
  publishMode: process.env.VITE_PORTAL_CDN_PUBLISH_MODE ?? 'jsdelivr',
67
- scripts: jsFiles.map(({ name, file, order, rawBytes, gzipBytes }) => ({
81
+ scripts: jsFiles.map(({ name, file, order, preload, rawBytes, gzipBytes }) => ({
68
82
  name,
69
83
  file,
70
84
  order,
85
+ preload,
71
86
  rawBytes,
72
87
  gzipBytes,
73
88
  url: `${resolveCdnBaseUrl()}/${file}`,
@@ -1,17 +1,91 @@
1
1
  /**
2
- * Vite env for HubSpot/CDN builds — .env.[mode] wins over stale shell VITE_* vars.
2
+ * Vite env for HubSpot/CDN builds — .env.[mode] wins over stale shell / .env.local VITE_* vars.
3
3
  */
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
4
6
  import { loadEnv } from 'vite'
5
7
  import { envWithPortalBuildId } from './resolve-portal-build-id.mjs'
6
8
  import { envWithPortalCdnConfig } from './load-portal-cdn-config.mjs'
7
9
 
10
+ /** @param {string} content */
11
+ export function parseEnvFile(content) {
12
+ const result = {}
13
+ for (const line of content.split('\n')) {
14
+ const trimmed = line.trim()
15
+ if (!trimmed || trimmed.startsWith('#')) continue
16
+ const eq = trimmed.indexOf('=')
17
+ if (eq <= 0) continue
18
+ const key = trimmed.slice(0, eq).trim()
19
+ let value = trimmed.slice(eq + 1).trim()
20
+ if (
21
+ (value.startsWith('"') && value.endsWith('"')) ||
22
+ (value.startsWith("'") && value.endsWith("'"))
23
+ ) {
24
+ value = value.slice(1, -1)
25
+ }
26
+ result[key] = value
27
+ }
28
+ return result
29
+ }
30
+
31
+ /** @param {'local' | 'dev' | 'stg' | 'prod'} mode @param {string} root */
32
+ export function readPortalTierEnvFile(mode, root) {
33
+ const path = resolve(root, `.env.${mode}`)
34
+ if (!existsSync(path)) return {}
35
+ return parseEnvFile(readFileSync(path, 'utf8'))
36
+ }
37
+
38
+ function stashViteProcessEnv() {
39
+ const stashed = {}
40
+ for (const key of Object.keys(process.env)) {
41
+ if (key.startsWith('VITE_')) {
42
+ stashed[key] = process.env[key]
43
+ delete process.env[key]
44
+ }
45
+ }
46
+ return stashed
47
+ }
48
+
49
+ /** @param {Record<string, string | undefined>} stashed */
50
+ function restoreViteProcessEnv(stashed) {
51
+ for (const [key, value] of Object.entries(stashed)) {
52
+ if (value != null) process.env[key] = value
53
+ }
54
+ }
55
+
56
+ /** loadEnv without shell / .env.local VITE_* polluting tier resolution */
57
+ /** @param {'local' | 'dev' | 'stg' | 'prod'} mode @param {string} root */
58
+ export function loadPortalFileEnv(mode, root) {
59
+ const stashed = stashViteProcessEnv()
60
+ try {
61
+ return loadEnv(mode, root, '')
62
+ } finally {
63
+ restoreViteProcessEnv(stashed)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * API URL baked into CDN bundles for a deploy tier — reads `.env.[mode]` only.
69
+ * @param {'local' | 'dev' | 'stg' | 'prod'} mode
70
+ * @param {string} root
71
+ */
72
+ export function resolveTierApiEndpoint(mode, root) {
73
+ const tier = readPortalTierEnvFile(mode, root)
74
+ const fromTier = tier.VITE_PUBLIC_REST_API_ENDPOINT
75
+ if (fromTier) return fromTier.replace(/\/$/, '')
76
+ const fromFile = loadPortalFileEnv(mode, root).VITE_PUBLIC_REST_API_ENDPOINT
77
+ const fromCi =
78
+ process.env.WOODPORTAL_API_BASE_URL || process.env.VITE_PUBLIC_REST_API_ENDPOINT
79
+ return (fromFile || fromCi)?.replace(/\/$/, '')
80
+ }
81
+
8
82
  /**
9
83
  * @param {'local' | 'dev' | 'stg' | 'prod'} mode
10
84
  * @param {string} root
11
85
  * @param {NodeJS.ProcessEnv} [extra]
12
86
  */
13
87
  export function envForPortalBuild(mode, root, extra = {}) {
14
- const fileEnv = loadEnv(mode, root, '')
88
+ const fileEnv = loadPortalFileEnv(mode, root)
15
89
  const base = { ...process.env }
16
90
 
17
91
  for (const key of Object.keys(base)) {
@@ -98,6 +98,9 @@ function authRemote(remote, token) {
98
98
  function copyCdnArtifacts(sourceDir, targetDir) {
99
99
  mkdirSync(targetDir, { recursive: true })
100
100
  for (const entry of readdirSync(sourceDir)) {
101
+ if (entry.endsWith('.map')) {
102
+ fail(`Refusing to publish source map: ${entry}`)
103
+ }
101
104
  const src = join(sourceDir, entry)
102
105
  const dest = join(targetDir, entry)
103
106
  rmSync(dest, { recursive: true, force: true })
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Refresh docs/audit/artifact-sizes.json from dist/cdn + HubSpot module output.
4
+ */
5
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
6
+ import { resolve, join } from 'node:path'
7
+ import { gzipSync } from 'node:zlib'
8
+ import { findProjectRoot } from './project-root.mjs'
9
+ import { moduleOutDir, readWoodsportalConfig } from './read-woodsportal-config.mjs'
10
+
11
+ const root = findProjectRoot()
12
+ const { config } = readWoodsportalConfig(root)
13
+
14
+ function metrics(path) {
15
+ const raw = readFileSync(path)
16
+ const gzip = gzipSync(raw)
17
+ return {
18
+ rawBytes: raw.length,
19
+ gzipBytes: gzip.length,
20
+ rawMiB: Number((raw.length / (1024 * 1024)).toFixed(3)),
21
+ gzipKiB: Number((gzip.length / 1024).toFixed(1)),
22
+ overValidationLimit: raw.length > 1.5 * 1024 * 1024,
23
+ }
24
+ }
25
+
26
+ function walkJs(dir, prefix = '') {
27
+ const out = []
28
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
29
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name
30
+ const full = join(dir, entry.name)
31
+ if (entry.isDirectory()) {
32
+ out.push(...walkJs(full, rel))
33
+ } else if (entry.name.endsWith('.js')) {
34
+ out.push(rel)
35
+ }
36
+ }
37
+ return out
38
+ }
39
+
40
+ const moduleDir = moduleOutDir(root)
41
+ const cdnDir = resolve(root, 'dist/cdn')
42
+ const artifacts = {}
43
+
44
+ for (const name of ['module.js', 'module.css', 'module.html', 'fields.json']) {
45
+ const p = resolve(moduleDir, name)
46
+ if (existsSync(p)) {
47
+ artifacts[name] = metrics(p)
48
+ }
49
+ }
50
+
51
+ const cdn = {}
52
+ if (existsSync(cdnDir)) {
53
+ let manifest = null
54
+ const manifestPath = resolve(cdnDir, 'manifest.json')
55
+ if (existsSync(manifestPath)) {
56
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
57
+ cdn['manifest.json'] = { generatedAt: manifest.generatedAt, buildId: manifest.buildId }
58
+ }
59
+ for (const file of walkJs(cdnDir)) {
60
+ cdn[file] = metrics(resolve(cdnDir, file))
61
+ }
62
+ for (const file of readdirSync(cdnDir).filter((f) => f.endsWith('.css'))) {
63
+ cdn[file] = metrics(resolve(cdnDir, file))
64
+ }
65
+ }
66
+
67
+ const doc = {
68
+ generatedAt: new Date().toISOString(),
69
+ embedMode: 'cdn',
70
+ hubspotLimits: { validationMiB: 1.5, bufferMiB: 2 },
71
+ artifacts,
72
+ cdn,
73
+ }
74
+
75
+ const outPath = resolve(root, 'docs/audit/artifact-sizes.json')
76
+ writeFileSync(outPath, JSON.stringify(doc, null, 2))
77
+ console.log(`[artifact-sizes] wrote ${outPath}`)
@@ -13,6 +13,7 @@ import { resolve } from 'node:path'
13
13
  import { findProjectRoot } from './project-root.mjs'
14
14
  import { resolvePortalBuildId } from './resolve-portal-build-id.mjs'
15
15
  import { resolvePortalViteMode } from './portal-env-mode.mjs'
16
+ import { resolveTierApiEndpoint } from './portal-vite-env.mjs'
16
17
 
17
18
  const root = findProjectRoot()
18
19
 
@@ -29,15 +30,11 @@ if (tierFlags.length !== 1) {
29
30
  }
30
31
 
31
32
  const mode = resolvePortalViteMode()
32
- const { loadEnv } = await import('vite')
33
- const fromFile = loadEnv(mode, root, '').VITE_PUBLIC_REST_API_ENDPOINT
34
- const fromEnv =
35
- process.env.VITE_PUBLIC_REST_API_ENDPOINT || process.env.WOODPORTAL_API_BASE_URL
36
- const expected = (fromFile || fromEnv)?.replace(/\/$/, '')
33
+ const expected = resolveTierApiEndpoint(mode, root)
37
34
 
38
35
  if (!expected) {
39
36
  console.error(
40
- `[verify-cdn-api] Missing VITE_PUBLIC_REST_API_ENDPOINT in .env.${mode} (or VITE_PUBLIC_REST_API_ENDPOINT / WOODPORTAL_API_BASE_URL env)`,
37
+ `[verify-cdn-api] Missing VITE_PUBLIC_REST_API_ENDPOINT in .env.${mode} (or WOODPORTAL_API_BASE_URL for CI)`,
41
38
  )
42
39
  process.exit(1)
43
40
  }
@@ -59,13 +56,23 @@ if (!appFile) {
59
56
  }
60
57
 
61
58
  const source = readFileSync(resolve(cdnDir, appFile), 'utf8')
62
- const found = [...source.matchAll(/https:\/\/api(?:\.dev)?\.woodsportal\.com/g)].map((m) => m[0])
63
- const unique = [...new Set(found)]
59
+ const bakedUrls = [
60
+ ...source.matchAll(/https:\/\/api(?:\.dev)?\.woodsportal\.com/g),
61
+ ...source.matchAll(/http:\/\/localhost:\d+/g),
62
+ ].map((m) => m[0])
63
+ const unique = [...new Set(bakedUrls)]
64
64
 
65
- if (!unique.includes(expected)) {
65
+ if (!source.includes(expected)) {
66
66
  console.error('[verify-cdn-api] API URL mismatch in dist/cdn — refusing to publish.')
67
- console.error(` expected (${mode}): ${expected}`)
67
+ console.error(` expected from .env.${mode}: ${expected}`)
68
68
  console.error(` found in ${appFile}: ${unique.join(', ') || '(none)'}`)
69
+ const shellApi = process.env.VITE_PUBLIC_REST_API_ENDPOINT
70
+ if (shellApi && shellApi.replace(/\/$/, '') !== expected) {
71
+ console.error('')
72
+ console.error(
73
+ ` Note: shell/.env.local has VITE_PUBLIC_REST_API_ENDPOINT=${shellApi} — ignored for deploy; CDN uses .env.${mode} only.`,
74
+ )
75
+ }
69
76
  console.error('')
70
77
  console.error(' Dev API on HubSpot: yarn deploy:dev')
71
78
  console.error(' Prod API on HubSpot: yarn deploy:prod')
@@ -67,7 +67,13 @@ export async function createDevServerViteConfig(options = {}) {
67
67
  ...prefixPlugins,
68
68
  ],
69
69
  resolve: {
70
- dedupe: ['react', 'react-dom'],
70
+ dedupe: [
71
+ 'react',
72
+ 'react-dom',
73
+ 'woodsportal-client-sdk',
74
+ 'woodsportal-client-sdk/auth',
75
+ 'woodsportal-client-sdk/crm',
76
+ ],
71
77
  alias: {
72
78
  ...consumerReactAliases(projectRoot),
73
79
  '@/dev/module-config': moduleConfigRoot,
@@ -38,7 +38,7 @@ function prodDevModuleConfigRouteStub(mode) {
38
38
  if (!isPrefix(mode)) {
39
39
  return null
40
40
  }
41
- const stub = resolve(root, 'src/routes/dev.module-config.prod-stub.tsx')
41
+ const stub = resolve(root, 'src/stubs/dev.module-config.cdn-route-stub.tsx')
42
42
  return {
43
43
  name: 'woodsportal-prod-dev-route-stub',
44
44
  enforce: 'pre',
@@ -55,6 +55,62 @@ function prodDevModuleConfigRouteStub(mode) {
55
55
  }
56
56
  }
57
57
 
58
+ function prodResolveIdStub(mode, { name, matchers, stub }) {
59
+ if (!isPrefix(mode)) {
60
+ return null
61
+ }
62
+ return {
63
+ name,
64
+ enforce: 'pre',
65
+ resolveId(source) {
66
+ for (const match of matchers) {
67
+ if (typeof match === 'string' ? source === match || source.endsWith(match) : match.test(source)) {
68
+ return stub
69
+ }
70
+ }
71
+ return null
72
+ },
73
+ }
74
+ }
75
+
76
+ function prodDevHubSpotLiveStub(mode) {
77
+ if (!isPrefix(mode)) {
78
+ return null
79
+ }
80
+ const liveStub = resolve(root, 'src/integrations/dev-hubspot-live-provider.prod-stub.tsx')
81
+ const syncStub = resolve(root, 'src/data/dev-hubspot-sync.prod-stub.ts')
82
+ return {
83
+ name: 'woodsportal-prod-dev-hubspot-live-stub',
84
+ enforce: 'pre',
85
+ resolveId(source) {
86
+ if (
87
+ source === '@/integrations/dev-hubspot-live-provider' ||
88
+ /\/integrations\/dev-hubspot-live-provider(\.tsx)?$/.test(source)
89
+ ) {
90
+ return liveStub
91
+ }
92
+ if (
93
+ source === '@/data/dev-hubspot-sync' ||
94
+ /\/data\/dev-hubspot-sync(\.ts)?$/.test(source)
95
+ ) {
96
+ return syncStub
97
+ }
98
+ return null
99
+ },
100
+ }
101
+ }
102
+
103
+ function prodSdkTerminalLogSinksStub(mode) {
104
+ return prodResolveIdStub(mode, {
105
+ name: 'woodsportal-prod-sdk-log-sinks-stub',
106
+ matchers: [
107
+ '@/integrations/sdk-terminal-log-sinks',
108
+ /\/integrations\/sdk-terminal-log-sinks(\.ts)?$/,
109
+ ],
110
+ stub: resolve(root, 'src/integrations/sdk-terminal-log-sinks.prod-stub.ts'),
111
+ })
112
+ }
113
+
58
114
  /** Ship CDN builds must not bundle localhost fixture menus from src/fixtures/demo-menu.ts */
59
115
  function prodDefaultDataStub(mode) {
60
116
  if (!isPrefix(mode)) {
@@ -81,6 +137,8 @@ export function portalPlugins(mode, { codeSplitting = false, routeFileIgnorePatt
81
137
  const routeIgnore = routeFileIgnorePattern ?? (isPrefix(mode) ? 'dev\\.' : undefined)
82
138
  return [
83
139
  prodDevModuleConfigRouteStub(mode),
140
+ prodDevHubSpotLiveStub(mode),
141
+ prodSdkTerminalLogSinksStub(mode),
84
142
  prodDefaultDataStub(mode),
85
143
  TanStackRouterVite({
86
144
  autoCodeSplitting: codeSplitting,
@@ -129,7 +187,9 @@ export function portalResolve(mode) {
129
187
  })
130
188
  }
131
189
 
132
- return { alias }
190
+ const dedupe = ['axios', 'pako', 'js-cookie']
191
+
192
+ return { alias, dedupe }
133
193
  }
134
194
 
135
195
  export { root, __dirname }
@@ -1,12 +1,22 @@
1
1
  export type ModuleConfigOverlaySide = 'left' | 'right'
2
2
 
3
- const STORAGE_KEY = 'woodsportal:module-config-overlay-side'
3
+ const STORAGE_KEY = 'wp.d.cfg.side'
4
+ const LEGACY_STORAGE_KEY = 'woodsportal:module-config-overlay-side'
4
5
 
5
6
  export function loadModuleConfigOverlaySide(): ModuleConfigOverlaySide {
6
7
  if (typeof window === 'undefined') return 'right'
7
8
  try {
8
- const value = window.localStorage.getItem(STORAGE_KEY)
9
- return value === 'left' ? 'left' : 'right'
9
+ const value =
10
+ window.localStorage.getItem(STORAGE_KEY) ??
11
+ window.localStorage.getItem(LEGACY_STORAGE_KEY)
12
+ if (value === 'left' || value === 'right') {
13
+ if (!window.localStorage.getItem(STORAGE_KEY)) {
14
+ window.localStorage.setItem(STORAGE_KEY, value)
15
+ window.localStorage.removeItem(LEGACY_STORAGE_KEY)
16
+ }
17
+ return value
18
+ }
19
+ return 'right'
10
20
  } catch {
11
21
  return 'right'
12
22
  }
@@ -1,6 +1,7 @@
1
1
  export type ModuleConfigToolbarTheme = 'light' | 'dark'
2
2
 
3
- const STORAGE_KEY = 'woodsportal:module-config-toolbar-theme'
3
+ const STORAGE_KEY = 'wp.d.cfg.theme'
4
+ const LEGACY_STORAGE_KEY = 'woodsportal:module-config-toolbar-theme'
4
5
 
5
6
  export function detectPortalTheme(): ModuleConfigToolbarTheme {
6
7
  if (typeof document === 'undefined') return 'light'
@@ -10,8 +11,16 @@ export function detectPortalTheme(): ModuleConfigToolbarTheme {
10
11
  export function loadModuleConfigToolbarTheme(): ModuleConfigToolbarTheme {
11
12
  if (typeof window === 'undefined') return 'light'
12
13
  try {
13
- const stored = window.localStorage.getItem(STORAGE_KEY)
14
- if (stored === 'dark' || stored === 'light') return stored
14
+ const stored =
15
+ window.localStorage.getItem(STORAGE_KEY) ??
16
+ window.localStorage.getItem(LEGACY_STORAGE_KEY)
17
+ if (stored === 'dark' || stored === 'light') {
18
+ if (!window.localStorage.getItem(STORAGE_KEY)) {
19
+ window.localStorage.setItem(STORAGE_KEY, stored)
20
+ window.localStorage.removeItem(LEGACY_STORAGE_KEY)
21
+ }
22
+ return stored
23
+ }
15
24
  } catch {
16
25
  // ignore
17
26
  }