hubspot-cms-sync 0.5.5 → 0.5.7
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 +1 -1
- package/src/build-static.mjs +55 -4
package/package.json
CHANGED
package/src/build-static.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// _redirects Cloudflare redirects from sync/redirects.csv
|
|
14
14
|
// _headers cache + basic security headers
|
|
15
15
|
|
|
16
|
-
import { mkdir, writeFile, cp, readFile } from 'node:fs/promises';
|
|
16
|
+
import { mkdir, writeFile, cp, readFile, readdir } from 'node:fs/promises';
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { join, dirname } from 'node:path';
|
|
19
19
|
|
|
@@ -34,6 +34,50 @@ async function loadTagSlugs(siteDir) {
|
|
|
34
34
|
return map;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Build a `theme` context from the theme fields (fields.json) so HubL `{{ theme.* }}`
|
|
38
|
+
// expressions in CSS resolve to the field defaults. HubSpot resolves these server-side;
|
|
39
|
+
// the static target must do it at build time or brand colors — var(--brand) etc. — break
|
|
40
|
+
// (an invalid custom property silently drops the whole declaration that references it).
|
|
41
|
+
export function themeFromFields(fields) {
|
|
42
|
+
const obj = {};
|
|
43
|
+
for (const f of Array.isArray(fields) ? fields : []) {
|
|
44
|
+
if (!f?.name) continue;
|
|
45
|
+
obj[f.name] = f.type === 'group' ? themeFromFields(f.children) : f.default;
|
|
46
|
+
}
|
|
47
|
+
return obj;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadThemeContext(siteDir) {
|
|
51
|
+
try {
|
|
52
|
+
return themeFromFields(JSON.parse(await readFile(join(siteDir, 'fields.json'), 'utf8')));
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Resolve `{{ theme.a.b.c }}` tokens against the theme context. Targeted to theme.* only,
|
|
59
|
+
// so it can't disturb CSS syntax; unknown paths are left untouched.
|
|
60
|
+
export function resolveThemeTokens(text, theme) {
|
|
61
|
+
return String(text).replace(/\{\{\s*theme\.([\w.]+)\s*\}\}/g, (m, path) => {
|
|
62
|
+
let v = theme;
|
|
63
|
+
for (const k of path.split('.')) v = v == null ? v : v[k];
|
|
64
|
+
return v == null ? m : String(v);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Copy a directory, resolving theme tokens in .css files (HubSpot does this server-side).
|
|
69
|
+
async function copyCssResolved(fromDir, toDir, theme) {
|
|
70
|
+
if (!existsSync(fromDir)) return;
|
|
71
|
+
await mkdir(toDir, { recursive: true });
|
|
72
|
+
for (const ent of await readdir(fromDir, { withFileTypes: true })) {
|
|
73
|
+
const from = join(fromDir, ent.name);
|
|
74
|
+
const to = join(toDir, ent.name);
|
|
75
|
+
if (ent.isDirectory()) await copyCssResolved(from, to, theme);
|
|
76
|
+
else if (ent.name.endsWith('.css')) await writeFile(to, resolveThemeTokens(await readFile(from, 'utf8'), theme), 'utf8');
|
|
77
|
+
else await cp(from, to);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
37
81
|
/**
|
|
38
82
|
* buildStatic({ siteDir, outDir, baseUrl, assetBase, trackingPortalId }) -> summary
|
|
39
83
|
*
|
|
@@ -102,7 +146,11 @@ export async function buildStatic({ siteDir, outDir, baseUrl = '', assetBase = '
|
|
|
102
146
|
}
|
|
103
147
|
|
|
104
148
|
// Assets. get_asset_url maps ../css|js|images -> /css|js|images; @asset:<p> -> /assets/<p>.
|
|
105
|
-
|
|
149
|
+
// CSS is rendered (theme tokens resolved) since HubSpot resolves {{ theme.* }} server-side;
|
|
150
|
+
// everything else is copied as bytes.
|
|
151
|
+
const theme = await loadThemeContext(siteDir);
|
|
152
|
+
await copyCssResolved(join(siteDir, 'css'), join(outDir, 'css'), theme);
|
|
153
|
+
for (const [src, dest] of [['js', 'js'], ['images', 'images'],
|
|
106
154
|
['content/assets', 'assets'], ['content/blog/assets', 'assets']]) {
|
|
107
155
|
const from = join(siteDir, src);
|
|
108
156
|
if (existsSync(from)) await cp(from, join(outDir, dest), { recursive: true });
|
|
@@ -118,10 +166,13 @@ export async function buildStatic({ siteDir, outDir, baseUrl = '', assetBase = '
|
|
|
118
166
|
redirectCount = lines.length;
|
|
119
167
|
}
|
|
120
168
|
|
|
169
|
+
// /assets/* are content-hashed, so immutable is safe. css/js use STABLE filenames, so
|
|
170
|
+
// immutable would pin stale styles at the edge for a year after a deploy — they must
|
|
171
|
+
// revalidate instead (cheap 304s) so CSS/JS fixes actually reach users.
|
|
121
172
|
await writeFile(join(outDir, '_headers'),
|
|
122
173
|
'/assets/*\n Cache-Control: public, max-age=31536000, immutable\n'
|
|
123
|
-
+ '/css/*\n Cache-Control: public, max-age=
|
|
124
|
-
+ '/js/*\n Cache-Control: public, max-age=
|
|
174
|
+
+ '/css/*\n Cache-Control: public, max-age=0, must-revalidate\n'
|
|
175
|
+
+ '/js/*\n Cache-Control: public, max-age=0, must-revalidate\n'
|
|
125
176
|
+ '/*\n X-Content-Type-Options: nosniff\n X-Frame-Options: SAMEORIGIN\n Referrer-Policy: strict-origin-when-cross-origin\n',
|
|
126
177
|
'utf8');
|
|
127
178
|
|