@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
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@woodsportal/hubspot-kit",
3
+ "version": "1.0.2",
4
+ "description": "WoodsCLI — HubSpot CMS dev kit by WoodsPortal. wp CLI, Vite presets, module-config overlay, and theme/module build pipelines.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "wp": "./dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "scripts",
14
+ "vite",
15
+ "templates",
16
+ "examples",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "SECURITY.md",
20
+ "LICENSE"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./package.json": "./package.json",
28
+ "./vite": {
29
+ "import": "./dist/vite/index.js"
30
+ },
31
+ "./dev/bootstrap": "./src/dev/bootstrap.tsx",
32
+ "./routing/route-format": "./src/routing/route-format.ts",
33
+ "./routing/build-api-routes": "./src/routing/build-api-routes.ts",
34
+ "./scripts/tailwind/prefix.env.js": "./scripts/tailwind/prefix.env.js",
35
+ "./scripts/*": "./scripts/*",
36
+ "./vite/*": "./vite/*"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Digital-Woods/digitalwoods.io-woodsportal-hubspot-kit.git"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "dev": "tsup --watch",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' && vitest run src/dev-module-config/__tests__/normalize-repeater-items.test.ts src/dev-module-config/__tests__/module-config-transform.test.ts",
51
+ "test:examples": "node examples/smoke/run-matrix.mjs --tier static",
52
+ "lint": "eslint src",
53
+ "verify": "npm run typecheck && npm run test && npm run test:examples && npm run build",
54
+ "prepublishOnly": "npm run verify"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "dependencies": {
60
+ "@babel/generator": "^7.27.0",
61
+ "@babel/parser": "^7.27.0",
62
+ "@babel/traverse": "^7.27.0",
63
+ "@babel/types": "^7.27.0",
64
+ "@clack/prompts": "^0.10.1",
65
+ "commander": "^13.1.0",
66
+ "picocolors": "^1.1.1",
67
+ "zod": "^3.24.2"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^22.13.4",
71
+ "vitest": "^3.2.4",
72
+ "tsx": "^4.19.3",
73
+ "tsup": "^8.4.0",
74
+ "typescript": "^5.7.3"
75
+ },
76
+ "peerDependencies": {
77
+ "@tailwindcss/vite": "^4.0.0",
78
+ "@tanstack/router-plugin": "^1.0.0",
79
+ "@vitejs/plugin-react": "^4.0.0",
80
+ "babel-plugin-react-compiler": "^1.0.0",
81
+ "react": "^18.0.0 || ^19.0.0",
82
+ "react-dom": "^18.0.0 || ^19.0.0",
83
+ "vite": "^6.0.0"
84
+ },
85
+ "peerDependenciesMeta": {
86
+ "@tailwindcss/vite": { "optional": true },
87
+ "@tanstack/router-plugin": { "optional": true },
88
+ "@vitejs/plugin-react": { "optional": true },
89
+ "babel-plugin-react-compiler": { "optional": true },
90
+ "react": { "optional": true },
91
+ "react-dom": { "optional": true },
92
+ "vite": { "optional": true }
93
+ }
94
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Audits fields.json leaf paths against supported module-config field types.
3
+ * Run: node scripts/audit-module-config-fields.mjs
4
+ */
5
+ import fs from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
10
+
11
+ const HUBSPOT_ONLY = new Set([
12
+ 'portal_cdn_base_url',
13
+ 'portal_build_id',
14
+ 'portal_vendor_script',
15
+ 'portal_app_script',
16
+ 'portal_app_css',
17
+ 'hubspot_data_source',
18
+ ])
19
+
20
+ const SUPPORTED_TYPES = new Set([
21
+ 'text',
22
+ 'number',
23
+ 'boolean',
24
+ 'choice',
25
+ 'color',
26
+ 'richtext',
27
+ 'image',
28
+ 'link',
29
+ 'url',
30
+ 'group',
31
+ ])
32
+
33
+ /** @typedef {{ name: string, type: string, children?: Field[], occurrence?: unknown, tab?: string }} Field */
34
+
35
+ /** @param {Field[]} fields @param {string} prefix */
36
+ function collectPaths(fields, prefix = '') {
37
+ /** @type {Array<{ path: string, type: string, name: string }>} */
38
+ const paths = []
39
+ for (const field of fields) {
40
+ const fieldPath = prefix ? `${prefix}.${field.name}` : field.name
41
+ const topName = field.name
42
+
43
+ if (HUBSPOT_ONLY.has(topName)) continue
44
+
45
+ if (field.type === 'group') {
46
+ if (field.occurrence) {
47
+ paths.push({ path: fieldPath, type: 'occurrence-group', name: field.name })
48
+ }
49
+ if (field.children?.length) {
50
+ paths.push(...collectPaths(field.children, field.occurrence ? fieldPath : fieldPath))
51
+ }
52
+ continue
53
+ }
54
+
55
+ paths.push({ path: fieldPath, type: field.type, name: field.name })
56
+ }
57
+ return paths
58
+ }
59
+
60
+ const fieldsPath = path.join(root, 'public/fields.json')
61
+ const fields = JSON.parse(await fs.readFile(fieldsPath, 'utf8'))
62
+ const paths = collectPaths(fields)
63
+ const unsupported = paths.filter((entry) => !SUPPORTED_TYPES.has(entry.type) && entry.type !== 'occurrence-group')
64
+
65
+ if (unsupported.length > 0) {
66
+ console.error('Unsupported module-config field paths:')
67
+ for (const entry of unsupported) {
68
+ console.error(` - ${entry.path} (${entry.type})`)
69
+ }
70
+ process.exit(1)
71
+ }
72
+
73
+ console.log(`OK: ${paths.length} field paths covered by module-config editors`)
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regression: snapshot dist/ layout hashes vs .wp-hs-baseline (optional).
4
+ * Invoked via: wp audit parity
5
+ */
6
+ import { spawnSync } from 'node:child_process'
7
+ import { createRequire } from 'node:module'
8
+ import { dirname, resolve } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const require = createRequire(import.meta.url)
12
+ const kitRoot = dirname(require.resolve('@woodsportal/hubspot-kit/package.json', {
13
+ paths: [dirname(fileURLToPath(import.meta.url))],
14
+ }))
15
+
16
+ const cli = resolve(kitRoot, 'dist/cli.js')
17
+ const result = spawnSync(process.execPath, [cli, 'audit', 'parity', ...process.argv.slice(2)], {
18
+ stdio: 'inherit',
19
+ env: process.env,
20
+ })
21
+
22
+ process.exit(result.status ?? 1)
@@ -0,0 +1,111 @@
1
+ /**
2
+ * TDZ audit: build with TanStack route code-splitting + dynamic imports (no inline).
3
+ * Usage: node scripts/audit-tdz-split.mjs
4
+ */
5
+ import { defineConfig, build } from 'vite'
6
+ import viteReact from '@vitejs/plugin-react'
7
+ import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
8
+ import { resolve, dirname } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+ import { execSync } from 'node:child_process'
11
+ import { readFileSync, readdirSync, existsSync } from 'node:fs'
12
+ import tailwindcss from '@tailwindcss/vite'
13
+ import tailwindPrefixPlugin from './tailwind/prefix-plugin.js'
14
+ import TailwindContentPlugin from './tailwind/tailwind-content-plugin.js'
15
+ import isPrefix from './tailwind/prefix.env.js'
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url))
18
+ const root = resolve(__dirname, '..')
19
+ const outDir = resolve(root, 'dist/TdzAudit')
20
+ const mode = 'production'
21
+
22
+ const config = defineConfig({
23
+ plugins: [
24
+ TanStackRouterVite({ autoCodeSplitting: true }),
25
+ viteReact({
26
+ babel: { plugins: ['babel-plugin-react-compiler'] },
27
+ }),
28
+ tailwindcss(),
29
+ ...(isPrefix(mode)
30
+ ? [tailwindPrefixPlugin(), TailwindContentPlugin()]
31
+ : []),
32
+ ],
33
+ resolve: { alias: { '@': resolve(root, './src') } },
34
+ define: {
35
+ 'process.env.NODE_ENV': JSON.stringify('production'),
36
+ 'process.env': {},
37
+ process: JSON.stringify({ env: {} }),
38
+ },
39
+ build: {
40
+ outDir,
41
+ emptyOutDir: true,
42
+ copyPublicDir: false,
43
+ cssCodeSplit: true,
44
+ lib: {
45
+ entry: resolve(root, 'src/main.tsx'),
46
+ name: 'TdzAuditModule',
47
+ fileName: () => 'module.js',
48
+ formats: ['iife'],
49
+ },
50
+ rollupOptions: {
51
+ external: [],
52
+ output: {
53
+ assetFileNames: 'module.css',
54
+ inlineDynamicImports: false,
55
+ },
56
+ },
57
+ minify: true,
58
+ sourcemap: true,
59
+ },
60
+ })
61
+
62
+ try {
63
+ await build(config)
64
+
65
+ const files = existsSync(outDir) ? readdirSync(outDir) : []
66
+ const jsFiles = files.filter((f) => f.endsWith('.js'))
67
+ let syntaxOk = true
68
+ let syntaxError = null
69
+ for (const f of jsFiles) {
70
+ try {
71
+ execSync(`node --check ${JSON.stringify(resolve(outDir, f))}`, {
72
+ stdio: 'pipe',
73
+ })
74
+ } catch (e) {
75
+ syntaxOk = false
76
+ syntaxError = String(e.stderr ?? e.message)
77
+ }
78
+ }
79
+
80
+ const mainJs = resolve(outDir, 'module.js')
81
+ const mainSize = existsSync(mainJs) ? readFileSync(mainJs).length : 0
82
+
83
+ console.log(
84
+ JSON.stringify(
85
+ {
86
+ outcome: 'build_succeeded',
87
+ jsFiles,
88
+ chunkCount: jsFiles.length,
89
+ mainModuleBytes: mainSize,
90
+ syntaxCheckPassed: syntaxOk,
91
+ syntaxError: syntaxError?.slice(0, 500) ?? null,
92
+ note:
93
+ 'Documented production failure: ReferenceError Cannot access before initialization when route lazy chunks load inside HubSpot single IIFE. Static node --check does not reproduce runtime TDZ.',
94
+ },
95
+ null,
96
+ 2,
97
+ ),
98
+ )
99
+ } catch (error) {
100
+ console.log(
101
+ JSON.stringify(
102
+ {
103
+ outcome: 'build_failed',
104
+ message: String(error.message ?? error).slice(0, 1200),
105
+ note: 'IIFE lib format + dynamic route imports often conflict at Rollup emit for HubSpot module.js.',
106
+ },
107
+ null,
108
+ 2,
109
+ ),
110
+ )
111
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build CDN vendor + app IIFE chunks into dist/cdn/
4
+ */
5
+ import { spawnSync } from 'node:child_process'
6
+ import { resolve, dirname } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { findProjectRoot } from './project-root.mjs'
9
+
10
+ import { envWithPortalBuildId } from './resolve-portal-build-id.mjs'
11
+ import { envForPortalBuild, defaultDatadogFlag } from './portal-vite-env.mjs'
12
+ import { resolvePortalViteMode } from './portal-env-mode.mjs'
13
+
14
+ const root = findProjectRoot()
15
+
16
+ const mode = resolvePortalViteMode()
17
+
18
+ const env = envForPortalBuild(mode, root, {
19
+ VITE_DATADOG_RUM_ENABLED: defaultDatadogFlag(mode),
20
+ })
21
+
22
+ function runWithEnv(config, buildMode) {
23
+ const result = spawnSync(
24
+ 'yarn',
25
+ ['vite', 'build', '--config', config, '--mode', buildMode],
26
+ { cwd: root, stdio: 'inherit', env },
27
+ )
28
+ if (result.status !== 0) {
29
+ process.exit(result.status ?? 1)
30
+ }
31
+ }
32
+
33
+ runWithEnv('vite.config.cdn-vendor.mjs', mode)
34
+ runWithEnv('vite.config.cdn-app.mjs', mode)
35
+
36
+ console.log('[build-cdn] vendor + app chunks written to dist/cdn/')
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI / local gate for HubSpot module and CDN chunk sizes.
4
+ */
5
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
6
+ import { resolve, dirname } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { findProjectRoot } from './project-root.mjs'
9
+ import { gzipSync } from 'node:zlib'
10
+
11
+ const root = findProjectRoot()
12
+ const embedMode = process.env.VITE_PORTAL_EMBED_MODE ?? 'cdn'
13
+
14
+ const HUBSPOT_FILE_LIMIT = Number(process.env.HUBSPOT_MODULE_RAW_LIMIT_BYTES ?? 1.5 * 1024 * 1024)
15
+ const BOOTSTRAP_RAW_LIMIT = Number(process.env.HUBSPOT_BOOTSTRAP_RAW_LIMIT_BYTES ?? 200 * 1024)
16
+ const CDN_CHUNK_RAW_LIMIT = Number(process.env.PORTAL_CDN_CHUNK_RAW_LIMIT_BYTES ?? 1.5 * 1024 * 1024)
17
+ /** Phase 1 app chunk may exceed 1.5 MiB until ESM route splitting (Phase 2). */
18
+ const CDN_APP_RAW_LIMIT = Number(
19
+ process.env.PORTAL_CDN_APP_RAW_LIMIT_BYTES ?? 2.3 * 1024 * 1024,
20
+ )
21
+
22
+ function checkFile(label, path, rawLimit) {
23
+ if (!existsSync(path)) {
24
+ console.error(`FAIL: missing ${label} at ${path}`)
25
+ process.exit(1)
26
+ }
27
+ const raw = readFileSync(path)
28
+ const gzip = gzipSync(raw)
29
+ console.log(
30
+ `${label}: ${(raw.length / 1024).toFixed(1)} KiB raw, ${(gzip.length / 1024).toFixed(1)} KiB gzip (limit ${(rawLimit / 1024).toFixed(0)} KiB raw)`,
31
+ )
32
+ if (raw.length > rawLimit) {
33
+ console.error(`FAIL: ${label} exceeds raw limit`)
34
+ process.exit(1)
35
+ }
36
+ }
37
+
38
+ const moduleDir = resolve(root, 'dist/WoodsPortal.Module')
39
+ const moduleJs = resolve(moduleDir, 'module.js')
40
+
41
+ if (embedMode === 'monolith') {
42
+ checkFile('module.js (monolith)', moduleJs, HUBSPOT_FILE_LIMIT)
43
+ } else {
44
+ checkFile('module.js (bootstrap)', moduleJs, BOOTSTRAP_RAW_LIMIT)
45
+ checkFile('module.css', resolve(moduleDir, 'module.css'), HUBSPOT_FILE_LIMIT)
46
+
47
+ const cdnDir = resolve(root, 'dist/cdn')
48
+ if (!existsSync(cdnDir)) {
49
+ console.error('FAIL: dist/cdn missing — run yarn build:cdn')
50
+ process.exit(1)
51
+ }
52
+ for (const file of readdirSync(cdnDir).filter((f) => f.endsWith('.js'))) {
53
+ const limit = file.startsWith('app.') ? CDN_APP_RAW_LIMIT : CDN_CHUNK_RAW_LIMIT
54
+ checkFile(`cdn/${file}`, resolve(cdnDir, file), limit)
55
+ }
56
+ }
57
+
58
+ console.log('OK: HubSpot / CDN bundle size checks passed')
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared project config resolution — `woodscli.json` only.
3
+ */
4
+ import { existsSync } from 'node:fs'
5
+ import { resolve } from 'node:path'
6
+
7
+ export const WOODSCLI_CONFIG_FILE = 'woodscli.json'
8
+ export const LEGACY_WOODSPORTAL_HS_FILE = 'woodsportal.hs.json'
9
+
10
+ /**
11
+ * @param {string} projectRoot
12
+ */
13
+ export function woodscliConfigPath(projectRoot) {
14
+ return resolve(projectRoot, WOODSCLI_CONFIG_FILE)
15
+ }
16
+
17
+ /**
18
+ * @param {string} projectRoot
19
+ */
20
+ export function legacyConfigPath(projectRoot) {
21
+ return resolve(projectRoot, LEGACY_WOODSPORTAL_HS_FILE)
22
+ }
23
+
24
+ /**
25
+ * @param {string} projectRoot
26
+ */
27
+ export function resolveProjectConfigPath(projectRoot) {
28
+ const fromEnv = process.env.WOODSCLI_CONFIG?.trim() || process.env.WP_HS_CONFIG?.trim() || null
29
+ if (fromEnv) {
30
+ const path = resolve(projectRoot, fromEnv)
31
+ return existsSync(path) ? path : null
32
+ }
33
+
34
+ const path = woodscliConfigPath(projectRoot)
35
+ return existsSync(path) ? path : null
36
+ }
37
+
38
+ /**
39
+ * @param {string} [from]
40
+ */
41
+ export function findProjectRootWithConfig(from = process.cwd()) {
42
+ if (process.env.WP_HS_PROJECT_ROOT?.trim()) {
43
+ return resolve(process.env.WP_HS_PROJECT_ROOT.trim())
44
+ }
45
+
46
+ let dir = resolve(from)
47
+ while (true) {
48
+ if (resolveProjectConfigPath(dir)) return dir
49
+ const parent = resolve(dir, '..')
50
+ if (parent === dir) return resolve(from)
51
+ dir = parent
52
+ }
53
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Detect HubSpot CMS project layout (module vs theme, adopted vs existing).
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs'
5
+ import { join, resolve } from 'node:path'
6
+
7
+ import { resolveProjectConfigPath } from './config-files.mjs'
8
+
9
+ /**
10
+ * @param {string} projectRoot
11
+ */
12
+ export function detectProjectLayout(projectRoot) {
13
+ const root = resolve(projectRoot)
14
+ const configPath = resolveProjectConfigPath(root)
15
+ const hasConfig = Boolean(configPath && existsSync(configPath))
16
+
17
+ let config = null
18
+ if (hasConfig && configPath) {
19
+ try {
20
+ config = JSON.parse(readFileSync(configPath, 'utf8'))
21
+ } catch {
22
+ config = null
23
+ }
24
+ }
25
+
26
+ const fieldsJsonPaths = [
27
+ join(root, 'public/fields.json'),
28
+ join(root, config?.dev?.fieldsJson ?? 'public/fields.json'),
29
+ ].filter((p, i, arr) => arr.indexOf(p) === i)
30
+
31
+ const moduleHtmlPaths = [join(root, 'public/module.html'), join(root, 'module.html')]
32
+
33
+ const themeJsonPaths = [
34
+ join(root, config?.theme?.themeJson ?? 'src/theme/theme.json'),
35
+ join(root, 'theme.json'),
36
+ join(root, 'src/theme/theme.json'),
37
+ ]
38
+
39
+ const hasFieldsJson = fieldsJsonPaths.some((p) => existsSync(p))
40
+ const hasModuleHtml = moduleHtmlPaths.some((p) => existsSync(p))
41
+ const hasThemeJson = themeJsonPaths.some((p) => existsSync(p))
42
+ const hasTemplates =
43
+ existsSync(join(root, 'src/theme/templates')) || existsSync(join(root, 'templates'))
44
+
45
+ const wphsDir = config?.dev?.moduleConfigDir ?? '.wphs'
46
+ const hasWphs = existsSync(join(root, wphsDir))
47
+
48
+ let track = 'unknown'
49
+ if (hasThemeJson || hasTemplates) track = 'theme'
50
+ else if (hasFieldsJson || hasModuleHtml) track = 'module'
51
+
52
+ let setup = 'existing'
53
+ if (hasConfig && hasWphs) setup = 'adopted'
54
+ else if (hasConfig && !hasWphs) setup = 'partial'
55
+ else if (!hasConfig && (hasFieldsJson || hasThemeJson)) setup = 'existing'
56
+
57
+ return {
58
+ projectRoot: root,
59
+ track,
60
+ setup,
61
+ hasConfig,
62
+ hasWphs,
63
+ hasFieldsJson,
64
+ hasModuleHtml,
65
+ hasThemeJson,
66
+ hasTemplates,
67
+ projectType: config?.projectType ?? null,
68
+ configPath: hasConfig ? configPath : null,
69
+ }
70
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scaffold WoodsCLI `.wphs/` workspace + kit symlinks.
4
+ */
5
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
6
+
7
+ import { findProjectRoot, getKitRoot } from './project-root.mjs'
8
+ import { wphsPaths, wphsWorkspaceSubdirs } from './wphs-paths.mjs'
9
+ import { ensureDevKitShims } from './vite/ensure-dev-kit-shims.js'
10
+
11
+ const WPHS_README = `# WoodsCLI workspace (.wphs/)
12
+
13
+ Local state for **WoodsCLI** (\`@woodsportal/hubspot-kit\`). Not shipped to HubSpot.
14
+
15
+ | Path | Purpose |
16
+ |------|---------|
17
+ | \`config/module-config.local.json\` | Live Content/Styles values (gitignored) |
18
+ | \`config/module-config.example.json\` | Example generated from \`fields.json\` |
19
+ | \`assets/branding/\` | Dev-tool uploaded images (gitignored) |
20
+ | \`fixtures/\` | Optional preview / hubspot-data overrides |
21
+ | \`cache/\` | CLI caches (gitignored) |
22
+ | \`audit/baseline/\` | Build parity snapshots (gitignored) |
23
+ | \`.kit-module-config\` | Symlink to kit dev-module-config |
24
+ | \`.kit-bootstrap.tsx\` | Symlink to kit dev bootstrap |
25
+
26
+ Run \`wp doctor\` to recreate missing folders.
27
+ `
28
+
29
+ /**
30
+ * @param {string} [projectRoot]
31
+ * @param {string} [kitRoot]
32
+ */
33
+ export function ensureWphsWorkspace(projectRoot, kitRoot) {
34
+ const root = projectRoot ?? findProjectRoot()
35
+ const kit = kitRoot ?? getKitRoot()
36
+ const paths = wphsPaths(root)
37
+
38
+ for (const dir of wphsWorkspaceSubdirs(root)) {
39
+ mkdirSync(dir, { recursive: true })
40
+ }
41
+
42
+ if (!existsSync(paths.readmePath)) {
43
+ writeFileSync(paths.readmePath, WPHS_README)
44
+ }
45
+
46
+ ensureDevKitShims(root, kit)
47
+ return paths
48
+ }
49
+
50
+ if (import.meta.url === `file://${process.argv[1]}`) {
51
+ const paths = ensureWphsWorkspace()
52
+ console.log(`[wphs] workspace ready at ${paths.workspaceRoot}`)
53
+ }
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Write dist/cdn/manifest.json after vendor + app builds.
4
+ */
5
+ import { readdirSync, writeFileSync, statSync } from 'node:fs'
6
+ import { resolve, dirname } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { findProjectRoot } from './project-root.mjs'
9
+ import { gzipSync } from 'node:zlib'
10
+ import { readFileSync } from 'node:fs'
11
+ import { resolveCdnBaseUrl, portalBuildId } from './resolve-cdn-base-url.mjs'
12
+
13
+ const root = findProjectRoot()
14
+ const cdnDir = resolve(root, 'dist/cdn')
15
+ const buildId = portalBuildId()
16
+
17
+ function fileMetrics(file) {
18
+ const path = resolve(cdnDir, file)
19
+ const raw = readFileSync(path)
20
+ return {
21
+ file,
22
+ rawBytes: raw.length,
23
+ gzipBytes: gzipSync(raw).length,
24
+ }
25
+ }
26
+
27
+ const jsFiles = readdirSync(cdnDir)
28
+ .filter((f) => f.endsWith('.js'))
29
+ .map((file) => {
30
+ const metrics = fileMetrics(file)
31
+ return {
32
+ ...metrics,
33
+ name: file.startsWith('vendor.') ? 'vendor' : file.startsWith('app.') ? 'app' : file,
34
+ order: file.startsWith('vendor.') ? 1 : file.startsWith('app.') ? 2 : 99,
35
+ }
36
+ })
37
+ .sort((a, b) => a.order - b.order)
38
+
39
+ const cssFiles = readdirSync(cdnDir)
40
+ .filter((f) => f.endsWith('.css'))
41
+ .map((file) => {
42
+ const metrics = fileMetrics(file)
43
+ return {
44
+ ...metrics,
45
+ name: file.startsWith('app.') ? 'app' : file,
46
+ order: 1,
47
+ }
48
+ })
49
+
50
+ const appCss = cssFiles.find((f) => f.name === 'app') ?? cssFiles[0]
51
+
52
+ if (!jsFiles.some((f) => f.name === 'vendor') || !jsFiles.some((f) => f.name === 'app')) {
53
+ console.error('[cdn-manifest] expected vendor.*.js and app.*.js in dist/cdn/')
54
+ process.exit(1)
55
+ }
56
+
57
+ if (!appCss) {
58
+ console.error('[cdn-manifest] expected app.*.css in dist/cdn/ — component styles load from CDN')
59
+ process.exit(1)
60
+ }
61
+
62
+ const manifest = {
63
+ generatedAt: new Date().toISOString(),
64
+ buildId,
65
+ baseUrl: resolveCdnBaseUrl(),
66
+ publishMode: process.env.VITE_PORTAL_CDN_PUBLISH_MODE ?? 'jsdelivr',
67
+ scripts: jsFiles.map(({ name, file, order, rawBytes, gzipBytes }) => ({
68
+ name,
69
+ file,
70
+ order,
71
+ rawBytes,
72
+ gzipBytes,
73
+ url: `${resolveCdnBaseUrl()}/${file}`,
74
+ })),
75
+ styles: cssFiles.map(({ name, file, order, rawBytes, gzipBytes }) => ({
76
+ name,
77
+ file,
78
+ order,
79
+ rawBytes,
80
+ gzipBytes,
81
+ url: `${resolveCdnBaseUrl()}/${file}`,
82
+ })),
83
+ }
84
+
85
+ writeFileSync(resolve(cdnDir, 'manifest.json'), JSON.stringify(manifest, null, 2))
86
+ console.log('[cdn-manifest] wrote dist/cdn/manifest.json')
87
+ for (const s of manifest.scripts) {
88
+ console.log(` ${s.file}: ${(s.rawBytes / 1024).toFixed(1)} KiB raw, ${(s.gzipBytes / 1024).toFixed(1)} KiB gzip`)
89
+ }
90
+ console.log(
91
+ ` ${appCss.file}: ${(appCss.rawBytes / 1024).toFixed(1)} KiB raw, ${(appCss.gzipBytes / 1024).toFixed(1)} KiB gzip (styles)`,
92
+ )