@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
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * CDN application chunk — mounts after vendor.js (HubSpot require_js order).
3
+ *
4
+ * Build-time: main.prod.css → app.{buildId}.css (merged into module.css at post-build).
5
+ * Runtime: skip JS CSS imports — HubSpot serves module.css on published pages.
3
6
  */
4
- import '@/assets/css/main.dev.css'
7
+ import '@/assets/css/main.prod.css'
5
8
 
6
9
  import { resolvePortalApiEndpoint } from '@/portal/api-endpoint'
10
+ import { ensurePortalCdnStylesheetIfNeeded } 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,7 +35,14 @@ async function autoMount() {
31
35
  if (!document.getElementById('app')) {
32
36
  return
33
37
  }
34
- await mountPortal()
38
+ await ensurePortalCdnStylesheetIfNeeded()
39
+ await mountPortal({ skipRuntimeCss: true })
35
40
  }
36
41
 
37
- void autoMount()
42
+ if (document.readyState === 'loading') {
43
+ document.addEventListener('DOMContentLoaded', () => {
44
+ void autoMount()
45
+ })
46
+ } else {
47
+ void autoMount()
48
+ }
@@ -1,23 +1,21 @@
1
1
  import { useStarterPreview } from '../hooks/use-starter-preview'
2
2
  import { STARTER_FEATURES, WOODSCLI_ABOUT } from '../content/woodscli-guide-content'
3
3
 
4
- function heroClass(style: ReturnType<typeof useStarterPreview>['heroStyle']): string {
5
- if (style === 'minimal') {
6
- return 'bg-[var(--mc-bg-shell,#f5f8fa)] text-[var(--mc-text,#33475b)] border-b border-[var(--mc-border,#cbd6e2)]'
7
- }
8
- if (style === 'bold') {
9
- return 'bg-[#1c1c1f] text-white'
10
- }
11
- return 'bg-gradient-to-br from-[var(--starter-accent,#0091ae)] to-[#ff7a59] text-white'
12
- }
13
-
14
4
  export function StarterHome() {
15
5
  const { title, tagline, showFeatureCards, accentColor, heroStyle } = useStarterPreview()
16
6
 
17
7
  return (
18
8
  <>
19
9
  <div style={{ ['--starter-accent' as string]: accentColor }}>
20
- <section className={`px-6 py-14 sm:px-10 sm:py-16 ${heroClass(heroStyle)}`}>
10
+ <section
11
+ className={
12
+ heroStyle === 'minimal'
13
+ ? 'px-6 py-14 sm:px-10 sm:py-16 bg-[var(--mc-bg-shell,#f5f8fa)] text-[var(--mc-text,#33475b)] border-b border-[var(--mc-border,#cbd6e2)]'
14
+ : heroStyle === 'bold'
15
+ ? 'px-6 py-14 sm:px-10 sm:py-16 bg-[#1c1c1f] text-white'
16
+ : 'px-6 py-14 sm:px-10 sm:py-16 bg-gradient-to-br from-[var(--starter-accent,#0091ae)] to-[#ff7a59] text-white'
17
+ }
18
+ >
21
19
  <div className="mx-auto max-w-4xl">
22
20
  <p className="mb-2 text-xs font-semibold uppercase tracking-widest opacity-80">
23
21
  {WOODSCLI_ABOUT.package} · local dev
@@ -12,18 +12,36 @@ function normalizeHeroStyle(raw: unknown): StarterPreviewValues['heroStyle'] {
12
12
  return 'gradient'
13
13
  }
14
14
 
15
+ /** CONTENT-tab fields may live under a `content` group (fields.json) or flat legacy keys. */
16
+ function readContentField(module: Record<string, unknown>, key: string): unknown {
17
+ const content = module.content
18
+ if (content && typeof content === 'object' && key in (content as Record<string, unknown>)) {
19
+ return (content as Record<string, unknown>)[key]
20
+ }
21
+ return module[key]
22
+ }
23
+
24
+ /** STYLE-tab fields may live under a `styles` group (fields.json) or flat legacy keys. */
25
+ function readStyleField(module: Record<string, unknown>, key: string): unknown {
26
+ const styles = module.styles
27
+ if (styles && typeof styles === 'object' && key in (styles as Record<string, unknown>)) {
28
+ return (styles as Record<string, unknown>)[key]
29
+ }
30
+ return module[key]
31
+ }
32
+
15
33
  export function mapStarterPreviewValues(module: Record<string, unknown>): StarterPreviewValues {
16
- const accent = module.accent_color as { color?: string } | undefined
34
+ const accent = readStyleField(module, 'accent_color') as { color?: string } | undefined
17
35
 
18
36
  return {
19
- title: String(module.title ?? 'My HubSpot Portal'),
37
+ title: String(readContentField(module, 'title') ?? 'My HubSpot Portal'),
20
38
  tagline: String(
21
- module.tagline ??
39
+ readContentField(module, 'tagline') ??
22
40
  'Customize this preview with Ctrl+Shift+Z, then replace this page with your portal app.',
23
41
  ),
24
- showFeatureCards: module.show_feature_cards !== false,
42
+ showFeatureCards: readContentField(module, 'show_feature_cards') !== false,
25
43
  accentColor: accent?.color ?? '#0091ae',
26
- heroStyle: normalizeHeroStyle(module.hero_style),
44
+ heroStyle: normalizeHeroStyle(readStyleField(module, 'hero_style')),
27
45
  }
28
46
  }
29
47
 
@@ -12,6 +12,7 @@ export function subscribeStarterPreview(
12
12
  ): () => void {
13
13
  const refresh = () => onChange(mapStarterPreviewValues(getModuleFieldValues()))
14
14
 
15
+ refresh()
15
16
  window.addEventListener(MODULE_CONFIG_CHANGED, refresh)
16
17
  return () => window.removeEventListener(MODULE_CONFIG_CHANGED, refresh)
17
18
  }
@@ -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.prod.css'
9
+
4
10
  declare global {
5
11
  interface Window {
6
12
  WoodsPortalHubSpotBootstrap?: { ready: true }
@@ -0,0 +1,115 @@
1
+ declare global {
2
+ interface Window {
3
+ hubSpotData?: {
4
+ assetBaseUrl?: string
5
+ buildId?: string
6
+ isInEditor?: boolean
7
+ }
8
+ hsInEditor?: boolean
9
+ }
10
+ }
11
+
12
+ /** Resolve app.{buildId}.css on the CDN mirror (jsDelivr / custom base). */
13
+ export function portalCdnStylesheetUrl(): string | null {
14
+ const base = window.hubSpotData?.assetBaseUrl?.replace(/\/$/, '')
15
+ const buildId = window.hubSpotData?.buildId?.trim()
16
+ if (!base || !buildId) {
17
+ return null
18
+ }
19
+ return `${base}/app.${buildId}.css`
20
+ }
21
+
22
+ function isHubSpotCmsEditor(): boolean {
23
+ if (typeof window === 'undefined') {
24
+ return false
25
+ }
26
+ return (
27
+ window.hubSpotData?.isInEditor === true ||
28
+ window.hsInEditor === true
29
+ )
30
+ }
31
+
32
+ /** True when HubSpot already loaded merged module.css with dw-prefixed portal utilities. */
33
+ export function hasPrefixedPortalStylesLoaded(): boolean {
34
+ if (typeof document === 'undefined') {
35
+ return false
36
+ }
37
+
38
+ for (const link of document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')) {
39
+ if (/module\.css/i.test(link.href)) {
40
+ return true
41
+ }
42
+ }
43
+
44
+ try {
45
+ for (const sheet of document.styleSheets) {
46
+ try {
47
+ const rules = sheet.cssRules
48
+ for (let i = 0; i < Math.min(rules.length, 100); i++) {
49
+ const text = rules[i]?.cssText ?? ''
50
+ if (text.includes('dw\\:') || text.includes('.dw:')) {
51
+ return true
52
+ }
53
+ }
54
+ } catch {
55
+ // Cross-origin stylesheets are not readable.
56
+ }
57
+ }
58
+ } catch {
59
+ // ignore
60
+ }
61
+
62
+ return false
63
+ }
64
+
65
+ /** Editor-only fallback when module.css did not load (external CDN CSS blocked in iframe). */
66
+ export function needsPortalCdnStylesheetFallback(): boolean {
67
+ if (!isHubSpotCmsEditor()) {
68
+ return false
69
+ }
70
+ return !hasPrefixedPortalStylesLoaded()
71
+ }
72
+
73
+ function waitForStylesheet(link: HTMLLinkElement): Promise<void> {
74
+ if (link.sheet) {
75
+ return Promise.resolve()
76
+ }
77
+
78
+ return new Promise((resolve) => {
79
+ link.addEventListener('load', () => resolve(), { once: true })
80
+ link.addEventListener('error', () => resolve(), { once: true })
81
+ })
82
+ }
83
+
84
+ /** Inject CDN app.{buildId}.css when HubSpot editor preview skipped module.css. */
85
+ export async function ensurePortalCdnStylesheet(): Promise<void> {
86
+ const href = portalCdnStylesheetUrl()
87
+ if (!href) {
88
+ return
89
+ }
90
+
91
+ const marker = 'data-portal-cdn-css'
92
+ const existing = document.querySelector<HTMLLinkElement>(`link[${marker}]`)
93
+ if (existing) {
94
+ if (existing.href === href) {
95
+ await waitForStylesheet(existing)
96
+ return
97
+ }
98
+ existing.remove()
99
+ }
100
+
101
+ const link = document.createElement('link')
102
+ link.rel = 'stylesheet'
103
+ link.href = href
104
+ link.crossOrigin = 'anonymous'
105
+ link.setAttribute(marker, 'true')
106
+ document.head.appendChild(link)
107
+ await waitForStylesheet(link)
108
+ }
109
+
110
+ export async function ensurePortalCdnStylesheetIfNeeded(): Promise<void> {
111
+ if (!needsPortalCdnStylesheetFallback()) {
112
+ return
113
+ }
114
+ await ensurePortalCdnStylesheet()
115
+ }
@@ -1,12 +1,42 @@
1
1
  import ReactDOM from 'react-dom/client'
2
2
 
3
+ import isPrefix from '@woodsportal/hubspot-kit/scripts/tailwind/prefix.env.js'
4
+
3
5
  import { App } from '@/app'
4
6
 
5
- /** Production mount — same App shell as woodsportal-client-frontend CDN app chunk. */
6
- export async function mountPortal() {
7
- const root = document.getElementById('app')
8
- if (!root) {
7
+ async function loadCss() {
8
+ if (isPrefix(import.meta.env.MODE)) {
9
+ await import('@/assets/css/main.prod.css')
10
+ } else {
11
+ await import('@/assets/css/main.dev.css')
12
+ }
13
+ }
14
+
15
+ export type MountPortalOptions = {
16
+ /** HubSpot serves module.css — skip duplicate global styles on CDN app chunk */
17
+ includeGlobalStyles?: boolean
18
+ /** HubSpot loads app styles via module.css — skip runtime CSS imports in CDN app chunk */
19
+ skipRuntimeCss?: boolean
20
+ /** Kit dev entry renders App separately with module-config shell */
21
+ skipAppRender?: boolean
22
+ }
23
+
24
+ /** Mount the portal into `#app` (CDN app chunk, monolith, or production paths). */
25
+ export async function mountPortal(options: MountPortalOptions = {}) {
26
+ const { skipRuntimeCss = false, skipAppRender = false } = options
27
+
28
+ if (!skipRuntimeCss) {
29
+ await loadCss()
30
+ }
31
+
32
+ if (skipAppRender) {
33
+ return
34
+ }
35
+
36
+ const rootElement = document.getElementById('app')
37
+ if (!rootElement) {
9
38
  return
10
39
  }
11
- ReactDOM.createRoot(root).render(<App />)
40
+
41
+ ReactDOM.createRoot(rootElement).render(<App />)
12
42
  }
@@ -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
  }
@@ -23,22 +23,29 @@
23
23
  "default": true
24
24
  },
25
25
  {
26
- "name": "accent_color",
27
- "label": "Accent color",
28
- "type": "color",
26
+ "type": "group",
27
+ "name": "styles",
28
+ "label": "Styles",
29
29
  "tab": "STYLE",
30
- "default": { "color": "#0091ae", "opacity": 100 }
31
- },
32
- {
33
- "name": "hero_style",
34
- "label": "Hero style",
35
- "type": "choice",
36
- "tab": "STYLE",
37
- "choices": [
38
- ["gradient", "Gradient"],
39
- ["minimal", "Minimal"],
40
- ["bold", "Bold"]
41
- ],
42
- "default": "gradient"
30
+ "children": [
31
+ {
32
+ "name": "accent_color",
33
+ "label": "Accent color",
34
+ "type": "color",
35
+ "default": { "color": "#0091ae", "opacity": 100 }
36
+ },
37
+ {
38
+ "name": "hero_style",
39
+ "label": "Hero style",
40
+ "type": "choice",
41
+ "display": "select",
42
+ "choices": [
43
+ ["gradient", "Gradient"],
44
+ ["minimal", "Minimal"],
45
+ ["bold", "Bold"]
46
+ ],
47
+ "default": "gradient"
48
+ }
49
+ ]
43
50
  }
44
51
  ]
@@ -0,0 +1,9 @@
1
+ {
2
+ "label": "WoodsPortal monolith 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
+ }
@@ -1,23 +1,21 @@
1
1
  import { useStarterPreview } from '../hooks/use-starter-preview'
2
2
  import { STARTER_FEATURES, WOODSCLI_ABOUT } from '../content/woodscli-guide-content'
3
3
 
4
- function heroClass(style: ReturnType<typeof useStarterPreview>['heroStyle']): string {
5
- if (style === 'minimal') {
6
- return 'bg-[var(--mc-bg-shell,#f5f8fa)] text-[var(--mc-text,#33475b)] border-b border-[var(--mc-border,#cbd6e2)]'
7
- }
8
- if (style === 'bold') {
9
- return 'bg-[#1c1c1f] text-white'
10
- }
11
- return 'bg-gradient-to-br from-[var(--starter-accent,#0091ae)] to-[#ff7a59] text-white'
12
- }
13
-
14
4
  export function StarterHome() {
15
5
  const { title, tagline, showFeatureCards, accentColor, heroStyle } = useStarterPreview()
16
6
 
17
7
  return (
18
8
  <>
19
9
  <div style={{ ['--starter-accent' as string]: accentColor }}>
20
- <section className={`px-6 py-14 sm:px-10 sm:py-16 ${heroClass(heroStyle)}`}>
10
+ <section
11
+ className={
12
+ heroStyle === 'minimal'
13
+ ? 'px-6 py-14 sm:px-10 sm:py-16 bg-[var(--mc-bg-shell,#f5f8fa)] text-[var(--mc-text,#33475b)] border-b border-[var(--mc-border,#cbd6e2)]'
14
+ : heroStyle === 'bold'
15
+ ? 'px-6 py-14 sm:px-10 sm:py-16 bg-[#1c1c1f] text-white'
16
+ : 'px-6 py-14 sm:px-10 sm:py-16 bg-gradient-to-br from-[var(--starter-accent,#0091ae)] to-[#ff7a59] text-white'
17
+ }
18
+ >
21
19
  <div className="mx-auto max-w-4xl">
22
20
  <p className="mb-2 text-xs font-semibold uppercase tracking-widest opacity-80">
23
21
  {WOODSCLI_ABOUT.package} · local dev
@@ -12,18 +12,36 @@ function normalizeHeroStyle(raw: unknown): StarterPreviewValues['heroStyle'] {
12
12
  return 'gradient'
13
13
  }
14
14
 
15
+ /** CONTENT-tab fields may live under a `content` group (fields.json) or flat legacy keys. */
16
+ function readContentField(module: Record<string, unknown>, key: string): unknown {
17
+ const content = module.content
18
+ if (content && typeof content === 'object' && key in (content as Record<string, unknown>)) {
19
+ return (content as Record<string, unknown>)[key]
20
+ }
21
+ return module[key]
22
+ }
23
+
24
+ /** STYLE-tab fields may live under a `styles` group (fields.json) or flat legacy keys. */
25
+ function readStyleField(module: Record<string, unknown>, key: string): unknown {
26
+ const styles = module.styles
27
+ if (styles && typeof styles === 'object' && key in (styles as Record<string, unknown>)) {
28
+ return (styles as Record<string, unknown>)[key]
29
+ }
30
+ return module[key]
31
+ }
32
+
15
33
  export function mapStarterPreviewValues(module: Record<string, unknown>): StarterPreviewValues {
16
- const accent = module.accent_color as { color?: string } | undefined
34
+ const accent = readStyleField(module, 'accent_color') as { color?: string } | undefined
17
35
 
18
36
  return {
19
- title: String(module.title ?? 'My HubSpot Portal'),
37
+ title: String(readContentField(module, 'title') ?? 'My HubSpot Portal'),
20
38
  tagline: String(
21
- module.tagline ??
39
+ readContentField(module, 'tagline') ??
22
40
  'Customize this preview with Ctrl+Shift+Z, then replace this page with your portal app.',
23
41
  ),
24
- showFeatureCards: module.show_feature_cards !== false,
42
+ showFeatureCards: readContentField(module, 'show_feature_cards') !== false,
25
43
  accentColor: accent?.color ?? '#0091ae',
26
- heroStyle: normalizeHeroStyle(module.hero_style),
44
+ heroStyle: normalizeHeroStyle(readStyleField(module, 'hero_style')),
27
45
  }
28
46
  }
29
47
 
@@ -12,6 +12,7 @@ export function subscribeStarterPreview(
12
12
  ): () => void {
13
13
  const refresh = () => onChange(mapStarterPreviewValues(getModuleFieldValues()))
14
14
 
15
+ refresh()
15
16
  window.addEventListener(MODULE_CONFIG_CHANGED, refresh)
16
17
  return () => window.removeEventListener(MODULE_CONFIG_CHANGED, refresh)
17
18
  }
@@ -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": {
@@ -1,11 +1,7 @@
1
- import { defineConfig } from 'vite'
2
- import viteReact from '@vitejs/plugin-react'
3
- import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
4
1
  import { existsSync } from 'node:fs'
5
- import { dirname, resolve } from 'node:path'
6
- import { fileURLToPath } from 'node:url'
7
- import tailwindcss from '@tailwindcss/vite'
2
+ import { resolve } from 'node:path'
8
3
 
4
+ import { importConsumerDep } from '../scripts/resolve-consumer-dep.mjs'
9
5
  import tailwindPrefixPlugin from '../scripts/tailwind/prefix-plugin.js'
10
6
  import TailwindContentPlugin from '../scripts/tailwind/tailwind-content-plugin.js'
11
7
  import sdkLogBridge from '../scripts/vite/sdk-log-bridge.js'
@@ -25,8 +21,18 @@ const { config } = readWoodsportalConfig(projectRoot)
25
21
  const routesDir = resolve(projectRoot, 'src/routes')
26
22
  const hasTanStackRoutes = existsSync(routesDir)
27
23
 
28
- export default defineConfig(({ mode, command }) => {
29
- return {
24
+ export default loadDevViteConfig()
25
+
26
+ async function loadDevViteConfig() {
27
+ // Load vite before plugins: parallel import of vite + @vitejs/plugin-react breaks on Node 20+ (CJS/ESM interop).
28
+ const { defineConfig } = await importConsumerDep('vite')
29
+ const [{ default: viteReact }, { TanStackRouterVite }, { default: tailwindcss }] = await Promise.all([
30
+ importConsumerDep('@vitejs/plugin-react'),
31
+ importConsumerDep('@tanstack/router-plugin/vite'),
32
+ importConsumerDep('@tailwindcss/vite'),
33
+ ])
34
+
35
+ return defineConfig(({ mode, command }) => ({
30
36
  root: projectRoot,
31
37
  plugins: [
32
38
  bootstrapShimResolver({ bootstrapShim, bootstrapGreenfieldShim, kitRoot }),
@@ -72,5 +78,5 @@ export default defineConfig(({ mode, command }) => {
72
78
  allow: [projectRoot, kitRoot],
73
79
  },
74
80
  },
75
- }
76
- })
81
+ }))
82
+ }