@coraltravelcenter/b2c-landing-builder 2.9.0 → 2.10.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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/index.mjs +13 -4
- package/src/deploy/deploy.mjs +60 -2
- package/src/deploy/page.mjs +1 -0
- package/src/deploy/reconcile.mjs +47 -0
- package/src/deploy/widgets.mjs +21 -2
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coraltravelcenter/b2c-landing-builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@coraltravelcenter/b2c-landing-builder",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.10.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@babel/parser": "7.28.6",
|
package/package.json
CHANGED
package/src/cli/index.mjs
CHANGED
|
@@ -12,6 +12,9 @@ Commands:
|
|
|
12
12
|
deploy --update Update the current CMS version in place
|
|
13
13
|
deploy --relink Attach the project to an existing CMS page
|
|
14
14
|
deploy --publish Publish after a successful deploy
|
|
15
|
+
deploy --section <key> Deploy one section, preserving all other remote sections
|
|
16
|
+
deploy --force Explicitly overwrite remote changes in the current scope
|
|
17
|
+
deploy --remote Keep remote content when changes conflict
|
|
15
18
|
deploy --assets-only --prune-assets
|
|
16
19
|
Remove obsolete project assets recorded in deploy-state
|
|
17
20
|
deploy --assets-only Build and sync assets with the B2C CDN
|
|
@@ -82,13 +85,17 @@ export async function runCli(args) {
|
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
87
|
if (command === "deploy") {
|
|
85
|
-
const allowed = new Set(["--assets-only", "--update", "--dry-run", "--relink", "--publish", "--prune-assets"]);
|
|
86
|
-
const flags = new Set(rest);
|
|
87
|
-
|
|
88
|
+
const allowed = new Set(["--assets-only", "--update", "--dry-run", "--relink", "--publish", "--prune-assets", "--force", "--remote"]);
|
|
89
|
+
const flags = new Set(rest.filter((argument) => allowed.has(argument)));
|
|
90
|
+
const sectionIndex = rest.indexOf("--section");
|
|
91
|
+
const section = sectionIndex >= 0 ? rest[sectionIndex + 1] : null;
|
|
92
|
+
const consumed = new Set([...flags, ...(sectionIndex >= 0 ? ["--section", section] : [])]);
|
|
93
|
+
if (rest.some((argument) => !consumed.has(argument)) || (sectionIndex >= 0 && !section) ||
|
|
94
|
+
(flags.has("--force") && flags.has("--remote")) ||
|
|
88
95
|
(flags.has("--assets-only") && (flags.has("--update") || flags.has("--relink") || flags.has("--publish"))) ||
|
|
89
96
|
(flags.has("--prune-assets") && !flags.has("--assets-only"))) {
|
|
90
97
|
throw new Error(
|
|
91
|
-
"Usage: b2c-landing-vite deploy [--
|
|
98
|
+
"Usage: b2c-landing-vite deploy [--section <key>] [--force | --remote] [--update] [--relink] [--publish] [--dry-run]"
|
|
92
99
|
);
|
|
93
100
|
}
|
|
94
101
|
const config = await loadConfig();
|
|
@@ -126,6 +133,8 @@ export async function runCli(args) {
|
|
|
126
133
|
relink: flags.has("--relink"),
|
|
127
134
|
publish: flags.has("--publish"),
|
|
128
135
|
pruneAssets: flags.has("--prune-assets"),
|
|
136
|
+
section,
|
|
137
|
+
strategy: flags.has("--force") ? "force" : flags.has("--remote") ? "remote" : "ask",
|
|
129
138
|
});
|
|
130
139
|
});
|
|
131
140
|
}
|
package/src/deploy/deploy.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import {planAssetChanges, uploadChangedAssets} from "./assets.mjs";
|
|
|
2
2
|
import {BackofficeClient} from "./backoffice-client.mjs";
|
|
3
3
|
import {readBuildManifest} from "./manifest.mjs";
|
|
4
4
|
import {readDeploymentState, writeDeploymentState} from "./state.mjs";
|
|
5
|
+
import {resolveSections, summarizeChanges} from "./reconcile.mjs";
|
|
5
6
|
import {
|
|
6
7
|
createPagePlacement,
|
|
7
8
|
prepareExistingPage,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
selectNewPagePlacement,
|
|
13
14
|
validateSavedPlacement,
|
|
14
15
|
} from "./page.mjs";
|
|
15
|
-
import {planWidgetChanges, syncWidgets} from "./widgets.mjs";
|
|
16
|
+
import {localSections, planWidgetChanges, remoteSections, syncWidgets} from "./widgets.mjs";
|
|
16
17
|
|
|
17
18
|
function paths(items) {
|
|
18
19
|
return items.map((item) => item.path);
|
|
@@ -139,6 +140,8 @@ export async function deployProject({
|
|
|
139
140
|
pruneAssets = false,
|
|
140
141
|
publish = false,
|
|
141
142
|
prompts,
|
|
143
|
+
section,
|
|
144
|
+
strategy = "ask",
|
|
142
145
|
log = console.log,
|
|
143
146
|
}) {
|
|
144
147
|
const manifest = readBuildManifest(root);
|
|
@@ -149,6 +152,7 @@ export async function deployProject({
|
|
|
149
152
|
? "[deploy] validating saved CMS placement..."
|
|
150
153
|
: relink ? "[deploy] selecting an existing CMS page..." : "[deploy] a new CMS page will be created");
|
|
151
154
|
let selectedPage = null;
|
|
155
|
+
let remoteBefore = null;
|
|
152
156
|
if (relink) selectedPage = await selectExistingPagePlacement({client: api, domain: preset.domain, prompts});
|
|
153
157
|
else if (previous.placement) {
|
|
154
158
|
selectedPage = {
|
|
@@ -156,6 +160,53 @@ export async function deployProject({
|
|
|
156
160
|
pageContent: await validateSavedPlacement({client: api, placement: previous.placement}),
|
|
157
161
|
};
|
|
158
162
|
}
|
|
163
|
+
const availableSections = manifest.blocks.map((block) => block.path.replace(/\.html$/, ""));
|
|
164
|
+
if (section && !availableSections.includes(section)) {
|
|
165
|
+
throw new Error(`Section ${JSON.stringify(section)} not found. Available sections: ${availableSections.join(", ") || "none"}`);
|
|
166
|
+
}
|
|
167
|
+
let reconciliation = null;
|
|
168
|
+
if (selectedPage) {
|
|
169
|
+
log(`[deploy] scope: ${section ? `section ${JSON.stringify(section)}` : "page"}`);
|
|
170
|
+
const local = localSections(manifest, root);
|
|
171
|
+
const remote = remoteSections(selectedPage.pageContent, selectedPage.placement, manifest.folder);
|
|
172
|
+
remoteBefore = remote;
|
|
173
|
+
const base = previous.cms?.sections || null;
|
|
174
|
+
let chosen = strategy;
|
|
175
|
+
reconciliation = resolveSections({local, remote, base, scope: section ? [section] : undefined, strategy: chosen === "ask" ? "merge" : chosen});
|
|
176
|
+
if (reconciliation.changes.length) {
|
|
177
|
+
const summary = summarizeChanges(reconciliation.changes);
|
|
178
|
+
for (const [kind, ids] of Object.entries(summary)) if (ids.length) log(`[deploy] ${kind}: ${ids.join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
if (reconciliation.unsafe.length && chosen === "ask") {
|
|
181
|
+
prompts ||= await (async () => {
|
|
182
|
+
const {isCancel, select} = await import("@clack/prompts");
|
|
183
|
+
return {select: async (options) => {
|
|
184
|
+
const value = await select(options);
|
|
185
|
+
if (isCancel(value)) throw new Error("Deploy cancelled");
|
|
186
|
+
return value;
|
|
187
|
+
}};
|
|
188
|
+
})();
|
|
189
|
+
chosen = await prompts.select({
|
|
190
|
+
message: "Remote CMS changes detected",
|
|
191
|
+
options: [
|
|
192
|
+
{label: "Keep remote changes", value: "remote"},
|
|
193
|
+
{label: `Overwrite ${section ? "this section" : "the page"} with local changes`, value: "force"},
|
|
194
|
+
{label: "Cancel deploy", value: "cancel"},
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
if (chosen === "cancel") throw new Error("Deploy cancelled; CMS was not modified");
|
|
198
|
+
reconciliation = resolveSections({local, remote, base, scope: section ? [section] : undefined, strategy: chosen});
|
|
199
|
+
} else if (reconciliation.unsafe.length) {
|
|
200
|
+
throw new Error(`Deploy stopped: remote changes require --force or --remote (${reconciliation.unsafe.map((item) => item.id).join(", ")})`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// No remote mutation happens until the page state and the user's resolution are known.
|
|
204
|
+
if (selectedPage) {
|
|
205
|
+
const verified = await validateSavedPlacement({client: api, placement: selectedPage.placement});
|
|
206
|
+
if (pageRevision(verified) !== pageRevision(selectedPage.pageContent)) {
|
|
207
|
+
throw new Error("CMS page changed during deploy. Nothing was modified; run deploy again.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
159
210
|
log("[deploy] synchronizing CDN assets...");
|
|
160
211
|
const assets = await uploadChangedAssets({
|
|
161
212
|
manifest,
|
|
@@ -180,11 +231,18 @@ export async function deployProject({
|
|
|
180
231
|
placement: page.placement,
|
|
181
232
|
pageContent: page.pageContent,
|
|
182
233
|
root,
|
|
234
|
+
sectionIds: reconciliation ? [...new Set([
|
|
235
|
+
...Object.keys(remoteBefore || {}),
|
|
236
|
+
...Object.keys(reconciliation.sections || {}),
|
|
237
|
+
])].filter((id) => reconciliation.sections?.[id] !== remoteBefore?.[id]) : null,
|
|
183
238
|
});
|
|
184
239
|
const state = {
|
|
185
240
|
...assets.state,
|
|
186
241
|
placement: page.placement,
|
|
187
|
-
cms: {
|
|
242
|
+
cms: {
|
|
243
|
+
verifiedAt: new Date().toISOString(),
|
|
244
|
+
sections: remoteSections(await api.getContent(page.placement.pageContentId), page.placement, manifest.folder),
|
|
245
|
+
},
|
|
188
246
|
};
|
|
189
247
|
let published = false;
|
|
190
248
|
let finalPageContent = await api.getContent(page.placement.pageContentId);
|
package/src/deploy/page.mjs
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function compareSections({local, remote, base}) {
|
|
2
|
+
const ids = [...new Set([...Object.keys(local), ...Object.keys(remote), ...Object.keys(base || {})])].sort();
|
|
3
|
+
const changes = [];
|
|
4
|
+
for (const id of ids) {
|
|
5
|
+
const localValue = local[id];
|
|
6
|
+
const remoteValue = remote[id];
|
|
7
|
+
const baseValue = base?.[id];
|
|
8
|
+
if (localValue === remoteValue) continue;
|
|
9
|
+
if (!base) {
|
|
10
|
+
changes.push({id, kind: "unknown", local: localValue, remote: remoteValue});
|
|
11
|
+
} else if (remoteValue === baseValue) {
|
|
12
|
+
changes.push({id, kind: "local", local: localValue, remote: remoteValue});
|
|
13
|
+
} else if (localValue === baseValue) {
|
|
14
|
+
changes.push({id, kind: "remote", local: localValue, remote: remoteValue});
|
|
15
|
+
} else {
|
|
16
|
+
changes.push({id, kind: "conflict", local: localValue, remote: remoteValue});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return changes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function summarizeChanges(changes) {
|
|
23
|
+
return Object.fromEntries(["local", "remote", "conflict", "unknown"].map((kind) => [
|
|
24
|
+
kind,
|
|
25
|
+
changes.filter((change) => change.kind === kind).map((change) => change.id),
|
|
26
|
+
]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveSections({local, remote, base, scope, strategy = "merge"}) {
|
|
30
|
+
const selected = scope?.length ? new Set(scope) : new Set(Object.keys(local));
|
|
31
|
+
const scopedLocal = Object.fromEntries(Object.entries(local).filter(([id]) => selected.has(id)));
|
|
32
|
+
const scopedRemote = Object.fromEntries(Object.entries(remote).filter(([id]) => selected.has(id)));
|
|
33
|
+
const scopedBase = base && Object.fromEntries(Object.entries(base).filter(([id]) => selected.has(id)));
|
|
34
|
+
const changes = compareSections({local: scopedLocal, remote: scopedRemote, base: scopedBase});
|
|
35
|
+
const unsafe = changes.filter((change) => change.kind === "conflict" || change.kind === "unknown" || change.kind === "remote");
|
|
36
|
+
if (strategy === "merge" && unsafe.length) return {changes, unsafe, sections: null};
|
|
37
|
+
const sections = {...remote};
|
|
38
|
+
for (const id of selected) {
|
|
39
|
+
const change = changes.find((item) => item.id === id);
|
|
40
|
+
if (strategy === "remote" && change) continue;
|
|
41
|
+
if (strategy === "force" || !change || change.kind === "local") {
|
|
42
|
+
if (local[id] === undefined) delete sections[id];
|
|
43
|
+
else sections[id] = local[id];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {changes, unsafe: [], sections};
|
|
47
|
+
}
|
package/src/deploy/widgets.mjs
CHANGED
|
@@ -14,6 +14,12 @@ export function readDeployBundles(manifest, root = process.cwd()) {
|
|
|
14
14
|
}));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function localSections(manifest, root = process.cwd()) {
|
|
18
|
+
return Object.fromEntries(readDeployBundles(manifest, root).map((bundle) => [
|
|
19
|
+
bundle.title.slice(bundle.title.lastIndexOf(" | ") + 3), bundle.source,
|
|
20
|
+
]));
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
function widgetsForPlacement(pageContent, layoutAreaId, folder) {
|
|
18
24
|
const widgets = pageContent.pageContents
|
|
19
25
|
?.find((content) => String(content.languageId) === RU_LANGUAGE_ID)?.widgets || [];
|
|
@@ -23,6 +29,12 @@ function widgetsForPlacement(pageContent, layoutAreaId, folder) {
|
|
|
23
29
|
);
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
export function remoteSections(pageContent, placement, folder) {
|
|
33
|
+
return Object.fromEntries(widgetsForPlacement(pageContent || {}, placement.layoutAreaId, folder).map((widget) => [
|
|
34
|
+
widget.cmsTitle.slice(widget.cmsTitle.lastIndexOf(" | ") + 3), widget.data?.content ?? widget.content ?? "",
|
|
35
|
+
]));
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
export function planWidgetChanges({manifest, placement, pageContent, root = process.cwd()}) {
|
|
27
39
|
const bundles = readDeployBundles(manifest, root);
|
|
28
40
|
const existing = widgetsForPlacement(pageContent || {}, placement.layoutAreaId, manifest.folder);
|
|
@@ -41,15 +53,20 @@ export function planWidgetChanges({manifest, placement, pageContent, root = proc
|
|
|
41
53
|
};
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
export async function syncWidgets({client, manifest, placement, pageContent, root = process.cwd()}) {
|
|
56
|
+
export async function syncWidgets({client, manifest, placement, pageContent, root = process.cwd(), sectionIds = null}) {
|
|
45
57
|
const bundles = readDeployBundles(manifest, root);
|
|
58
|
+
const selected = sectionIds && new Set(sectionIds);
|
|
59
|
+
const scopedBundles = selected
|
|
60
|
+
? bundles.filter((bundle) => selected.has(bundle.title.slice(bundle.title.lastIndexOf(" | ") + 3)))
|
|
61
|
+
: bundles;
|
|
46
62
|
const existing = widgetsForPlacement(pageContent, placement.layoutAreaId, manifest.folder);
|
|
47
63
|
const activeIds = new Set();
|
|
48
64
|
let added = 0;
|
|
49
65
|
let updated = 0;
|
|
50
66
|
let reordered = 0;
|
|
51
67
|
|
|
52
|
-
for (const
|
|
68
|
+
for (const bundle of scopedBundles) {
|
|
69
|
+
const index = bundles.indexOf(bundle);
|
|
53
70
|
let widget = existing.find((candidate) => candidate.cmsTitle === bundle.title);
|
|
54
71
|
if (widget) {
|
|
55
72
|
await client.updateWidget(bundle.title, widget.contentWidgetId, bundle.source);
|
|
@@ -77,6 +94,8 @@ export async function syncWidgets({client, manifest, placement, pageContent, roo
|
|
|
77
94
|
|
|
78
95
|
let removed = 0;
|
|
79
96
|
for (const widget of existing) {
|
|
97
|
+
const id = widget.cmsTitle.slice(widget.cmsTitle.lastIndexOf(" | ") + 3);
|
|
98
|
+
if (selected && !selected.has(id)) continue;
|
|
80
99
|
if (activeIds.has(widget.contentWidgetId)) continue;
|
|
81
100
|
await client.removeWidget({
|
|
82
101
|
contentWidgetId: widget.contentWidgetId,
|