@woodsportal/hubspot-kit 1.0.31 → 1.0.34

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 (57) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli.js +798 -281
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/examples/fixtures/adopted-module-hybrid/woodscli.json +1 -1
  7. package/examples/fixtures/adopted-module-monolith/woodscli.json +1 -1
  8. package/examples/greenfield/module-hybrid-cdn/public/fields.json +26 -20
  9. package/examples/greenfield/module-hybrid-cdn/public/meta.json +9 -0
  10. package/examples/greenfield/module-hybrid-cdn/public/module.html +3 -4
  11. package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.prod.css +6 -0
  12. package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +5 -0
  13. package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +6 -0
  14. package/examples/greenfield/module-hybrid-cdn/src/portal/ensure-portal-cdn-stylesheet.ts +58 -0
  15. package/examples/greenfield/module-hybrid-cdn/woodscli.json +1 -1
  16. package/examples/greenfield/module-monolith/woodscli.json +1 -1
  17. package/package.json +2 -2
  18. package/scripts/build-cdn.mjs +6 -1
  19. package/scripts/bump-module-build-id.mjs +28 -0
  20. package/scripts/hubspot-module-postbuild.mjs +35 -1
  21. package/scripts/load-portal-cdn-config.mjs +21 -3
  22. package/scripts/portal-build-id.mjs +346 -0
  23. package/scripts/portal-build-id.test.mjs +132 -0
  24. package/scripts/publish-cdn-github.mjs +60 -15
  25. package/scripts/resolve-cdn-base-url.mjs +2 -1
  26. package/scripts/resolve-module-build-id-meta.mjs +7 -0
  27. package/scripts/resolve-portal-build-id.mjs +16 -56
  28. package/scripts/tailwind/prefix-plugin.js +46 -8
  29. package/scripts/tailwind/tailwind-content-plugin.js +0 -1
  30. package/scripts/vite/portal-build-shared.js +3 -1
  31. package/src/dev-module-config/__tests__/flatten-visible-fields.test.ts +107 -0
  32. package/src/dev-module-config/runtime/module-config-bridge.ts +10 -2
  33. package/src/dev-module-config/schema/types.ts +10 -0
  34. package/src/dev-module-config/schema/visibility.ts +42 -4
  35. package/src/dev-module-config/ui/ModuleConfigShell.tsx +10 -9
  36. package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +3 -3
  37. package/templates/module-hybrid-cdn/public/fields.json +52 -41
  38. package/templates/module-hybrid-cdn/public/meta.json +9 -0
  39. package/templates/module-hybrid-cdn/public/module.html +18 -4
  40. package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +8 -0
  41. package/templates/module-hybrid-cdn/src/assets/css/main.prod.css +12 -0
  42. package/templates/module-hybrid-cdn/src/assets/css/tailwind.css.config.css +7 -0
  43. package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +14 -3
  44. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +9 -11
  45. package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +23 -5
  46. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +1 -0
  47. package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +6 -0
  48. package/templates/module-hybrid-cdn/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
  49. package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +35 -5
  50. package/templates/module-hybrid-cdn/woodscli.json +1 -1
  51. package/templates/module-monolith/public/fields.json +23 -16
  52. package/templates/module-monolith/public/meta.json +9 -0
  53. package/templates/module-monolith/src/features/starter/components/starter-home.tsx +9 -11
  54. package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +23 -5
  55. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +1 -0
  56. package/templates/module-monolith/woodscli.json +1 -1
  57. package/vite/vite.config.dev.mjs +16 -10
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ var WoodsPortalHsSchema = z.object({
30
30
  }).optional(),
31
31
  cdn: z.object({
32
32
  mode: z.enum(["jsdelivr", "custom", "local", "none"]).default("jsdelivr"),
33
- buildIdFile: z.string().default(".portal-build-id"),
33
+ buildIdFile: z.string().default(".module-build-id"),
34
34
  mirror: z.string().default("portal.cdn.json")
35
35
  }).default({}),
36
36
  entries: z.object({
@@ -65,7 +65,7 @@ var DEFAULT_WOODSPORTAL_HS = {
65
65
  },
66
66
  cdn: {
67
67
  mode: "jsdelivr",
68
- buildIdFile: ".portal-build-id",
68
+ buildIdFile: ".module-build-id",
69
69
  mirror: "portal.cdn.json"
70
70
  },
71
71
  entries: {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schema/woodsportal-hs.ts","../src/lib/config.ts","../src/adapters/types.ts","../src/lib/generic-field-mapper.ts"],"sourcesContent":["import { z } from 'zod'\n\nexport const CONFIG_VERSION = 1\n\nexport const WPHS_WORKSPACE_DIR = '.wphs' as const\n\nexport const WoodsPortalHsSchema = z.object({\n configVersion: z.number().int().default(CONFIG_VERSION),\n projectType: z.enum(['module-hybrid-cdn', 'module-monolith', 'theme-starter']),\n module: z.object({\n name: z.string().min(1),\n uploadPath: z.string().min(1),\n outDir: z.string().default('dist/WoodsPortal.Module'),\n }),\n theme: z\n .object({\n name: z.string().min(1),\n uploadPath: z.string().min(1),\n srcDir: z.string().default('src/theme'),\n themeJson: z.string().default('src/theme/theme.json'),\n })\n .optional(),\n cdn: z\n .object({\n mode: z.enum(['jsdelivr', 'custom', 'local', 'none']).default('jsdelivr'),\n buildIdFile: z.string().default('.portal-build-id'),\n mirror: z.string().default('portal.cdn.json'),\n })\n .default({}),\n entries: z\n .object({\n app: z.string().default('src/cdn/app-entry.ts'),\n vendor: z.string().default('src/cdn/vendor-entry.ts'),\n hubspotBootstrap: z.string().default('src/hubspot/bootstrap.ts'),\n /** Monolith production IIFE entry (wp build --hubspot-only) */\n module: z.string().default('src/hubspot/module-entry.tsx'),\n localDev: z.string().default('src/main.tsx'),\n })\n .default({}),\n env: z\n .object({\n tiers: z.array(z.enum(['dev', 'stg', 'prod'])).default(['dev', 'stg', 'prod']),\n })\n .default({}),\n dev: z\n .object({\n moduleConfigDir: z.literal(WPHS_WORKSPACE_DIR).default(WPHS_WORKSPACE_DIR),\n adapter: z.string().default('./wphs.adapter.ts'),\n fieldsJson: z.string().default('public/fields.json'),\n themeFieldsJson: z.string().optional(),\n })\n .default({}),\n scaffold: z\n .object({\n /** hash = woodsportal-client-frontend (HubSpot CMS); browser = local clean URLs; none = single page */\n router: z.enum(['hash', 'browser', 'none']).default('hash'),\n })\n .optional(),\n})\n\nexport type WoodsPortalHsConfig = z.infer<typeof WoodsPortalHsSchema>\n\nexport const DEFAULT_WOODSPORTAL_HS: WoodsPortalHsConfig = {\n configVersion: CONFIG_VERSION,\n projectType: 'module-hybrid-cdn',\n module: {\n name: 'woodsportal-module',\n uploadPath: 'dw-modules/woodsportal-module.module',\n outDir: 'dist/WoodsPortal.Module',\n },\n cdn: {\n mode: 'jsdelivr',\n buildIdFile: '.portal-build-id',\n mirror: 'portal.cdn.json',\n },\n entries: {\n app: 'src/cdn/app-entry.ts',\n vendor: 'src/cdn/vendor-entry.ts',\n hubspotBootstrap: 'src/hubspot/bootstrap.ts',\n module: 'src/hubspot/module-entry.tsx',\n localDev: 'src/main.tsx',\n },\n env: {\n tiers: ['dev', 'stg', 'prod'],\n },\n dev: {\n moduleConfigDir: WPHS_WORKSPACE_DIR,\n adapter: './wphs.adapter.ts',\n fieldsJson: 'public/fields.json',\n },\n}\n\nexport function isModuleProject(config: WoodsPortalHsConfig): boolean {\n return config.projectType === 'module-hybrid-cdn' || config.projectType === 'module-monolith'\n}\n\nexport function isThemeProject(config: WoodsPortalHsConfig): boolean {\n return config.projectType === 'theme-starter'\n}\n\nexport function usesCdnPipeline(config: WoodsPortalHsConfig): boolean {\n return config.projectType === 'module-hybrid-cdn' && config.cdn.mode !== 'none'\n}\n","import { readFileSync } from 'node:fs'\n\nimport {\n resolveProjectConfig,\n WOODSCLI_CONFIG_FILE,\n} from './config-files.js'\nimport { WoodsPortalHsSchema, type WoodsPortalHsConfig } from '../schema/woodsportal-hs.js'\nimport { WpHsError, ExitCode } from './errors.js'\nimport { findProjectRoot } from './paths.js'\n\nexport type LoadedProjectConfig = {\n root: string\n config: WoodsPortalHsConfig\n configPath: string\n configFileName: string\n}\n\nexport function loadConfig(projectRoot?: string): LoadedProjectConfig {\n const root = projectRoot ?? findProjectRoot()\n const resolved = resolveProjectConfig(root)\n\n if (!resolved) {\n throw new WpHsError(`Missing ${WOODSCLI_CONFIG_FILE}`, {\n exitCode: ExitCode.USER_ERROR,\n remediation: 'Run wp init. Legacy woodsportal.hs.json → run wp migrate --yes.',\n })\n }\n\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(resolved.path, 'utf8'))\n } catch (cause) {\n throw new WpHsError(`Invalid JSON in ${resolved.fileName}`, {\n exitCode: ExitCode.USER_ERROR,\n cause,\n })\n }\n\n const parsed = WoodsPortalHsSchema.safeParse(raw)\n if (!parsed.success) {\n throw new WpHsError(`${resolved.fileName} validation failed: ${parsed.error.message}`, {\n exitCode: ExitCode.USER_ERROR,\n remediation: 'Run wp config validate for details.',\n })\n }\n\n return {\n root,\n config: parsed.data,\n configPath: resolved.path,\n configFileName: resolved.fileName,\n }\n}\n\nexport function validateConfigFile(projectRoot?: string): {\n valid: boolean\n errors: string[]\n config?: WoodsPortalHsConfig\n} {\n try {\n const { config } = loadConfig(projectRoot)\n return { valid: true, errors: [], config }\n } catch (error) {\n if (error instanceof WpHsError) {\n return { valid: false, errors: [error.message] }\n }\n return { valid: false, errors: [String(error)] }\n }\n}\n","/** Must match dev-module-config/schema/types.ts */\nexport const MODULE_CONFIG_CHANGED = 'woodsportal:module-config-changed'\n\nexport type ModuleConfigBuiltState = {\n built: Record<string, unknown>\n}\n\n/** WoodsCLI consumer hook — mount app + sync module-config live data. */\nexport interface HubSpotKitAdapter {\n syncLiveData(built: Record<string, unknown>): void\n getDefaultHubSpotData?(): Record<string, unknown>\n mountDevApp?(root: unknown): void | Promise<void>\n}\n\nexport function bindAdapterToModuleConfig(adapter: HubSpotKitAdapter): void {\n if (typeof globalThis.window === 'undefined') return\n globalThis.window.addEventListener(MODULE_CONFIG_CHANGED, (event: Event) => {\n const detail = (event as CustomEvent<ModuleConfigBuiltState>).detail\n adapter.syncLiveData(detail.built)\n })\n}\n\nexport function syncAdapterFromModuleState(\n adapter: HubSpotKitAdapter,\n state: ModuleConfigBuiltState,\n): void {\n adapter.syncLiveData(state.built)\n}\n\n/** Minimal adapter for greenfield templates without a full app. */\nexport const defaultHubSpotKitAdapter: HubSpotKitAdapter = {\n syncLiveData(built) {\n const w = globalThis as typeof globalThis & { __WPHS_DATA__?: Record<string, unknown> }\n w.__WPHS_DATA__ = built\n },\n getDefaultHubSpotData() {\n return {}\n },\n}\n","/**\n * Generic fields.json → flat runtime object (WoodsCLI default transform).\n */\nexport type GenericFieldDefinition = {\n name: string\n type: string\n default?: unknown\n children?: GenericFieldDefinition[]\n}\n\nexport type GenericFieldValues = Record<string, unknown>\n\nfunction flattenField(\n field: GenericFieldDefinition,\n values: GenericFieldValues,\n out: Record<string, unknown>,\n): void {\n const key = field.name\n const value = values[key]\n\n if (field.type === 'group' && field.children && value && typeof value === 'object') {\n const groupValues = value as GenericFieldValues\n for (const child of field.children) {\n flattenField(child, groupValues, out)\n }\n return\n }\n\n if (field.type === 'repeater' && Array.isArray(value)) {\n out[key] = value\n return\n }\n\n out[key] = value ?? field.default ?? null\n}\n\nexport function mapFieldsToRuntime(\n schema: GenericFieldDefinition[],\n values: GenericFieldValues,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const field of schema) {\n flattenField(field, values, out)\n }\n return out\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/schema/woodsportal-hs.ts","../src/lib/config.ts","../src/adapters/types.ts","../src/lib/generic-field-mapper.ts"],"sourcesContent":["import { z } from 'zod'\n\nexport const CONFIG_VERSION = 1\n\nexport const WPHS_WORKSPACE_DIR = '.wphs' as const\n\nexport const WoodsPortalHsSchema = z.object({\n configVersion: z.number().int().default(CONFIG_VERSION),\n projectType: z.enum(['module-hybrid-cdn', 'module-monolith', 'theme-starter']),\n module: z.object({\n name: z.string().min(1),\n uploadPath: z.string().min(1),\n outDir: z.string().default('dist/WoodsPortal.Module'),\n }),\n theme: z\n .object({\n name: z.string().min(1),\n uploadPath: z.string().min(1),\n srcDir: z.string().default('src/theme'),\n themeJson: z.string().default('src/theme/theme.json'),\n })\n .optional(),\n cdn: z\n .object({\n mode: z.enum(['jsdelivr', 'custom', 'local', 'none']).default('jsdelivr'),\n buildIdFile: z.string().default('.module-build-id'),\n mirror: z.string().default('portal.cdn.json'),\n })\n .default({}),\n entries: z\n .object({\n app: z.string().default('src/cdn/app-entry.ts'),\n vendor: z.string().default('src/cdn/vendor-entry.ts'),\n hubspotBootstrap: z.string().default('src/hubspot/bootstrap.ts'),\n /** Monolith production IIFE entry (wp build --hubspot-only) */\n module: z.string().default('src/hubspot/module-entry.tsx'),\n localDev: z.string().default('src/main.tsx'),\n })\n .default({}),\n env: z\n .object({\n tiers: z.array(z.enum(['dev', 'stg', 'prod'])).default(['dev', 'stg', 'prod']),\n })\n .default({}),\n dev: z\n .object({\n moduleConfigDir: z.literal(WPHS_WORKSPACE_DIR).default(WPHS_WORKSPACE_DIR),\n adapter: z.string().default('./wphs.adapter.ts'),\n fieldsJson: z.string().default('public/fields.json'),\n themeFieldsJson: z.string().optional(),\n })\n .default({}),\n scaffold: z\n .object({\n /** hash = woodsportal-client-frontend (HubSpot CMS); browser = local clean URLs; none = single page */\n router: z.enum(['hash', 'browser', 'none']).default('hash'),\n })\n .optional(),\n})\n\nexport type WoodsPortalHsConfig = z.infer<typeof WoodsPortalHsSchema>\n\nexport const DEFAULT_WOODSPORTAL_HS: WoodsPortalHsConfig = {\n configVersion: CONFIG_VERSION,\n projectType: 'module-hybrid-cdn',\n module: {\n name: 'woodsportal-module',\n uploadPath: 'dw-modules/woodsportal-module.module',\n outDir: 'dist/WoodsPortal.Module',\n },\n cdn: {\n mode: 'jsdelivr',\n buildIdFile: '.module-build-id',\n mirror: 'portal.cdn.json',\n },\n entries: {\n app: 'src/cdn/app-entry.ts',\n vendor: 'src/cdn/vendor-entry.ts',\n hubspotBootstrap: 'src/hubspot/bootstrap.ts',\n module: 'src/hubspot/module-entry.tsx',\n localDev: 'src/main.tsx',\n },\n env: {\n tiers: ['dev', 'stg', 'prod'],\n },\n dev: {\n moduleConfigDir: WPHS_WORKSPACE_DIR,\n adapter: './wphs.adapter.ts',\n fieldsJson: 'public/fields.json',\n },\n}\n\nexport function isModuleProject(config: WoodsPortalHsConfig): boolean {\n return config.projectType === 'module-hybrid-cdn' || config.projectType === 'module-monolith'\n}\n\nexport function isThemeProject(config: WoodsPortalHsConfig): boolean {\n return config.projectType === 'theme-starter'\n}\n\nexport function usesCdnPipeline(config: WoodsPortalHsConfig): boolean {\n return config.projectType === 'module-hybrid-cdn' && config.cdn.mode !== 'none'\n}\n","import { readFileSync } from 'node:fs'\n\nimport {\n resolveProjectConfig,\n WOODSCLI_CONFIG_FILE,\n} from './config-files.js'\nimport { WoodsPortalHsSchema, type WoodsPortalHsConfig } from '../schema/woodsportal-hs.js'\nimport { WpHsError, ExitCode } from './errors.js'\nimport { findProjectRoot } from './paths.js'\n\nexport type LoadedProjectConfig = {\n root: string\n config: WoodsPortalHsConfig\n configPath: string\n configFileName: string\n}\n\nexport function loadConfig(projectRoot?: string): LoadedProjectConfig {\n const root = projectRoot ?? findProjectRoot()\n const resolved = resolveProjectConfig(root)\n\n if (!resolved) {\n throw new WpHsError(`Missing ${WOODSCLI_CONFIG_FILE}`, {\n exitCode: ExitCode.USER_ERROR,\n remediation: 'Run wp init. Legacy woodsportal.hs.json → run wp migrate --yes.',\n })\n }\n\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(resolved.path, 'utf8'))\n } catch (cause) {\n throw new WpHsError(`Invalid JSON in ${resolved.fileName}`, {\n exitCode: ExitCode.USER_ERROR,\n cause,\n })\n }\n\n const parsed = WoodsPortalHsSchema.safeParse(raw)\n if (!parsed.success) {\n throw new WpHsError(`${resolved.fileName} validation failed: ${parsed.error.message}`, {\n exitCode: ExitCode.USER_ERROR,\n remediation: 'Run wp config validate for details.',\n })\n }\n\n return {\n root,\n config: parsed.data,\n configPath: resolved.path,\n configFileName: resolved.fileName,\n }\n}\n\nexport function validateConfigFile(projectRoot?: string): {\n valid: boolean\n errors: string[]\n config?: WoodsPortalHsConfig\n} {\n try {\n const { config } = loadConfig(projectRoot)\n return { valid: true, errors: [], config }\n } catch (error) {\n if (error instanceof WpHsError) {\n return { valid: false, errors: [error.message] }\n }\n return { valid: false, errors: [String(error)] }\n }\n}\n","/** Must match dev-module-config/schema/types.ts */\nexport const MODULE_CONFIG_CHANGED = 'woodsportal:module-config-changed'\n\nexport type ModuleConfigBuiltState = {\n built: Record<string, unknown>\n}\n\n/** WoodsCLI consumer hook — mount app + sync module-config live data. */\nexport interface HubSpotKitAdapter {\n syncLiveData(built: Record<string, unknown>): void\n getDefaultHubSpotData?(): Record<string, unknown>\n mountDevApp?(root: unknown): void | Promise<void>\n}\n\nexport function bindAdapterToModuleConfig(adapter: HubSpotKitAdapter): void {\n if (typeof globalThis.window === 'undefined') return\n globalThis.window.addEventListener(MODULE_CONFIG_CHANGED, (event: Event) => {\n const detail = (event as CustomEvent<ModuleConfigBuiltState>).detail\n adapter.syncLiveData(detail.built)\n })\n}\n\nexport function syncAdapterFromModuleState(\n adapter: HubSpotKitAdapter,\n state: ModuleConfigBuiltState,\n): void {\n adapter.syncLiveData(state.built)\n}\n\n/** Minimal adapter for greenfield templates without a full app. */\nexport const defaultHubSpotKitAdapter: HubSpotKitAdapter = {\n syncLiveData(built) {\n const w = globalThis as typeof globalThis & { __WPHS_DATA__?: Record<string, unknown> }\n w.__WPHS_DATA__ = built\n },\n getDefaultHubSpotData() {\n return {}\n },\n}\n","/**\n * Generic fields.json → flat runtime object (WoodsCLI default transform).\n */\nexport type GenericFieldDefinition = {\n name: string\n type: string\n default?: unknown\n children?: GenericFieldDefinition[]\n}\n\nexport type GenericFieldValues = Record<string, unknown>\n\nfunction flattenField(\n field: GenericFieldDefinition,\n values: GenericFieldValues,\n out: Record<string, unknown>,\n): void {\n const key = field.name\n const value = values[key]\n\n if (field.type === 'group' && field.children && value && typeof value === 'object') {\n const groupValues = value as GenericFieldValues\n for (const child of field.children) {\n flattenField(child, groupValues, out)\n }\n return\n }\n\n if (field.type === 'repeater' && Array.isArray(value)) {\n out[key] = value\n return\n }\n\n out[key] = value ?? field.default ?? null\n}\n\nexport function mapFieldsToRuntime(\n schema: GenericFieldDefinition[],\n values: GenericFieldValues,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const field of schema) {\n flattenField(field, values, out)\n }\n return out\n}\n"],"mappings":"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","names":[]}
@@ -2,6 +2,6 @@
2
2
  "configVersion": 1,
3
3
  "projectType": "module-hybrid-cdn",
4
4
  "module": {"name": "demo", "uploadPath": "dw-modules/demo.module", "outDir": "dist/Demo.Module"},
5
- "cdn": {"mode": "jsdelivr", "buildIdFile": ".portal-build-id", "mirror": "portal.cdn.json"},
5
+ "cdn": {"mode": "jsdelivr", "buildIdFile": ".module-build-id", "mirror": "portal.cdn.json"},
6
6
  "dev": {"moduleConfigDir": ".wphs", "adapter": "./wphs.adapter.ts", "fieldsJson": "public/fields.json"}
7
7
  }
@@ -2,6 +2,6 @@
2
2
  "configVersion": 1,
3
3
  "projectType": "module-monolith",
4
4
  "module": {"name": "demo", "uploadPath": "dw-modules/demo.module", "outDir": "dist/Demo.Module"},
5
- "cdn": {"mode": "none", "buildIdFile": ".portal-build-id", "mirror": "portal.cdn.json"},
5
+ "cdn": {"mode": "none", "buildIdFile": ".module-build-id", "mirror": "portal.cdn.json"},
6
6
  "dev": {"moduleConfigDir": ".wphs", "fieldsJson": "public/fields.json"}
7
7
  }
@@ -24,26 +24,32 @@
24
24
  "default": true
25
25
  },
26
26
  {
27
- "name": "accent_color",
28
- "label": "Accent color",
29
- "type": "color",
27
+ "type": "group",
28
+ "name": "styles",
29
+ "label": "Styles",
30
30
  "tab": "STYLE",
31
- "default": {
32
- "color": "#0091ae",
33
- "opacity": 100
34
- }
35
- },
36
- {
37
- "name": "hero_style",
38
- "label": "Hero style",
39
- "type": "choice",
40
- "display": "select",
41
- "tab": "STYLE",
42
- "choices": [
43
- ["gradient", "Gradient"],
44
- ["minimal", "Minimal"],
45
- ["bold", "Bold"]
46
- ],
47
- "default": "gradient"
31
+ "children": [
32
+ {
33
+ "name": "accent_color",
34
+ "label": "Accent color",
35
+ "type": "color",
36
+ "default": {
37
+ "color": "#0091ae",
38
+ "opacity": 100
39
+ }
40
+ },
41
+ {
42
+ "name": "hero_style",
43
+ "label": "Hero style",
44
+ "type": "choice",
45
+ "display": "select",
46
+ "choices": [
47
+ ["gradient", "Gradient"],
48
+ ["minimal", "Minimal"],
49
+ ["bold", "Bold"]
50
+ ],
51
+ "default": "gradient"
52
+ }
53
+ ]
48
54
  }
49
55
  ]
@@ -0,0 +1,9 @@
1
+ {
2
+ "label": "WoodsPortal hybrid module",
3
+ "css_assets": [],
4
+ "external_js": [],
5
+ "global": false,
6
+ "host_template_types": ["PAGE", "BLOG_POST", "BLOG_LISTING"],
7
+ "content_types": ["SITE_PAGE", "LANDING_PAGE", "BLOG_POST", "BLOG_LISTING"],
8
+ "is_available_for_new_content": true
9
+ }
@@ -6,8 +6,8 @@
6
6
  title: {{ module.title|tojson }},
7
7
  tagline: {{ module.tagline|tojson }},
8
8
  show_feature_cards: {{ module.show_feature_cards|lower }},
9
- accent_color: {{ module.accent_color|tojson }},
10
- hero_style: {{ module.hero_style|tojson }},
9
+ accent_color: {{ module.styles.accent_color|tojson }},
10
+ hero_style: {{ module.styles.hero_style|tojson }},
11
11
  assetBaseUrl: "__PORTAL_CDN_BASE__",
12
12
  buildId: "__PORTAL_BUILD_ID__",
13
13
  isInEditor: {{ in_cms_editor|lower }}
@@ -16,8 +16,7 @@
16
16
  {% set portal_cdn_base = "__PORTAL_CDN_BASE__" %}
17
17
  {% set portal_vendor_script = "__PORTAL_VENDOR_FILE__" %}
18
18
  {% set portal_app_script = "__PORTAL_APP_FILE__" %}
19
- {% set portal_app_css = "__PORTAL_APP_CSS_FILE__" %}
20
- {{ require_css(portal_cdn_base ~ '/' ~ portal_app_css, { position: 'footer' }) }}
19
+ {# App styles ship in module.css (HubSpot editor cannot rely on external CDN CSS). #}
21
20
  {{ require_js(portal_cdn_base ~ '/' ~ portal_vendor_script, { position: 'footer', defer: true }) }}
22
21
  {{ require_js(portal_cdn_base ~ '/' ~ portal_app_script, { position: 'footer', defer: true }) }}
23
22
  <div id="app"></div>
@@ -0,0 +1,6 @@
1
+ @import 'tailwindcss' prefix(dw);
2
+
3
+ /* Prefixed utilities from tailwind-content.js (.temp) — must match prefix plugin on TSX */
4
+ @source '../../../.temp/**/*.{js,jsx,ts,tsx}';
5
+ @source '../../../.wphs/.kit-module-config/**/*.{ts,tsx}';
6
+ @source '../../../node_modules/@woodsportal/hubspot-kit/src/dev-module-config/**/*.{ts,tsx}';
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * CDN application chunk — mounts after vendor.js (HubSpot require_js order).
3
+ *
4
+ * Build extracts main.dev.css → app.{buildId}.css (linked from module.html).
5
+ * Runtime also injects that stylesheet when HubSpot preview skips require_css.
3
6
  */
4
7
  import '@/assets/css/main.dev.css'
5
8
 
6
9
  import { resolvePortalApiEndpoint } from '@/portal/api-endpoint'
10
+ import { ensurePortalCdnStylesheet } from '@/portal/ensure-portal-cdn-stylesheet'
7
11
  import { mountPortal } from '@/portal/mount-portal'
8
12
 
9
13
  declare const __PORTAL_BUILD_ID__: string | undefined
@@ -31,6 +35,7 @@ async function autoMount() {
31
35
  if (!document.getElementById('app')) {
32
36
  return
33
37
  }
38
+ await ensurePortalCdnStylesheet()
34
39
  await mountPortal()
35
40
  }
36
41
 
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * Thin HubSpot module.js bootstrap — JS app loads from CDN via require_js in module.html.
3
+ *
4
+ * CSS imports are extracted to module.css by the HubSpot Vite build (same pattern as
5
+ * digitalwoods.io-woodsportal-client-frontend). HubSpot CMS editor does not reliably
6
+ * load external jsDelivr stylesheets from module.html.
3
7
  */
8
+ import '@/assets/css/main.dev.css'
9
+
4
10
  declare global {
5
11
  interface Window {
6
12
  WoodsPortalHubSpotBootstrap?: { ready: true }
@@ -0,0 +1,58 @@
1
+ declare global {
2
+ interface Window {
3
+ hubSpotData?: {
4
+ assetBaseUrl?: string
5
+ buildId?: string
6
+ }
7
+ }
8
+ }
9
+
10
+ /** Resolve app.{buildId}.css on the CDN mirror (jsDelivr / custom base). */
11
+ export function portalCdnStylesheetUrl(): string | null {
12
+ const base = window.hubSpotData?.assetBaseUrl?.replace(/\/$/, '')
13
+ const buildId = window.hubSpotData?.buildId?.trim()
14
+ if (!base || !buildId) {
15
+ return null
16
+ }
17
+ return `${base}/app.${buildId}.css`
18
+ }
19
+
20
+ function waitForStylesheet(link: HTMLLinkElement): Promise<void> {
21
+ if (link.sheet) {
22
+ return Promise.resolve()
23
+ }
24
+
25
+ return new Promise((resolve) => {
26
+ link.addEventListener('load', () => resolve(), { once: true })
27
+ link.addEventListener('error', () => resolve(), { once: true })
28
+ })
29
+ }
30
+
31
+ /**
32
+ * HubSpot module.html uses require_css for app.*.css, but the page editor preview
33
+ * can skip external footer stylesheets. Inject the CDN stylesheet before React mounts.
34
+ */
35
+ export async function ensurePortalCdnStylesheet(): Promise<void> {
36
+ const href = portalCdnStylesheetUrl()
37
+ if (!href) {
38
+ return
39
+ }
40
+
41
+ const marker = 'data-portal-cdn-css'
42
+ const existing = document.querySelector<HTMLLinkElement>(`link[${marker}]`)
43
+ if (existing) {
44
+ if (existing.href === href) {
45
+ await waitForStylesheet(existing)
46
+ return
47
+ }
48
+ existing.remove()
49
+ }
50
+
51
+ const link = document.createElement('link')
52
+ link.rel = 'stylesheet'
53
+ link.href = href
54
+ link.crossOrigin = 'anonymous'
55
+ link.setAttribute(marker, 'true')
56
+ document.head.appendChild(link)
57
+ await waitForStylesheet(link)
58
+ }
@@ -2,7 +2,7 @@
2
2
  "configVersion": 1,
3
3
  "projectType": "module-hybrid-cdn",
4
4
  "module": {"name": "my-portal-module", "uploadPath": "dw-modules/my-portal-module.module", "outDir": "dist/MyPortal.Module"},
5
- "cdn": {"mode": "jsdelivr", "buildIdFile": ".portal-build-id", "mirror": "portal.cdn.json"},
5
+ "cdn": {"mode": "jsdelivr", "buildIdFile": ".module-build-id", "mirror": "portal.cdn.json"},
6
6
  "entries": {"app": "src/cdn/app-entry.ts", "vendor": "src/cdn/vendor-entry.ts", "hubspotBootstrap": "src/hubspot/bootstrap.ts", "localDev": "src/main.tsx"},
7
7
  "dev": {"moduleConfigDir": ".wphs", "adapter": "./wphs.adapter.ts", "fieldsJson": "public/fields.json"}
8
8
  }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "cdn": {
10
10
  "mode": "none",
11
- "buildIdFile": ".portal-build-id",
11
+ "buildIdFile": ".module-build-id",
12
12
  "mirror": "portal.cdn.json"
13
13
  },
14
14
  "entries": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woodsportal/hubspot-kit",
3
- "version": "1.0.31",
3
+ "version": "1.0.34",
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",
@@ -51,7 +51,7 @@
51
51
  "build": "tsup",
52
52
  "dev": "tsup --watch",
53
53
  "typecheck": "tsc --noEmit",
54
- "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",
54
+ "test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' 'scripts/portal-build-id.test.mjs' && vitest run src/dev-module-config/__tests__/normalize-repeater-items.test.ts src/dev-module-config/__tests__/module-config-transform.test.ts src/dev-module-config/__tests__/flatten-visible-fields.test.ts",
55
55
  "test:examples": "node examples/smoke/run-matrix.mjs --tier static",
56
56
  "lint": "eslint src",
57
57
  "verify": "npm run typecheck && npm run test && npm run test:examples && npm run build",
@@ -9,7 +9,8 @@ import { findProjectRoot } from './project-root.mjs'
9
9
 
10
10
  import { envWithPortalBuildId } from './resolve-portal-build-id.mjs'
11
11
  import { envForPortalBuild, defaultDatadogFlag } from './portal-vite-env.mjs'
12
- import { resolvePortalViteMode } from './portal-env-mode.mjs'
12
+ import { isPrefixBuildMode, resolvePortalViteMode } from './portal-env-mode.mjs'
13
+ import tailwindContent from './tailwind/tailwind-content.js'
13
14
  import { resolveViteConfig } from './resolve-vite-config.mjs'
14
15
  import {
15
16
  exitOnSpawnFailure,
@@ -21,6 +22,10 @@ const root = findProjectRoot()
21
22
 
22
23
  const mode = resolvePortalViteMode()
23
24
 
25
+ if (isPrefixBuildMode(mode)) {
26
+ await tailwindContent()
27
+ }
28
+
24
29
  const env = envForPortalBuild(mode, root, {
25
30
  VITE_DATADOG_RUM_ENABLED: defaultDatadogFlag(mode),
26
31
  })
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Bump .wphs/state/module-build-id.json and print JSON result to stdout.
4
+ * Used by wp deploy / wp build --cdn-only before CDN builds.
5
+ */
6
+ import { bumpModuleBuildId, clearPortalBuildIdCache } from './portal-build-id.mjs'
7
+ import { findProjectRoot } from './project-root.mjs'
8
+
9
+ const args = new Set(process.argv.slice(2))
10
+ const dryRun = args.has('--dry-run')
11
+ const tierArg = [...args].find((a) => a.startsWith('--tier='))
12
+ const tier = tierArg?.split('=')[1]
13
+
14
+ clearPortalBuildIdCache()
15
+
16
+ try {
17
+ const result = bumpModuleBuildId({
18
+ projectRoot: findProjectRoot(),
19
+ tier,
20
+ dryRun,
21
+ env: process.env,
22
+ })
23
+ process.stdout.write(`${JSON.stringify(result)}\n`)
24
+ } catch (error) {
25
+ const message = error instanceof Error ? error.message : String(error)
26
+ console.error(`[bump-module-build-id] ERROR: ${message}`)
27
+ process.exit(1)
28
+ }
@@ -22,6 +22,7 @@ const ALLOWED_DIST_FILES = new Set([
22
22
  'module.js',
23
23
  'module.css',
24
24
  'fields.json',
25
+ 'meta.json',
25
26
  ])
26
27
 
27
28
  /**
@@ -89,6 +90,35 @@ export function injectCdnManifestIntoModuleHtml(html, manifestPath) {
89
90
  .replaceAll('__PORTAL_APP_CSS_FILE__', appCss.file)
90
91
  }
91
92
 
93
+ /**
94
+ * HubSpot serves module.css with every module instance (including the CMS editor preview).
95
+ * External jsDelivr stylesheets from module.html are often blocked in the editor iframe.
96
+ */
97
+ export function mergeCdnAppStylesIntoModuleCss(moduleDir, manifestPath) {
98
+ if (!existsSync(manifestPath)) {
99
+ return false
100
+ }
101
+
102
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
103
+ const appCss = manifest.styles?.find((s) => s.name === 'app') ?? manifest.styles?.[0]
104
+ if (!appCss?.file) {
105
+ throw new Error('manifest.json must include app styles for module.css merge')
106
+ }
107
+
108
+ const cdnCssPath = resolve(moduleDir, '../cdn', appCss.file)
109
+ if (!existsSync(cdnCssPath)) {
110
+ throw new Error(
111
+ `Missing ${cdnCssPath} — run build:cdn before hubspot post-build (needed for module.css)`,
112
+ )
113
+ }
114
+
115
+ const banner =
116
+ '/* Hybrid CDN app styles — copied from dist/cdn at build time for HubSpot module.css */\n'
117
+ const css = readFileSync(cdnCssPath, 'utf8')
118
+ writeFileSync(resolve(moduleDir, 'module.css'), `${banner}${css}`, 'utf8')
119
+ return true
120
+ }
121
+
92
122
  /**
93
123
  * @param {string} moduleDir absolute path to dist/WoodsPortal.Module
94
124
  * @param {string} publicDir absolute path to public/
@@ -120,7 +150,7 @@ export function finalizeHubSpotModuleDist(moduleDir, publicDir, options = {}) {
120
150
  }
121
151
  }
122
152
 
123
- for (const file of ['module.html', 'fields.json']) {
153
+ for (const file of ['module.html', 'fields.json', 'meta.json']) {
124
154
  const src = resolve(publicDir, file)
125
155
  if (!existsSync(src)) {
126
156
  throw new Error(`Missing public/${file} (required for HubSpot module upload)`)
@@ -134,6 +164,10 @@ export function finalizeHubSpotModuleDist(moduleDir, publicDir, options = {}) {
134
164
  }
135
165
  }
136
166
 
167
+ if (existsSync(manifestPath)) {
168
+ mergeCdnAppStylesIntoModuleCss(moduleDir, manifestPath)
169
+ }
170
+
137
171
  const escaped = escapeHubSpotModuleJs(readFileSync(jsPath, 'utf8'))
138
172
  assertNoForbiddenHubLTokens(escaped, jsPath)
139
173
  writeFileSync(jsPath, escaped, 'utf8')
@@ -28,8 +28,14 @@ function readWoodscliConfig(projectRoot) {
28
28
  }
29
29
  }
30
30
 
31
+ const LOCAL_CDN_MIRROR_FILE = 'portal.cdn.local.json'
32
+
31
33
  function resolveMirrorPath(projectRoot, hsConfig) {
32
34
  const mirror = hsConfig?.cdn?.mirror?.trim() || 'portal.cdn.json'
35
+ const localMirror = resolve(projectRoot, LOCAL_CDN_MIRROR_FILE)
36
+ if (existsSync(localMirror)) {
37
+ return localMirror
38
+ }
33
39
  return resolve(projectRoot, mirror)
34
40
  }
35
41
 
@@ -61,6 +67,8 @@ export function loadPortalCdnConfig(env = process.env, projectRoot) {
61
67
  fileConfig.publishMode ||
62
68
  DEFAULTS.publishMode
63
69
 
70
+ const usingLocalMirror = loadedFrom?.endsWith(LOCAL_CDN_MIRROR_FILE) ?? false
71
+
64
72
  return {
65
73
  publishMode,
66
74
  cdnMode,
@@ -69,6 +77,7 @@ export function loadPortalCdnConfig(env = process.env, projectRoot) {
69
77
  cdnPathInRepo: fileConfig.cdnPathInRepo || DEFAULTS.cdnPathInRepo,
70
78
  defaultBranch: fileConfig.defaultBranch || DEFAULTS.defaultBranch,
71
79
  configPath: loadedFrom,
80
+ usingLocalMirror,
72
81
  }
73
82
  }
74
83
 
@@ -82,10 +91,19 @@ export function envWithPortalCdnConfig(baseEnv = process.env, projectRoot) {
82
91
  }
83
92
  }
84
93
 
85
- /** jsDelivr base URL for a tagged release. */
86
- export function jsDelivrBaseUrl(github, buildId, cdnPathInRepo = DEFAULTS.cdnPathInRepo) {
94
+ /**
95
+ * jsDelivr base URL for GitHub CDN mirror.
96
+ * Uses the default branch ref (not the git tag): chunk filenames already include buildId,
97
+ * and jsDelivr indexes branches reliably (tag refs often 404 until propagated).
98
+ */
99
+ export function jsDelivrBaseUrl(
100
+ github,
101
+ cdnPathInRepo = DEFAULTS.cdnPathInRepo,
102
+ branch = DEFAULTS.defaultBranch,
103
+ ) {
87
104
  const path = cdnPathInRepo.replace(/^\//, '').replace(/\/$/, '')
88
- return `https://cdn.jsdelivr.net/gh/${github}@${buildId}/${path}`
105
+ const ref = branch.replace(/^@/, '').trim() || DEFAULTS.defaultBranch
106
+ return `https://cdn.jsdelivr.net/gh/${github}@${ref}/${path}`
89
107
  }
90
108
 
91
109
  export { DEFAULTS }