@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/CHANGELOG.md +1 -1
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/scripts/audit-tdz-split.mjs +11 -5
- package/scripts/check-cdn-size-regression.mjs +49 -0
- package/scripts/check-hubspot-bundle-size.mjs +18 -4
- package/scripts/generate-cdn-bundle-breakdown.mjs +61 -0
- package/scripts/generate-cdn-manifest.mjs +23 -8
- package/scripts/portal-vite-env.mjs +76 -2
- package/scripts/publish-cdn-github.mjs +3 -0
- package/scripts/refresh-artifact-sizes.mjs +77 -0
- package/scripts/verify-cdn-api-endpoint.mjs +17 -10
- package/scripts/vite/create-dev-server-config.mjs +7 -1
- package/scripts/vite/portal-build-shared.js +62 -2
- package/src/dev-module-config/ui/module-config-overlay-side.ts +13 -3
- package/src/dev-module-config/ui/module-config-toolbar-theme.ts +12 -3
- package/vite/vite.config.cdn-app.mjs +41 -30
- package/vite/vite.config.cdn-audit.mjs +34 -0
- package/vite/vite.config.cdn-esm.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woodsportal/hubspot-kit",
|
|
3
|
-
"version": "1.0.41-dev.
|
|
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 =
|
|
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/
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
56
|
-
const
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
63
|
-
|
|
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 (!
|
|
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
|
|
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: [
|
|
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/
|
|
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
|
-
|
|
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 = '
|
|
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 =
|
|
9
|
-
|
|
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 = '
|
|
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 =
|
|
14
|
-
|
|
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
|
}
|