@woodsportal/hubspot-kit 5.0.3 → 5.0.4-beta.0
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 +2 -2
- package/scripts/build-cdn.mjs +30 -0
- package/scripts/cdn-release-artifacts.mjs +76 -0
- package/scripts/cdn-release-artifacts.test.mjs +20 -0
- package/scripts/generate-cdn-manifest.mjs +54 -15
- package/scripts/hubspot-module-postbuild.mjs +72 -25
- package/scripts/hubspot-module-postbuild.test.mjs +86 -0
- package/scripts/portal-build-id.test.mjs +25 -2
- package/scripts/resolve-cdn-base-url.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woodsportal/hubspot-kit",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4-beta.0",
|
|
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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"build": "tsup",
|
|
53
53
|
"dev": "tsup --watch",
|
|
54
54
|
"typecheck": "tsc --noEmit",
|
|
55
|
-
"test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/cli/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' 'scripts/portal-build-id.test.mjs' 'scripts/resolve-consumer-dep.test.mjs' 'scripts/wp-cli-smoke.test.mjs' 'scripts/vite/ensure-dev-kit-shims.test.mjs' 'scripts/vite/consumer-react-aliases.test.mjs' 'scripts/vite/resolve-from-project-root.test.mjs' 'scripts/tailwind/prefix-plugin.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 src/dev-module-config/__tests__/drill-down-panels.test.ts",
|
|
55
|
+
"test": "node --import tsx --test 'src/lib/**/*.test.ts' 'src/cli/**/*.test.ts' 'src/schema/**/*.test.ts' 'src/vite/**/*.test.ts' 'scripts/portal-build-id.test.mjs' 'scripts/cdn-release-artifacts.test.mjs' 'scripts/hubspot-module-postbuild.test.mjs' 'scripts/resolve-consumer-dep.test.mjs' 'scripts/wp-cli-smoke.test.mjs' 'scripts/vite/ensure-dev-kit-shims.test.mjs' 'scripts/vite/consumer-react-aliases.test.mjs' 'scripts/vite/resolve-from-project-root.test.mjs' 'scripts/tailwind/prefix-plugin.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 src/dev-module-config/__tests__/drill-down-panels.test.ts",
|
|
56
56
|
"test:examples": "node examples/smoke/run-matrix.mjs --tier static",
|
|
57
57
|
"test:dist-pack": "node --test scripts/dist-cli-pack-smoke.test.mjs",
|
|
58
58
|
"lint": "eslint src",
|
package/scripts/build-cdn.mjs
CHANGED
|
@@ -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,
|
|
6
|
-
import { resolve
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { findProjectRoot } from './project-root.mjs'
|
|
5
|
+
import { readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
9
7
|
import { gzipSync } from 'node:zlib'
|
|
10
|
-
|
|
11
|
-
import {
|
|
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,23 @@ function scriptEntry(file) {
|
|
|
37
45
|
return { name: file, order: 99, preload: false }
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
const
|
|
48
|
+
const allJsRelPaths = readdirSync(cdnDir, { recursive: true })
|
|
41
49
|
.filter((f) => String(f).endsWith('.js'))
|
|
42
|
-
.map(
|
|
43
|
-
|
|
50
|
+
.map(String)
|
|
51
|
+
|
|
52
|
+
const staleJs = allJsRelPaths.filter((file) => !cdnArtifactMatchesBuildId(buildId, file))
|
|
53
|
+
if (staleJs.length > 0) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[cdn-manifest] removing ${staleJs.length} stale .js artifact(s) for buildId ${buildId}: ${staleJs.join(', ')}`,
|
|
56
|
+
)
|
|
57
|
+
for (const file of staleJs) {
|
|
58
|
+
rmSync(resolve(cdnDir, file), { force: true })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const jsFiles = allJsRelPaths
|
|
63
|
+
.filter((file) => cdnArtifactMatchesBuildId(buildId, file))
|
|
64
|
+
.map((file) => {
|
|
44
65
|
const metrics = fileMetrics(file)
|
|
45
66
|
const entry = scriptEntry(file)
|
|
46
67
|
return {
|
|
@@ -50,8 +71,19 @@ const jsFiles = readdirSync(cdnDir, { recursive: true })
|
|
|
50
71
|
})
|
|
51
72
|
.sort((a, b) => a.order - b.order || a.file.localeCompare(b.file))
|
|
52
73
|
|
|
53
|
-
const
|
|
54
|
-
|
|
74
|
+
const allCssFiles = readdirSync(cdnDir).filter((f) => f.endsWith('.css'))
|
|
75
|
+
const staleCss = allCssFiles.filter((file) => !cdnArtifactMatchesBuildId(buildId, file))
|
|
76
|
+
if (staleCss.length > 0) {
|
|
77
|
+
console.warn(
|
|
78
|
+
`[cdn-manifest] removing ${staleCss.length} stale .css artifact(s) for buildId ${buildId}: ${staleCss.join(', ')}`,
|
|
79
|
+
)
|
|
80
|
+
for (const file of staleCss) {
|
|
81
|
+
rmSync(resolve(cdnDir, file), { force: true })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cssFiles = allCssFiles
|
|
86
|
+
.filter((file) => cdnArtifactMatchesBuildId(buildId, file))
|
|
55
87
|
.map((file) => {
|
|
56
88
|
const metrics = fileMetrics(file)
|
|
57
89
|
return {
|
|
@@ -61,15 +93,22 @@ const cssFiles = readdirSync(cdnDir)
|
|
|
61
93
|
}
|
|
62
94
|
})
|
|
63
95
|
|
|
64
|
-
const
|
|
96
|
+
const vendorExpected = expectedCdnScriptBasename('vendor', buildId)
|
|
97
|
+
const appExpected = expectedCdnScriptBasename('app', buildId)
|
|
98
|
+
const appCssExpected = expectedCdnAppCssBasename(buildId)
|
|
65
99
|
|
|
66
|
-
if (!jsFiles.some((f) => f.
|
|
67
|
-
console.error(
|
|
100
|
+
if (!jsFiles.some((f) => f.file === vendorExpected) || !jsFiles.some((f) => f.file === appExpected)) {
|
|
101
|
+
console.error(
|
|
102
|
+
`[cdn-manifest] expected ${vendorExpected} and ${appExpected} in dist/cdn/ (buildId=${buildId})`,
|
|
103
|
+
)
|
|
104
|
+
console.error(`[cdn-manifest] found js: ${jsFiles.map((f) => f.file).join(', ') || '(none)'}`)
|
|
68
105
|
process.exit(1)
|
|
69
106
|
}
|
|
70
107
|
|
|
108
|
+
const appCss = cssFiles.find((f) => f.file === appCssExpected) ?? cssFiles.find((f) => f.name === 'app')
|
|
109
|
+
|
|
71
110
|
if (!appCss) {
|
|
72
|
-
console.error(
|
|
111
|
+
console.error(`[cdn-manifest] expected ${appCssExpected} in dist/cdn/ — component styles load from CDN`)
|
|
73
112
|
process.exit(1)
|
|
74
113
|
}
|
|
75
114
|
|
|
@@ -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 = ['{{', '{%', '<%', '%>']
|
|
@@ -54,14 +55,70 @@ export function assertValidJavaScriptSyntax(jsPath) {
|
|
|
54
55
|
execSync(`node --check ${JSON.stringify(jsPath)}`, { stdio: 'pipe' })
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* CodeBuild sets MODULE_BUILD_ID; never let a leftover manifest.buildId win.
|
|
60
|
+
* @param {{ buildId?: string }} manifest
|
|
61
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
62
|
+
*/
|
|
63
|
+
export function resolveInjectBuildId(manifest, env = process.env) {
|
|
64
|
+
const envId = env.MODULE_BUILD_ID?.trim() || env.PORTAL_BUILD_ID?.trim()
|
|
65
|
+
const manifestId = manifest.buildId?.trim()
|
|
66
|
+
const buildId = envId || manifestId
|
|
67
|
+
if (!buildId) {
|
|
68
|
+
throw new Error('module.html inject needs manifest.buildId or MODULE_BUILD_ID')
|
|
69
|
+
}
|
|
70
|
+
return buildId
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pin HubL/JS CDN fields even when a stale worker already baked an older build id.
|
|
75
|
+
* @param {string} html
|
|
76
|
+
* @param {{ baseUrl: string, buildId: string, vendorFile: string, appFile: string, appCssFile: string }} pins
|
|
77
|
+
*/
|
|
78
|
+
export function pinModuleHtmlCdnUrls(html, pins) {
|
|
79
|
+
const { baseUrl, buildId, vendorFile, appFile, appCssFile } = pins
|
|
80
|
+
let out = html
|
|
81
|
+
.replaceAll('__PORTAL_CDN_BASE__', baseUrl)
|
|
82
|
+
.replaceAll('__PORTAL_BUILD_ID__', buildId)
|
|
83
|
+
.replaceAll('__PORTAL_VENDOR_FILE__', vendorFile)
|
|
84
|
+
.replaceAll('__PORTAL_APP_FILE__', appFile)
|
|
85
|
+
.replaceAll('__PORTAL_APP_CSS_FILE__', appCssFile)
|
|
86
|
+
|
|
87
|
+
out = out.replace(/assetBaseUrl:\s*"[^"]*"/g, `assetBaseUrl: "${baseUrl}"`)
|
|
88
|
+
out = out.replace(/buildId:\s*"[^"]*"/g, `buildId: "${buildId}"`)
|
|
89
|
+
out = out.replace(
|
|
90
|
+
/\{% set portal_cdn_base = "[^"]*" %\}/g,
|
|
91
|
+
`{% set portal_cdn_base = "${baseUrl}" %}`,
|
|
92
|
+
)
|
|
93
|
+
out = out.replace(
|
|
94
|
+
/\{% set portal_vendor_script = "[^"]*" %\}/g,
|
|
95
|
+
`{% set portal_vendor_script = "${vendorFile}" %}`,
|
|
96
|
+
)
|
|
97
|
+
out = out.replace(
|
|
98
|
+
/\{% set portal_app_script = "[^"]*" %\}/g,
|
|
99
|
+
`{% set portal_app_script = "${appFile}" %}`,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (!out.includes(buildId) || !out.includes(vendorFile) || !out.includes(appFile)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`module.html inject failed: expected ${vendorFile} and ${appFile} for buildId ${buildId}`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
if (out.includes('__PORTAL_')) {
|
|
108
|
+
throw new Error('module.html still has CDN placeholders after inject')
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
57
113
|
/**
|
|
58
114
|
* Replace CDN placeholders in module.html from dist/cdn/manifest.json.
|
|
59
115
|
* @param {string} html
|
|
60
116
|
* @param {string} manifestPath
|
|
117
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
61
118
|
*/
|
|
62
|
-
export function injectCdnManifestIntoModuleHtml(html, manifestPath) {
|
|
119
|
+
export function injectCdnManifestIntoModuleHtml(html, manifestPath, env = process.env) {
|
|
63
120
|
if (!existsSync(manifestPath)) {
|
|
64
|
-
if (html.includes('__PORTAL_CDN_BASE__')) {
|
|
121
|
+
if (html.includes('__PORTAL_CDN_BASE__') || /vendor\.\d/.test(html) || html.includes('portal_vendor_script')) {
|
|
65
122
|
throw new Error(
|
|
66
123
|
`Missing ${manifestPath} — run yarn build:cdn:dev or yarn build:cdn:prod before hubspot post-build`,
|
|
67
124
|
)
|
|
@@ -70,24 +127,16 @@ export function injectCdnManifestIntoModuleHtml(html, manifestPath) {
|
|
|
70
127
|
}
|
|
71
128
|
|
|
72
129
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return html
|
|
86
|
-
.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)
|
|
130
|
+
const buildId = resolveInjectBuildId(manifest, env)
|
|
131
|
+
const { vendorFile, appFile, appCssFile } = resolveCdnPrimaryScripts({ ...manifest, buildId })
|
|
132
|
+
|
|
133
|
+
return pinModuleHtmlCdnUrls(html, {
|
|
134
|
+
baseUrl: manifest.baseUrl,
|
|
135
|
+
buildId,
|
|
136
|
+
vendorFile,
|
|
137
|
+
appFile,
|
|
138
|
+
appCssFile,
|
|
139
|
+
})
|
|
91
140
|
}
|
|
92
141
|
|
|
93
142
|
/**
|
|
@@ -100,12 +149,10 @@ export function mergeCdnAppStylesIntoModuleCss(moduleDir, manifestPath) {
|
|
|
100
149
|
}
|
|
101
150
|
|
|
102
151
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
throw new Error('manifest.json must include app styles for module.css merge')
|
|
106
|
-
}
|
|
152
|
+
const buildId = resolveInjectBuildId(manifest)
|
|
153
|
+
const { appCssFile } = resolveCdnPrimaryScripts({ ...manifest, buildId })
|
|
107
154
|
|
|
108
|
-
const cdnCssPath = resolve(moduleDir, '../cdn',
|
|
155
|
+
const cdnCssPath = resolve(moduleDir, '../cdn', appCssFile)
|
|
109
156
|
if (!existsSync(cdnCssPath)) {
|
|
110
157
|
throw new Error(
|
|
111
158
|
`Missing ${cdnCssPath} — run build:cdn before hubspot post-build (needed for module.css)`,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
injectCdnManifestIntoModuleHtml,
|
|
9
|
+
pinModuleHtmlCdnUrls,
|
|
10
|
+
resolveInjectBuildId,
|
|
11
|
+
} from './hubspot-module-postbuild.mjs'
|
|
12
|
+
|
|
13
|
+
const placeholders = ` assetBaseUrl: "__PORTAL_CDN_BASE__",
|
|
14
|
+
buildId: "__PORTAL_BUILD_ID__",
|
|
15
|
+
{% set portal_cdn_base = "__PORTAL_CDN_BASE__" %}
|
|
16
|
+
{% set portal_vendor_script = "__PORTAL_VENDOR_FILE__" %}
|
|
17
|
+
{% set portal_app_script = "__PORTAL_APP_FILE__" %}
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
const baked800 = ` assetBaseUrl: "https://cdn.example/cdn",
|
|
21
|
+
buildId: "5.0.2-beta.800",
|
|
22
|
+
{% set portal_cdn_base = "https://cdn.example/cdn" %}
|
|
23
|
+
{% set portal_vendor_script = "vendor.5.0.2-beta.800.js" %}
|
|
24
|
+
{% set portal_app_script = "app.5.0.2-beta.800.js" %}
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
describe('resolveInjectBuildId', () => {
|
|
28
|
+
it('prefers MODULE_BUILD_ID over a stale manifest.buildId', () => {
|
|
29
|
+
assert.equal(
|
|
30
|
+
resolveInjectBuildId({ buildId: '5.0.2-beta.800' }, { MODULE_BUILD_ID: '5.0.2-beta.804' }),
|
|
31
|
+
'5.0.2-beta.804',
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('pinModuleHtmlCdnUrls', () => {
|
|
37
|
+
it('rewrites leftover baked 800 pins to the current build id', () => {
|
|
38
|
+
const out = pinModuleHtmlCdnUrls(baked800, {
|
|
39
|
+
baseUrl: 'https://cdn.example/cdn',
|
|
40
|
+
buildId: '5.0.2-beta.804',
|
|
41
|
+
vendorFile: 'vendor.5.0.2-beta.804.js',
|
|
42
|
+
appFile: 'app.5.0.2-beta.804.js',
|
|
43
|
+
appCssFile: 'app.5.0.2-beta.804.css',
|
|
44
|
+
})
|
|
45
|
+
assert.match(out, /buildId: "5\.0\.2-beta\.804"/)
|
|
46
|
+
assert.match(out, /vendor\.5\.0\.2-beta\.804\.js/)
|
|
47
|
+
assert.match(out, /app\.5\.0\.2-beta\.804\.js/)
|
|
48
|
+
assert.doesNotMatch(out, /5\.0\.2-beta\.800/)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('injectCdnManifestIntoModuleHtml', () => {
|
|
53
|
+
it('pins vendor/app from MODULE_BUILD_ID even when the manifest still lists 800 first', () => {
|
|
54
|
+
const dir = mkdtempSync(join(tmpdir(), 'wp-mod-html-'))
|
|
55
|
+
const manifestPath = join(dir, 'manifest.json')
|
|
56
|
+
writeFileSync(
|
|
57
|
+
manifestPath,
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
baseUrl: 'https://cdn.example/cdn',
|
|
60
|
+
buildId: '5.0.2-beta.800',
|
|
61
|
+
scripts: [
|
|
62
|
+
{ name: 'vendor', file: 'vendor.5.0.2-beta.800.js' },
|
|
63
|
+
{ name: 'vendor', file: 'vendor.5.0.2-beta.804.js' },
|
|
64
|
+
{ name: 'app', file: 'app.5.0.2-beta.800.js' },
|
|
65
|
+
{ name: 'app', file: 'app.5.0.2-beta.804.js' },
|
|
66
|
+
],
|
|
67
|
+
styles: [
|
|
68
|
+
{ name: 'app', file: 'app.5.0.2-beta.800.css' },
|
|
69
|
+
{ name: 'app', file: 'app.5.0.2-beta.804.css' },
|
|
70
|
+
],
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const fromPlaceholders = injectCdnManifestIntoModuleHtml(placeholders, manifestPath, {
|
|
75
|
+
MODULE_BUILD_ID: '5.0.2-beta.804',
|
|
76
|
+
})
|
|
77
|
+
assert.match(fromPlaceholders, /vendor\.5\.0\.2-beta\.804\.js/)
|
|
78
|
+
assert.doesNotMatch(fromPlaceholders, /5\.0\.2-beta\.800/)
|
|
79
|
+
|
|
80
|
+
const fromBaked = injectCdnManifestIntoModuleHtml(baked800, manifestPath, {
|
|
81
|
+
MODULE_BUILD_ID: '5.0.2-beta.804',
|
|
82
|
+
})
|
|
83
|
+
assert.match(fromBaked, /vendor\.5\.0\.2-beta\.804\.js/)
|
|
84
|
+
assert.doesNotMatch(fromBaked, /5\.0\.2-beta\.800/)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -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)
|