@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.
- package/CHANGELOG.md +45 -0
- package/dist/cli.js +798 -281
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/examples/fixtures/adopted-module-hybrid/woodscli.json +1 -1
- package/examples/fixtures/adopted-module-monolith/woodscli.json +1 -1
- package/examples/greenfield/module-hybrid-cdn/public/fields.json +26 -20
- package/examples/greenfield/module-hybrid-cdn/public/meta.json +9 -0
- package/examples/greenfield/module-hybrid-cdn/public/module.html +3 -4
- package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.prod.css +6 -0
- package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +5 -0
- package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +6 -0
- package/examples/greenfield/module-hybrid-cdn/src/portal/ensure-portal-cdn-stylesheet.ts +58 -0
- package/examples/greenfield/module-hybrid-cdn/woodscli.json +1 -1
- package/examples/greenfield/module-monolith/woodscli.json +1 -1
- package/package.json +2 -2
- package/scripts/build-cdn.mjs +6 -1
- package/scripts/bump-module-build-id.mjs +28 -0
- package/scripts/hubspot-module-postbuild.mjs +35 -1
- package/scripts/load-portal-cdn-config.mjs +21 -3
- package/scripts/portal-build-id.mjs +346 -0
- package/scripts/portal-build-id.test.mjs +132 -0
- package/scripts/publish-cdn-github.mjs +60 -15
- package/scripts/resolve-cdn-base-url.mjs +2 -1
- package/scripts/resolve-module-build-id-meta.mjs +7 -0
- package/scripts/resolve-portal-build-id.mjs +16 -56
- package/scripts/tailwind/prefix-plugin.js +46 -8
- package/scripts/tailwind/tailwind-content-plugin.js +0 -1
- package/scripts/vite/portal-build-shared.js +3 -1
- package/src/dev-module-config/__tests__/flatten-visible-fields.test.ts +107 -0
- package/src/dev-module-config/runtime/module-config-bridge.ts +10 -2
- package/src/dev-module-config/schema/types.ts +10 -0
- package/src/dev-module-config/schema/visibility.ts +42 -4
- package/src/dev-module-config/ui/ModuleConfigShell.tsx +10 -9
- package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +3 -3
- package/templates/module-hybrid-cdn/public/fields.json +52 -41
- package/templates/module-hybrid-cdn/public/meta.json +9 -0
- package/templates/module-hybrid-cdn/public/module.html +18 -4
- package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +8 -0
- package/templates/module-hybrid-cdn/src/assets/css/main.prod.css +12 -0
- package/templates/module-hybrid-cdn/src/assets/css/tailwind.css.config.css +7 -0
- package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +14 -3
- package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +9 -11
- package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +23 -5
- package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +1 -0
- package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +6 -0
- package/templates/module-hybrid-cdn/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
- package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +35 -5
- package/templates/module-hybrid-cdn/woodscli.json +1 -1
- package/templates/module-monolith/public/fields.json +23 -16
- package/templates/module-monolith/public/meta.json +9 -0
- package/templates/module-monolith/src/features/starter/components/starter-home.tsx +9 -11
- package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +23 -5
- package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +1 -0
- package/templates/module-monolith/woodscli.json +1 -1
- package/vite/vite.config.dev.mjs +16 -10
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module build ID: per-developer CDN pins, auto-increment, and layered resolution.
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { resolveProjectConfigPath } from './config-files.mjs'
|
|
9
|
+
import { findProjectRoot } from './project-root.mjs'
|
|
10
|
+
import { wphsPaths } from './wphs-paths.mjs'
|
|
11
|
+
|
|
12
|
+
export const LEGACY_BUILD_ID_FILE = '.portal-build-id'
|
|
13
|
+
export const DEFAULT_BUILD_ID_FILE = '.module-build-id'
|
|
14
|
+
export const MODULE_BUILD_ID_STATE_FILE = 'module-build-id.json'
|
|
15
|
+
export const DEVELOPER_CONFIG_FILE = 'developer.json'
|
|
16
|
+
|
|
17
|
+
const SLUG_MAX_LEN = 12
|
|
18
|
+
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])?$/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} input
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizeDevSlug(input) {
|
|
24
|
+
if (!input?.trim()) return undefined
|
|
25
|
+
const slug = input
|
|
26
|
+
.trim()
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '')
|
|
30
|
+
.slice(0, SLUG_MAX_LEN)
|
|
31
|
+
if (!slug || !SLUG_PATTERN.test(slug)) return undefined
|
|
32
|
+
return slug
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} baseline
|
|
37
|
+
* @param {string} slug
|
|
38
|
+
* @param {number} seq
|
|
39
|
+
*/
|
|
40
|
+
export function formatBuildId(baseline, slug, seq) {
|
|
41
|
+
const base = baseline?.trim()
|
|
42
|
+
const devSlug = sanitizeDevSlug(slug)
|
|
43
|
+
const n = Number(seq)
|
|
44
|
+
if (!base) {
|
|
45
|
+
throw new Error('module build id baseline is required')
|
|
46
|
+
}
|
|
47
|
+
if (!devSlug) {
|
|
48
|
+
throw new Error('module build id dev slug is required')
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
51
|
+
throw new Error(`module build id seq must be a positive integer (got ${seq})`)
|
|
52
|
+
}
|
|
53
|
+
const id = `${base}.${devSlug}.${n}`
|
|
54
|
+
if (!isGitTagSafe(id)) {
|
|
55
|
+
throw new Error(`module build id is not git-tag safe: ${id}`)
|
|
56
|
+
}
|
|
57
|
+
return id
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} value
|
|
62
|
+
*/
|
|
63
|
+
export function isGitTagSafe(value) {
|
|
64
|
+
if (!value?.trim()) return false
|
|
65
|
+
if (value.includes(' ') || value.includes('..')) return false
|
|
66
|
+
return /^[A-Za-z0-9._-]+$/.test(value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Strip legacy numeric or per-dev suffixes to recover channel baseline.
|
|
71
|
+
* @param {string} raw
|
|
72
|
+
*/
|
|
73
|
+
export function stripToBaseline(raw) {
|
|
74
|
+
const line = raw?.trim()
|
|
75
|
+
if (!line) return undefined
|
|
76
|
+
|
|
77
|
+
const perDev = line.match(/^(v\d+\.\d+\.\d+(?:-[a-z0-9]+)?)\.[a-z0-9-]{1,12}\.\d+$/i)
|
|
78
|
+
if (perDev) return perDev[1]
|
|
79
|
+
|
|
80
|
+
const numericSuffix = line.match(/^(v\d+\.\d+\.\d+(?:-[a-z0-9]+)?)\.\d+$/)
|
|
81
|
+
if (numericSuffix) return numericSuffix[1]
|
|
82
|
+
|
|
83
|
+
return line
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} projectRoot
|
|
88
|
+
*/
|
|
89
|
+
export function getBuildIdFilePath(projectRoot) {
|
|
90
|
+
const configPath = resolveProjectConfigPath(projectRoot)
|
|
91
|
+
if (configPath) {
|
|
92
|
+
try {
|
|
93
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
94
|
+
if (config.cdn?.buildIdFile) {
|
|
95
|
+
return resolve(projectRoot, config.cdn.buildIdFile)
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore invalid config
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return resolve(projectRoot, DEFAULT_BUILD_ID_FILE)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} projectRoot
|
|
106
|
+
*/
|
|
107
|
+
export function getLegacyBuildIdFilePath(projectRoot) {
|
|
108
|
+
return resolve(projectRoot, LEGACY_BUILD_ID_FILE)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} filePath
|
|
113
|
+
*/
|
|
114
|
+
function readFirstLine(filePath) {
|
|
115
|
+
if (!existsSync(filePath)) return undefined
|
|
116
|
+
const line = readFileSync(filePath, 'utf8')
|
|
117
|
+
.split('\n')
|
|
118
|
+
.map((l) => l.trim())
|
|
119
|
+
.find((l) => l.length > 0 && !l.startsWith('#'))
|
|
120
|
+
return line || undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} projectRoot
|
|
125
|
+
*/
|
|
126
|
+
export function readBaselineBuildId(projectRoot) {
|
|
127
|
+
const primary = getBuildIdFilePath(projectRoot)
|
|
128
|
+
const fromPrimary = readFirstLine(primary)
|
|
129
|
+
if (fromPrimary) return stripToBaseline(fromPrimary)
|
|
130
|
+
|
|
131
|
+
const legacy = getLegacyBuildIdFilePath(projectRoot)
|
|
132
|
+
const fromLegacy = readFirstLine(legacy)
|
|
133
|
+
if (fromLegacy) return stripToBaseline(fromLegacy)
|
|
134
|
+
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} projectRoot
|
|
140
|
+
*/
|
|
141
|
+
export function moduleBuildIdStatePath(projectRoot) {
|
|
142
|
+
return resolve(wphsPaths(projectRoot).stateDir, MODULE_BUILD_ID_STATE_FILE)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} projectRoot
|
|
147
|
+
*/
|
|
148
|
+
export function developerConfigPath(projectRoot) {
|
|
149
|
+
return resolve(wphsPaths(projectRoot).configDir, DEVELOPER_CONFIG_FILE)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @typedef {{ slug?: string, seq?: number, baseline?: string, tier?: string, updatedAt?: string }} ModuleBuildIdState
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} projectRoot
|
|
158
|
+
* @returns {ModuleBuildIdState | undefined}
|
|
159
|
+
*/
|
|
160
|
+
export function readModuleBuildIdState(projectRoot) {
|
|
161
|
+
const path = moduleBuildIdStatePath(projectRoot)
|
|
162
|
+
if (!existsSync(path)) return undefined
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} projectRoot
|
|
172
|
+
* @param {ModuleBuildIdState} state
|
|
173
|
+
* @param {boolean} [dryRun]
|
|
174
|
+
*/
|
|
175
|
+
export function writeModuleBuildIdState(projectRoot, state, dryRun = false) {
|
|
176
|
+
const path = moduleBuildIdStatePath(projectRoot)
|
|
177
|
+
if (!dryRun) {
|
|
178
|
+
mkdirSync(resolve(path, '..'), { recursive: true })
|
|
179
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`)
|
|
180
|
+
}
|
|
181
|
+
return path
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
186
|
+
*/
|
|
187
|
+
export function readEnvBuildIdOverride(env = process.env) {
|
|
188
|
+
const explicit = env.PORTAL_BUILD_ID?.trim() || env.MODULE_BUILD_ID?.trim()
|
|
189
|
+
if (explicit) {
|
|
190
|
+
return { id: explicit, source: 'env' }
|
|
191
|
+
}
|
|
192
|
+
const ref = env.GITHUB_REF_NAME?.trim()
|
|
193
|
+
if (ref) {
|
|
194
|
+
return { id: ref, source: 'github-ref' }
|
|
195
|
+
}
|
|
196
|
+
return undefined
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
201
|
+
*/
|
|
202
|
+
export function hasExplicitBuildIdOverride(env = process.env) {
|
|
203
|
+
return Boolean(readEnvBuildIdOverride(env))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
208
|
+
* @param {string} [projectRoot]
|
|
209
|
+
*/
|
|
210
|
+
export function resolveDevSlug(env = process.env, projectRoot = findProjectRoot()) {
|
|
211
|
+
const fromEnv = sanitizeDevSlug(env.PORTAL_CDN_DEV_SLUG) ?? sanitizeDevSlug(env.MODULE_CDN_DEV_SLUG)
|
|
212
|
+
if (fromEnv) return fromEnv
|
|
213
|
+
|
|
214
|
+
const devConfigPath = developerConfigPath(projectRoot)
|
|
215
|
+
if (existsSync(devConfigPath)) {
|
|
216
|
+
try {
|
|
217
|
+
const raw = JSON.parse(readFileSync(devConfigPath, 'utf8'))
|
|
218
|
+
const fromFile = sanitizeDevSlug(raw?.slug)
|
|
219
|
+
if (fromFile) return fromFile
|
|
220
|
+
} catch {
|
|
221
|
+
// ignore
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const gitName = spawnSync('git', ['config', 'user.name'], {
|
|
226
|
+
encoding: 'utf8',
|
|
227
|
+
cwd: projectRoot,
|
|
228
|
+
})
|
|
229
|
+
if (gitName.status === 0) {
|
|
230
|
+
const fromGit = sanitizeDevSlug(gitName.stdout)
|
|
231
|
+
if (fromGit) return fromGit
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @typedef {'env' | 'github-ref' | 'state' | 'baseline' | 'fallback'} BuildIdSource
|
|
239
|
+
*/
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
243
|
+
* @param {string} [projectRoot]
|
|
244
|
+
*/
|
|
245
|
+
export function resolveModuleBuildIdMeta(env = process.env, projectRoot = findProjectRoot()) {
|
|
246
|
+
const override = readEnvBuildIdOverride(env)
|
|
247
|
+
if (override) {
|
|
248
|
+
return { id: override.id, source: override.source, slug: undefined, seq: undefined, baseline: undefined }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const baseline = readBaselineBuildId(projectRoot)
|
|
252
|
+
const state = readModuleBuildIdState(projectRoot)
|
|
253
|
+
const slug = sanitizeDevSlug(state?.slug) ?? resolveDevSlug(env, projectRoot)
|
|
254
|
+
|
|
255
|
+
if (baseline && slug && state?.seq && Number.isInteger(state.seq) && state.seq >= 1) {
|
|
256
|
+
const stateBaseline = state.baseline ? stripToBaseline(state.baseline) : baseline
|
|
257
|
+
return {
|
|
258
|
+
id: formatBuildId(stateBaseline ?? baseline, slug, state.seq),
|
|
259
|
+
source: 'state',
|
|
260
|
+
slug,
|
|
261
|
+
seq: state.seq,
|
|
262
|
+
baseline: stateBaseline ?? baseline,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (baseline) {
|
|
267
|
+
return { id: baseline, source: 'baseline', slug, seq: undefined, baseline }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { id: 'dev', source: 'fallback', slug, seq: undefined, baseline: undefined }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
275
|
+
* @param {string} [projectRoot]
|
|
276
|
+
*/
|
|
277
|
+
export function resolveModuleBuildId(env = process.env, projectRoot = findProjectRoot()) {
|
|
278
|
+
return resolveModuleBuildIdMeta(env, projectRoot).id
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {{ projectRoot?: string, tier?: string, dryRun?: boolean, env?: NodeJS.ProcessEnv }} [options]
|
|
283
|
+
*/
|
|
284
|
+
export function bumpModuleBuildId(options = {}) {
|
|
285
|
+
const projectRoot = options.projectRoot ?? findProjectRoot()
|
|
286
|
+
const env = options.env ?? process.env
|
|
287
|
+
const dryRun = options.dryRun ?? false
|
|
288
|
+
|
|
289
|
+
if (hasExplicitBuildIdOverride(env)) {
|
|
290
|
+
const override = readEnvBuildIdOverride(env)
|
|
291
|
+
return { id: override.id, bumped: false, source: override.source }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const baseline = readBaselineBuildId(projectRoot)
|
|
295
|
+
if (!baseline) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`${DEFAULT_BUILD_ID_FILE} missing — run wp setup or add a channel baseline (e.g. v0.0.1-dev)`,
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const slug = resolveDevSlug(env, projectRoot)
|
|
302
|
+
if (!slug) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
'CDN dev slug not configured — set PORTAL_CDN_DEV_SLUG, run wp setup, or configure git user.name',
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const state = readModuleBuildIdState(projectRoot) ?? {}
|
|
309
|
+
const stateBaseline = state.baseline ? stripToBaseline(state.baseline) : baseline
|
|
310
|
+
const nextSeq =
|
|
311
|
+
state.slug === slug && stateBaseline === baseline && Number.isInteger(state.seq)
|
|
312
|
+
? state.seq + 1
|
|
313
|
+
: 1
|
|
314
|
+
|
|
315
|
+
const id = formatBuildId(baseline, slug, nextSeq)
|
|
316
|
+
const nextState = {
|
|
317
|
+
slug,
|
|
318
|
+
seq: nextSeq,
|
|
319
|
+
baseline,
|
|
320
|
+
tier: options.tier,
|
|
321
|
+
updatedAt: new Date().toISOString(),
|
|
322
|
+
}
|
|
323
|
+
writeModuleBuildIdState(projectRoot, nextState, dryRun)
|
|
324
|
+
|
|
325
|
+
return { id, bumped: true, source: 'state', slug, seq: nextSeq, baseline }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let cached
|
|
329
|
+
|
|
330
|
+
export function clearPortalBuildIdCache() {
|
|
331
|
+
cached = undefined
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Cached build id for the current Node process (file + env read once). */
|
|
335
|
+
export function portalBuildId() {
|
|
336
|
+
if (cached === undefined) {
|
|
337
|
+
cached = resolveModuleBuildId()
|
|
338
|
+
}
|
|
339
|
+
return cached
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Inject resolved id into env for child processes (vite, etc.). */
|
|
343
|
+
export function envWithPortalBuildId(baseEnv = process.env, projectRoot = findProjectRoot()) {
|
|
344
|
+
const buildId = resolveModuleBuildId(baseEnv, projectRoot)
|
|
345
|
+
return { ...baseEnv, PORTAL_BUILD_ID: buildId, MODULE_BUILD_ID: buildId }
|
|
346
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { afterEach, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
bumpModuleBuildId,
|
|
9
|
+
clearPortalBuildIdCache,
|
|
10
|
+
formatBuildId,
|
|
11
|
+
hasExplicitBuildIdOverride,
|
|
12
|
+
readBaselineBuildId,
|
|
13
|
+
resolveDevSlug,
|
|
14
|
+
resolveModuleBuildId,
|
|
15
|
+
resolveModuleBuildIdMeta,
|
|
16
|
+
sanitizeDevSlug,
|
|
17
|
+
stripToBaseline,
|
|
18
|
+
writeModuleBuildIdState,
|
|
19
|
+
} from './portal-build-id.mjs'
|
|
20
|
+
import { wphsPaths } from './wphs-paths.mjs'
|
|
21
|
+
|
|
22
|
+
const roots = []
|
|
23
|
+
|
|
24
|
+
function makeProject() {
|
|
25
|
+
const root = mkdtempSync(join(tmpdir(), 'wp-build-id-'))
|
|
26
|
+
roots.push(root)
|
|
27
|
+
mkdirSync(join(root, '.wphs', 'config'), { recursive: true })
|
|
28
|
+
mkdirSync(join(root, '.wphs', 'state'), { recursive: true })
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(root, 'woodscli.json'),
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
configVersion: 1,
|
|
33
|
+
projectType: 'module-hybrid-cdn',
|
|
34
|
+
module: { name: 'test', uploadPath: 'x.module', outDir: 'dist' },
|
|
35
|
+
cdn: { mode: 'jsdelivr', buildIdFile: '.module-build-id', mirror: 'portal.cdn.json' },
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
writeFileSync(join(root, '.module-build-id'), '# baseline\nv0.0.1-dev\n')
|
|
39
|
+
return root
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
clearPortalBuildIdCache()
|
|
44
|
+
for (const root of roots.splice(0)) {
|
|
45
|
+
rmSync(root, { recursive: true, force: true })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('sanitizeDevSlug', () => {
|
|
50
|
+
it('normalizes names', () => {
|
|
51
|
+
assert.equal(sanitizeDevSlug('Manab Roy'), 'manab-roy')
|
|
52
|
+
assert.equal(sanitizeDevSlug(' JDoe '), 'jdoe')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('stripToBaseline', () => {
|
|
57
|
+
it('strips numeric legacy suffix', () => {
|
|
58
|
+
assert.equal(stripToBaseline('v0.0.1-dev.10'), 'v0.0.1-dev')
|
|
59
|
+
})
|
|
60
|
+
it('strips per-dev suffix', () => {
|
|
61
|
+
assert.equal(stripToBaseline('v0.0.1-dev.manab.3'), 'v0.0.1-dev')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('formatBuildId', () => {
|
|
66
|
+
it('formats slugged ids', () => {
|
|
67
|
+
assert.equal(formatBuildId('v0.0.1-dev', 'manab', 1), 'v0.0.1-dev.manab.1')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('resolveModuleBuildId', () => {
|
|
72
|
+
it('prefers env override', () => {
|
|
73
|
+
const root = makeProject()
|
|
74
|
+
const id = resolveModuleBuildId({ PORTAL_BUILD_ID: 'ci-abc' }, root)
|
|
75
|
+
assert.equal(id, 'ci-abc')
|
|
76
|
+
assert.equal(hasExplicitBuildIdOverride({ PORTAL_BUILD_ID: 'ci-abc' }), true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('uses state when present', () => {
|
|
80
|
+
const root = makeProject()
|
|
81
|
+
writeFileSync(join(root, '.wphs', 'config', 'developer.json'), JSON.stringify({ slug: 'manab' }))
|
|
82
|
+
writeModuleBuildIdState(root, { slug: 'manab', seq: 4, baseline: 'v0.0.1-dev' })
|
|
83
|
+
const meta = resolveModuleBuildIdMeta({}, root)
|
|
84
|
+
assert.equal(meta.id, 'v0.0.1-dev.manab.4')
|
|
85
|
+
assert.equal(meta.source, 'state')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('falls back to baseline without state', () => {
|
|
89
|
+
const root = makeProject()
|
|
90
|
+
writeFileSync(join(root, '.wphs', 'config', 'developer.json'), JSON.stringify({ slug: 'manab' }))
|
|
91
|
+
assert.equal(readBaselineBuildId(root), 'v0.0.1-dev')
|
|
92
|
+
const meta = resolveModuleBuildIdMeta({}, root)
|
|
93
|
+
assert.equal(meta.id, 'v0.0.1-dev')
|
|
94
|
+
assert.equal(meta.source, 'baseline')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('bumpModuleBuildId', () => {
|
|
99
|
+
it('increments seq for same slug and baseline', () => {
|
|
100
|
+
const root = makeProject()
|
|
101
|
+
writeFileSync(join(root, '.wphs', 'config', 'developer.json'), JSON.stringify({ slug: 'manab' }))
|
|
102
|
+
|
|
103
|
+
const first = bumpModuleBuildId({ projectRoot: root, tier: 'dev' })
|
|
104
|
+
assert.equal(first.id, 'v0.0.1-dev.manab.1')
|
|
105
|
+
assert.equal(first.bumped, true)
|
|
106
|
+
|
|
107
|
+
const second = bumpModuleBuildId({ projectRoot: root, tier: 'dev' })
|
|
108
|
+
assert.equal(second.id, 'v0.0.1-dev.manab.2')
|
|
109
|
+
|
|
110
|
+
const statePath = join(wphsPaths(root).stateDir, 'module-build-id.json')
|
|
111
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'))
|
|
112
|
+
assert.equal(state.seq, 2)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('respects env override without bumping', () => {
|
|
116
|
+
const root = makeProject()
|
|
117
|
+
const result = bumpModuleBuildId({
|
|
118
|
+
projectRoot: root,
|
|
119
|
+
env: { PORTAL_BUILD_ID: 'release-1' },
|
|
120
|
+
})
|
|
121
|
+
assert.equal(result.id, 'release-1')
|
|
122
|
+
assert.equal(result.bumped, false)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('resolveDevSlug', () => {
|
|
127
|
+
it('reads slug from developer.json', () => {
|
|
128
|
+
const root = makeProject()
|
|
129
|
+
writeFileSync(join(root, '.wphs', 'config', 'developer.json'), JSON.stringify({ slug: 'alice' }))
|
|
130
|
+
assert.equal(resolveDevSlug({}, root), 'alice')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -32,13 +32,14 @@ import {
|
|
|
32
32
|
isQuietBuild,
|
|
33
33
|
portalSubprocessStdio,
|
|
34
34
|
} from './portal-subprocess-stdio.mjs'
|
|
35
|
-
import {
|
|
35
|
+
import { resolveModuleBuildIdMeta } from './portal-build-id.mjs'
|
|
36
36
|
|
|
37
37
|
const root = findProjectRoot()
|
|
38
38
|
|
|
39
39
|
const args = new Set(process.argv.slice(2))
|
|
40
40
|
const dryRun = args.has('--dry-run')
|
|
41
41
|
const skipJsDelivrCheck = args.has('--skip-jsdelivr-check')
|
|
42
|
+
const forceTag = args.has('--force-tag')
|
|
42
43
|
|
|
43
44
|
function log(msg) {
|
|
44
45
|
if (isQuietBuild()) {
|
|
@@ -106,7 +107,7 @@ function copyCdnArtifacts(sourceDir, targetDir) {
|
|
|
106
107
|
|
|
107
108
|
function writeCdnReadme(repoRoot, buildId, config) {
|
|
108
109
|
const readmePath = join(repoRoot, 'README.md')
|
|
109
|
-
const base = jsDelivrBaseUrl(config.github,
|
|
110
|
+
const base = jsDelivrBaseUrl(config.github, config.cdnPathInRepo, config.defaultBranch)
|
|
110
111
|
const body = `# woodsportal-client-frontend-cdn
|
|
111
112
|
|
|
112
113
|
Public CDN assets for the WoodsPortal HubSpot module (**Strategy B — Hybrid CDN**).
|
|
@@ -116,10 +117,10 @@ Public CDN assets for the WoodsPortal HubSpot module (**Strategy B — Hybrid CD
|
|
|
116
117
|
| | |
|
|
117
118
|
|---|---|
|
|
118
119
|
| Latest tag | \`${buildId}\` |
|
|
119
|
-
| jsDelivr base | ${base} |
|
|
120
|
+
| jsDelivr base | ${base} (@${config.defaultBranch}) |
|
|
120
121
|
| Source repo | [digitalwoods.io-woodsportal-client-frontend](https://github.com/Digital-Woods/digitalwoods.io-woodsportal-client-frontend) (private) |
|
|
121
122
|
|
|
122
|
-
## jsDelivr URLs (
|
|
123
|
+
## jsDelivr URLs (build \`${buildId}\` on \`${config.defaultBranch}\`)
|
|
123
124
|
|
|
124
125
|
- \`${base}/vendor.${buildId}.js\`
|
|
125
126
|
- \`${base}/app.${buildId}.js\`
|
|
@@ -130,8 +131,29 @@ Published at ${new Date().toISOString()}.
|
|
|
130
131
|
writeFileSync(readmePath, body, 'utf8')
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
async function
|
|
134
|
-
const
|
|
134
|
+
async function assertGithubRepoPublic(github) {
|
|
135
|
+
const res = await fetch(`https://api.github.com/repos/${github}`, {
|
|
136
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
137
|
+
})
|
|
138
|
+
if (res.status === 404) {
|
|
139
|
+
fail(
|
|
140
|
+
`GitHub repo ${github} not found or not visible — jsDelivr requires a public CDN mirror repo.`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
fail(`Could not verify GitHub repo ${github} (HTTP ${res.status}).`)
|
|
145
|
+
}
|
|
146
|
+
const data = await res.json()
|
|
147
|
+
if (data.private === true) {
|
|
148
|
+
fail(
|
|
149
|
+
`CDN mirror ${github} is private — jsDelivr only serves public GitHub repos. ` +
|
|
150
|
+
'Make the repo public (Settings → Danger zone → Change visibility) or use a public mirror.',
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function checkJsDelivr(config, vendorFile, appFile, appCssFile) {
|
|
156
|
+
const base = jsDelivrBaseUrl(config.github, config.cdnPathInRepo, config.defaultBranch)
|
|
135
157
|
for (const file of [appCssFile, vendorFile, appFile]) {
|
|
136
158
|
const url = `${base}/${file}`
|
|
137
159
|
log(`Checking jsDelivr (may take a minute on first publish): ${url}`)
|
|
@@ -149,7 +171,8 @@ async function checkJsDelivr(config, buildId, vendorFile, appFile, appCssFile) {
|
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
const config = loadPortalCdnConfig()
|
|
152
|
-
const
|
|
174
|
+
const buildMeta = resolveModuleBuildIdMeta()
|
|
175
|
+
const buildId = buildMeta.id
|
|
153
176
|
const sourceCdn = resolve(root, 'dist/cdn')
|
|
154
177
|
const token = process.env.PORTAL_CDN_PUBLISH_TOKEN ?? process.env.GITHUB_TOKEN
|
|
155
178
|
|
|
@@ -174,12 +197,12 @@ if (!appCssFile) {
|
|
|
174
197
|
|
|
175
198
|
if (!vendorFile.includes(buildId) || !appFile.includes(buildId) || !appCssFile.includes(buildId)) {
|
|
176
199
|
fail(
|
|
177
|
-
`Build id mismatch:
|
|
178
|
-
'
|
|
200
|
+
`Build id mismatch: module build id is "${buildId}" but chunks are ${vendorFile}, ${appFile}, ${appCssFile}. ` +
|
|
201
|
+
'Run `wp deploy` (auto-bumps id) or rebuild CDN after aligning `.module-build-id`.',
|
|
179
202
|
)
|
|
180
203
|
}
|
|
181
204
|
|
|
182
|
-
const jsdelivrBase = jsDelivrBaseUrl(config.github,
|
|
205
|
+
const jsdelivrBase = jsDelivrBaseUrl(config.github, config.cdnPathInRepo, config.defaultBranch)
|
|
183
206
|
|
|
184
207
|
const appSource = readFileSync(join(sourceCdn, appFile), 'utf8')
|
|
185
208
|
const bakedApis = [
|
|
@@ -196,12 +219,20 @@ if (bakedApis.length === 1 && bakedApis[0] === 'https://api.woodsportal.com') {
|
|
|
196
219
|
|
|
197
220
|
log('Configuration')
|
|
198
221
|
log(` config file: ${config.configPath ?? '(defaults)'}`)
|
|
199
|
-
log(` build id / tag: ${buildId}`)
|
|
222
|
+
log(` build id / tag: ${buildId} (source: ${buildMeta.source})`)
|
|
223
|
+
if (buildMeta.slug) {
|
|
224
|
+
log(` dev slug: ${buildMeta.slug}${buildMeta.seq ? ` · seq ${buildMeta.seq}` : ''}`)
|
|
225
|
+
}
|
|
226
|
+
if (config.usingLocalMirror) {
|
|
227
|
+
log(' CDN mirror: portal.cdn.local.json override')
|
|
228
|
+
}
|
|
200
229
|
log(` GitHub (jsDelivr): ${config.github}`)
|
|
201
230
|
log(` git remote: ${config.gitRemote}`)
|
|
202
231
|
log(` target path: ${config.cdnPathInRepo}/`)
|
|
203
232
|
log(` jsDelivr base: ${jsdelivrBase}`)
|
|
204
233
|
|
|
234
|
+
await assertGithubRepoPublic(config.github)
|
|
235
|
+
|
|
205
236
|
if (dryRun) {
|
|
206
237
|
log('Dry run — would copy:')
|
|
207
238
|
for (const f of readdirSync(sourceCdn)) {
|
|
@@ -257,6 +288,14 @@ if (!status) {
|
|
|
257
288
|
)
|
|
258
289
|
}
|
|
259
290
|
|
|
291
|
+
const remoteTag = runCapture('git', ['ls-remote', 'origin', `refs/tags/${buildId}`], { cwd: workDir })
|
|
292
|
+
if (remoteTag && !forceTag) {
|
|
293
|
+
fail(
|
|
294
|
+
`Tag ${buildId} already exists on remote CDN repo — run wp deploy to auto-bump the module build id, ` +
|
|
295
|
+
'or pass --force-tag only if you intend to overwrite that release.',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
260
299
|
const existingTag = spawnSync('git', ['rev-parse', buildId], {
|
|
261
300
|
cwd: workDir,
|
|
262
301
|
stdio: 'pipe',
|
|
@@ -264,13 +303,19 @@ const existingTag = spawnSync('git', ['rev-parse', buildId], {
|
|
|
264
303
|
})
|
|
265
304
|
|
|
266
305
|
if (existingTag.status === 0) {
|
|
267
|
-
|
|
306
|
+
if (!forceTag) {
|
|
307
|
+
fail(
|
|
308
|
+
`Tag ${buildId} already exists locally — run wp deploy to bump .module-build-id or pass --force-tag.`,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
log(`Tag ${buildId} already exists locally — deleting and recreating on current commit (--force-tag).`)
|
|
268
312
|
run('git', ['tag', '-d', buildId], { cwd: workDir })
|
|
269
313
|
}
|
|
270
314
|
|
|
271
315
|
run('git', ['tag', '-a', buildId, '-m', `WoodsPortal CDN ${buildId}`], { cwd: workDir })
|
|
272
316
|
run('git', ['push', '-u', 'origin', branch], { cwd: workDir })
|
|
273
|
-
|
|
317
|
+
const tagPushArgs = ['push', 'origin', `refs/tags/${buildId}`, ...(forceTag ? ['--force'] : [])]
|
|
318
|
+
run('git', tagPushArgs, { cwd: workDir })
|
|
274
319
|
|
|
275
320
|
log('')
|
|
276
321
|
log('Published successfully.')
|
|
@@ -280,12 +325,12 @@ log(` jsDelivr app: ${jsdelivrBase}/${appFile}`)
|
|
|
280
325
|
log(` jsDelivr css: ${jsdelivrBase}/${appCssFile}`)
|
|
281
326
|
log('')
|
|
282
327
|
log('Next steps:')
|
|
283
|
-
log(
|
|
328
|
+
log(` 1. jsDelivr serves from @${config.defaultBranch} (versioned chunk filenames include ${buildId}).`)
|
|
284
329
|
log(' 2. yarn build:hubspot:dev # dev API + jsDelivr URLs')
|
|
285
330
|
log(' yarn build:hubspot:prod # prod API')
|
|
286
331
|
log(' 3. yarn upload:hubspot')
|
|
287
332
|
log(' Or: yarn deploy:dev | yarn deploy:prod')
|
|
288
333
|
|
|
289
334
|
if (!skipJsDelivrCheck) {
|
|
290
|
-
await checkJsDelivr(config,
|
|
335
|
+
await checkJsDelivr(config, vendorFile, appFile, appCssFile)
|
|
291
336
|
}
|
|
@@ -28,5 +28,6 @@ export function resolveCdnBaseUrl(env = process.env) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const cdnPath = config.cdnPathInRepo.replace(/^\//, '').replace(/\/$/, '')
|
|
31
|
-
|
|
31
|
+
const branch = config.defaultBranch.replace(/^@/, '').trim() || 'main'
|
|
32
|
+
return `https://cdn.jsdelivr.net/gh/${config.github}@${branch}/${cdnPath}`
|
|
32
33
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { clearPortalBuildIdCache, resolveModuleBuildIdMeta } from './portal-build-id.mjs'
|
|
3
|
+
import { findProjectRoot } from './project-root.mjs'
|
|
4
|
+
|
|
5
|
+
clearPortalBuildIdCache()
|
|
6
|
+
const meta = resolveModuleBuildIdMeta(process.env, findProjectRoot())
|
|
7
|
+
process.stdout.write(`${JSON.stringify(meta)}\n`)
|