@woodsportal/hubspot-kit 5.0.3-beta.0 → 5.0.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woodsportal/hubspot-kit",
3
- "version": "5.0.3-beta.0",
3
+ "version": "5.0.3-beta.2",
4
4
  "packageManager": "pnpm@10.12.1",
5
5
  "description": "WoodsCLI — HubSpot CMS dev kit by WoodsPortal. wp CLI, Vite presets, module-config overlay, and theme/module build pipelines.",
6
6
  "type": "module",
@@ -19,7 +19,35 @@ import {
19
19
  } from './portal-subprocess-stdio.mjs'
20
20
  import { spawnViteSync } from './resolve-vite-spawn.mjs'
21
21
 
22
+ import { existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
23
+
24
+ /** Drop versioned CDN outputs from prior builds on the same worker. */
25
+ function purgeStaleCdnArtifacts(cdnDir, buildId) {
26
+ if (!existsSync(cdnDir)) {
27
+ mkdirSync(cdnDir, { recursive: true })
28
+ return
29
+ }
30
+
31
+ for (const entry of readdirSync(cdnDir)) {
32
+ if (entry === 'assets') continue
33
+ const full = resolve(cdnDir, entry)
34
+ if (entry.endsWith('.js') || entry.endsWith('.css') || entry === 'manifest.json' || entry === 'n.json') {
35
+ rmSync(full, { force: true })
36
+ continue
37
+ }
38
+ if (entry === 'chunks') {
39
+ rmSync(full, { recursive: true, force: true })
40
+ }
41
+ }
42
+
43
+ if (buildId) {
44
+ console.log(`[build-cdn] purged prior dist/cdn artifacts before buildId ${buildId}`)
45
+ }
46
+ }
47
+
48
+
22
49
  const root = findProjectRoot()
50
+ const cdnDir = resolve(root, 'dist/cdn')
23
51
 
24
52
  const mode = resolvePortalViteMode()
25
53
 
@@ -31,6 +59,8 @@ const env = envForPortalBuild(mode, root, {
31
59
  VITE_DATADOG_RUM_ENABLED: defaultDatadogFlag(mode),
32
60
  })
33
61
 
62
+ purgeStaleCdnArtifacts(cdnDir, env.MODULE_BUILD_ID ?? env.PORTAL_BUILD_ID)
63
+
34
64
  function runWithEnv(config, buildMode) {
35
65
  const result = spawnViteSync(
36
66
  root,
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Resolve vendor/app/css filenames for the active CDN build id.
3
+ * Prevents stale dist/cdn artifacts from an older build id winning .find(name === 'vendor').
4
+ */
5
+
6
+ /**
7
+ * @param {string} buildId
8
+ * @param {string} file
9
+ */
10
+ export function cdnArtifactMatchesBuildId(buildId, file) {
11
+ return String(file).includes(buildId)
12
+ }
13
+
14
+ /**
15
+ * @param {string} role vendor | app
16
+ * @param {string} buildId
17
+ */
18
+ export function expectedCdnScriptBasename(role, buildId) {
19
+ return `${role}.${buildId}.js`
20
+ }
21
+
22
+ /** @param {string} buildId */
23
+ export function expectedCdnAppCssBasename(buildId) {
24
+ return `app.${buildId}.css`
25
+ }
26
+
27
+ /**
28
+ * @param {{ buildId: string, scripts?: Array<{ name?: string, file?: string }>, styles?: Array<{ name?: string, file?: string }> }} manifest
29
+ */
30
+ export function resolveCdnPrimaryScripts(manifest) {
31
+ const buildId = manifest.buildId?.trim()
32
+ if (!buildId) {
33
+ throw new Error('manifest.json missing buildId')
34
+ }
35
+
36
+ const vendorExpected = expectedCdnScriptBasename('vendor', buildId)
37
+ const appExpected = expectedCdnScriptBasename('app', buildId)
38
+ const appCssExpected = expectedCdnAppCssBasename(buildId)
39
+
40
+ const vendor = manifest.scripts?.find((s) => s.file === vendorExpected)
41
+ const app = manifest.scripts?.find((s) => s.file === appExpected)
42
+ const appCss =
43
+ manifest.styles?.find((s) => s.file === appCssExpected) ??
44
+ manifest.styles?.find((s) => s.name === 'app' && s.file?.includes(buildId))
45
+
46
+ if (!vendor?.file || !app?.file) {
47
+ const vendorCandidates = (manifest.scripts ?? [])
48
+ .filter((s) => s.name === 'vendor')
49
+ .map((s) => s.file)
50
+ .join(', ')
51
+ const appCandidates = (manifest.scripts ?? [])
52
+ .filter((s) => s.name === 'app')
53
+ .map((s) => s.file)
54
+ .join(', ')
55
+ throw new Error(
56
+ `manifest.json must include ${vendorExpected} and ${appExpected} (buildId=${buildId}). ` +
57
+ `Found vendor=[${vendorCandidates || 'none'}], app=[${appCandidates || 'none'}]`,
58
+ )
59
+ }
60
+
61
+ if (!appCss?.file) {
62
+ throw new Error(`manifest.json must include ${appCssExpected} for buildId ${buildId}`)
63
+ }
64
+
65
+ for (const [label, file] of [
66
+ ['vendor', vendor.file],
67
+ ['app', app.file],
68
+ ['app css', appCss.file],
69
+ ]) {
70
+ if (!cdnArtifactMatchesBuildId(buildId, file)) {
71
+ throw new Error(`manifest ${label} file ${file} does not match buildId ${buildId}`)
72
+ }
73
+ }
74
+
75
+ return { buildId, vendorFile: vendor.file, appFile: app.file, appCssFile: appCss.file }
76
+ }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { resolveCdnPrimaryScripts } from './cdn-release-artifacts.mjs'
5
+
6
+ test('resolveCdnPrimaryScripts picks scripts for manifest buildId, not lexicographically first vendor', () => {
7
+ const manifest = {
8
+ buildId: '5.0.2-beta.802',
9
+ scripts: [
10
+ { name: 'vendor', file: 'vendor.5.0.2-beta.800.js' },
11
+ { name: 'vendor', file: 'vendor.5.0.2-beta.802.js' },
12
+ { name: 'app', file: 'app.5.0.2-beta.802.js' },
13
+ ],
14
+ styles: [{ name: 'app', file: 'app.5.0.2-beta.802.css' }],
15
+ }
16
+
17
+ const resolved = resolveCdnPrimaryScripts(manifest)
18
+ assert.equal(resolved.vendorFile, 'vendor.5.0.2-beta.802.js')
19
+ assert.equal(resolved.appFile, 'app.5.0.2-beta.802.js')
20
+ })
@@ -2,13 +2,21 @@
2
2
  /**
3
3
  * Write dist/cdn/manifest.json after vendor + app builds.
4
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'
5
+ import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
6
+ import { resolve } from 'node:path'
9
7
  import { gzipSync } from 'node:zlib'
10
- import { readFileSync } from 'node:fs'
11
- import { resolveCdnBaseUrl, portalBuildId } from './resolve-cdn-base-url.mjs'
8
+
9
+ import {
10
+ cdnArtifactMatchesBuildId,
11
+ expectedCdnAppCssBasename,
12
+ expectedCdnScriptBasename,
13
+ } from './cdn-release-artifacts.mjs'
14
+ import { findProjectRoot } from './project-root.mjs'
15
+ import { clearPortalBuildIdCache, resolveCdnBaseUrl, portalBuildId } from './resolve-cdn-base-url.mjs'
16
+
17
+ if (process.env.MODULE_BUILD_ID?.trim() || process.env.PORTAL_BUILD_ID?.trim()) {
18
+ clearPortalBuildIdCache()
19
+ }
12
20
 
13
21
  const root = findProjectRoot()
14
22
  const cdnDir = resolve(root, 'dist/cdn')
@@ -37,10 +45,20 @@ function scriptEntry(file) {
37
45
  return { name: file, order: 99, preload: false }
38
46
  }
39
47
 
40
- const jsFiles = readdirSync(cdnDir, { recursive: true })
48
+ const allJsRelPaths = readdirSync(cdnDir, { recursive: true })
41
49
  .filter((f) => String(f).endsWith('.js'))
42
- .map((rel) => {
43
- const file = String(rel)
50
+ .map(String)
51
+
52
+ const staleJs = allJsRelPaths.filter((file) => !cdnArtifactMatchesBuildId(buildId, file))
53
+ if (staleJs.length > 0) {
54
+ console.warn(
55
+ `[cdn-manifest] ignoring ${staleJs.length} stale .js artifact(s) for buildId ${buildId}: ${staleJs.join(', ')}`,
56
+ )
57
+ }
58
+
59
+ const jsFiles = allJsRelPaths
60
+ .filter((file) => cdnArtifactMatchesBuildId(buildId, file))
61
+ .map((file) => {
44
62
  const metrics = fileMetrics(file)
45
63
  const entry = scriptEntry(file)
46
64
  return {
@@ -50,8 +68,16 @@ const jsFiles = readdirSync(cdnDir, { recursive: true })
50
68
  })
51
69
  .sort((a, b) => a.order - b.order || a.file.localeCompare(b.file))
52
70
 
53
- const cssFiles = readdirSync(cdnDir)
54
- .filter((f) => f.endsWith('.css'))
71
+ const allCssFiles = readdirSync(cdnDir).filter((f) => f.endsWith('.css'))
72
+ const staleCss = allCssFiles.filter((file) => !cdnArtifactMatchesBuildId(buildId, file))
73
+ if (staleCss.length > 0) {
74
+ console.warn(
75
+ `[cdn-manifest] ignoring ${staleCss.length} stale .css artifact(s) for buildId ${buildId}: ${staleCss.join(', ')}`,
76
+ )
77
+ }
78
+
79
+ const cssFiles = allCssFiles
80
+ .filter((file) => cdnArtifactMatchesBuildId(buildId, file))
55
81
  .map((file) => {
56
82
  const metrics = fileMetrics(file)
57
83
  return {
@@ -61,15 +87,22 @@ const cssFiles = readdirSync(cdnDir)
61
87
  }
62
88
  })
63
89
 
64
- const appCss = cssFiles.find((f) => f.name === 'app') ?? cssFiles[0]
90
+ const vendorExpected = expectedCdnScriptBasename('vendor', buildId)
91
+ const appExpected = expectedCdnScriptBasename('app', buildId)
92
+ const appCssExpected = expectedCdnAppCssBasename(buildId)
65
93
 
66
- if (!jsFiles.some((f) => f.name === 'vendor') || !jsFiles.some((f) => f.name === 'app')) {
67
- console.error('[cdn-manifest] expected vendor.*.js and app.*.js in dist/cdn/')
94
+ if (!jsFiles.some((f) => f.file === vendorExpected) || !jsFiles.some((f) => f.file === appExpected)) {
95
+ console.error(
96
+ `[cdn-manifest] expected ${vendorExpected} and ${appExpected} in dist/cdn/ (buildId=${buildId})`,
97
+ )
98
+ console.error(`[cdn-manifest] found js: ${jsFiles.map((f) => f.file).join(', ') || '(none)'}`)
68
99
  process.exit(1)
69
100
  }
70
101
 
102
+ const appCss = cssFiles.find((f) => f.file === appCssExpected) ?? cssFiles.find((f) => f.name === 'app')
103
+
71
104
  if (!appCss) {
72
- console.error('[cdn-manifest] expected app.*.css in dist/cdn/ — component styles load from CDN')
105
+ console.error(`[cdn-manifest] expected ${appCssExpected} in dist/cdn/ — component styles load from CDN`)
73
106
  process.exit(1)
74
107
  }
75
108
 
@@ -13,6 +13,7 @@ import {
13
13
  writeFileSync,
14
14
  } from 'node:fs'
15
15
  import { resolve } from 'node:path'
16
+ import { resolveCdnPrimaryScripts } from './cdn-release-artifacts.mjs'
16
17
 
17
18
  /** Substrings that break when HubSpot tokenizes module.js (Jinja/HubL). */
18
19
  export const FORBIDDEN_HUBL_IN_MODULE_JS = ['{{', '{%', '<%', '%>']
@@ -70,24 +71,14 @@ export function injectCdnManifestIntoModuleHtml(html, manifestPath) {
70
71
  }
71
72
 
72
73
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
73
- const vendor = manifest.scripts?.find((s) => s.name === 'vendor')
74
- const app = manifest.scripts?.find((s) => s.name === 'app')
75
- const appCss = manifest.styles?.find((s) => s.name === 'app') ?? manifest.styles?.[0]
76
-
77
- if (!vendor?.file || !app?.file) {
78
- throw new Error('manifest.json must include vendor and app scripts')
79
- }
80
-
81
- if (!appCss?.file) {
82
- throw new Error('manifest.json must include app styles (app.*.css from yarn build:cdn:dev or build:cdn:prod)')
83
- }
74
+ const { buildId, vendorFile, appFile, appCssFile } = resolveCdnPrimaryScripts(manifest)
84
75
 
85
76
  return html
86
77
  .replaceAll('__PORTAL_CDN_BASE__', manifest.baseUrl)
87
- .replaceAll('__PORTAL_BUILD_ID__', manifest.buildId)
88
- .replaceAll('__PORTAL_VENDOR_FILE__', vendor.file)
89
- .replaceAll('__PORTAL_APP_FILE__', app.file)
90
- .replaceAll('__PORTAL_APP_CSS_FILE__', appCss.file)
78
+ .replaceAll('__PORTAL_BUILD_ID__', buildId)
79
+ .replaceAll('__PORTAL_VENDOR_FILE__', vendorFile)
80
+ .replaceAll('__PORTAL_APP_FILE__', appFile)
81
+ .replaceAll('__PORTAL_APP_CSS_FILE__', appCssFile)
91
82
  }
92
83
 
93
84
  /**
@@ -100,12 +91,9 @@ export function mergeCdnAppStylesIntoModuleCss(moduleDir, manifestPath) {
100
91
  }
101
92
 
102
93
  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
- }
94
+ const { appCssFile } = resolveCdnPrimaryScripts(manifest)
107
95
 
108
- const cdnCssPath = resolve(moduleDir, '../cdn', appCss.file)
96
+ const cdnCssPath = resolve(moduleDir, '../cdn', appCssFile)
109
97
  if (!existsSync(cdnCssPath)) {
110
98
  throw new Error(
111
99
  `Missing ${cdnCssPath} — run build:cdn before hubspot post-build (needed for module.css)`,
@@ -20,6 +20,16 @@ import {
20
20
  import { wphsPaths } from './wphs-paths.mjs'
21
21
 
22
22
  const roots = []
23
+ /** CI sets GITHUB_REF_NAME; local bump tests must not inherit it (legacy override is tested separately). */
24
+ function localBuildIdEnv(overrides = {}) {
25
+ const env = { ...process.env }
26
+ delete env.GITHUB_REF_NAME
27
+ delete env.GITHUB_REF
28
+ delete env.PORTAL_BUILD_ID
29
+ delete env.MODULE_BUILD_ID
30
+ return { ...env, ...overrides }
31
+ }
32
+
23
33
 
24
34
  function makeProject() {
25
35
  const root = mkdtempSync(join(tmpdir(), 'wp-build-id-'))
@@ -100,11 +110,11 @@ describe('bumpModuleBuildId', () => {
100
110
  const root = makeProject()
101
111
  writeFileSync(join(root, '.wphs', 'config', 'developer.json'), JSON.stringify({ slug: 'manab' }))
102
112
 
103
- const first = bumpModuleBuildId({ projectRoot: root, tier: 'dev' })
113
+ const first = bumpModuleBuildId({ projectRoot: root, tier: 'dev', env: localBuildIdEnv() })
104
114
  assert.equal(first.id, 'v0.0.1-dev.manab.1')
105
115
  assert.equal(first.bumped, true)
106
116
 
107
- const second = bumpModuleBuildId({ projectRoot: root, tier: 'dev' })
117
+ const second = bumpModuleBuildId({ projectRoot: root, tier: 'dev', env: localBuildIdEnv() })
108
118
  assert.equal(second.id, 'v0.0.1-dev.manab.2')
109
119
 
110
120
  const statePath = join(wphsPaths(root).stateDir, 'module-build-id.json')
@@ -112,6 +122,19 @@ describe('bumpModuleBuildId', () => {
112
122
  assert.equal(state.seq, 2)
113
123
  })
114
124
 
125
+ it('uses GITHUB_REF_NAME when set (legacy resolvePortalBuildId CI pin)', () => {
126
+ const root = makeProject()
127
+ writeFileSync(join(root, '.wphs', 'config', 'developer.json'), JSON.stringify({ slug: 'manab' }))
128
+
129
+ const result = bumpModuleBuildId({
130
+ projectRoot: root,
131
+ env: localBuildIdEnv({ GITHUB_REF_NAME: '5.0.2-beta.802' }),
132
+ })
133
+ assert.equal(result.id, '5.0.2-beta.802')
134
+ assert.equal(result.bumped, false)
135
+ assert.equal(result.source, 'github-ref')
136
+ })
137
+
115
138
  it('respects env override without bumping', () => {
116
139
  const root = makeProject()
117
140
  const result = bumpModuleBuildId({
@@ -2,9 +2,9 @@
2
2
  * Resolve CDN base URL from env (jsDelivr GitHub, custom, or local).
3
3
  */
4
4
  import { loadPortalCdnConfig } from './load-portal-cdn-config.mjs'
5
- import { portalBuildId, resolvePortalBuildId } from './resolve-portal-build-id.mjs'
5
+ import { clearPortalBuildIdCache, portalBuildId, resolvePortalBuildId } from './resolve-portal-build-id.mjs'
6
6
 
7
- export { portalBuildId, resolvePortalBuildId }
7
+ export { clearPortalBuildIdCache, portalBuildId, resolvePortalBuildId }
8
8
 
9
9
  export function resolveCdnBaseUrl(env = process.env) {
10
10
  const config = loadPortalCdnConfig(env)