@coraltravelcenter/b2c-landing-builder 2.6.2 → 2.7.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/README.md +5 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/index.mjs +16 -4
- package/src/deploy/assets.mjs +41 -5
- package/src/deploy/backoffice-client.mjs +8 -1
- package/src/deploy/deploy.mjs +72 -5
- package/src/deploy/page.mjs +16 -3
- package/src/deploy/widgets.mjs +18 -0
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ b2c-landing-vite check
|
|
|
22
22
|
b2c-landing-vite dev
|
|
23
23
|
b2c-landing-vite build
|
|
24
24
|
b2c-landing-vite deploy
|
|
25
|
+
b2c-landing-vite deploy --dry-run
|
|
25
26
|
b2c-landing-vite deploy --update
|
|
26
27
|
b2c-landing-vite deploy --assets-only
|
|
27
28
|
b2c-landing-vite block:add hero
|
|
@@ -40,6 +41,10 @@ b2c-landing-vite update
|
|
|
40
41
|
- `deploy` при первом запуске предлагает выбрать application, layout и HTML-зону,
|
|
41
42
|
создаёт CMS-страницу и размещает блоки в порядке `order.json`. При повторном
|
|
42
43
|
запуске для опубликованной страницы создаётся checkout-версия.
|
|
44
|
+
- `deploy --dry-run` выполняет свежую локальную сборку, читает состояние CMS и
|
|
45
|
+
показывает точный план загрузки ассетов, создания версии и изменений виджетов.
|
|
46
|
+
Страница, виджеты, ассеты и локальный deploy-state при этом не изменяются.
|
|
47
|
+
- `deploy --assets-only --dry-run` показывает только план синхронизации ассетов.
|
|
43
48
|
- `deploy --update` обновляет сохранённую версию страницы без создания новой.
|
|
44
49
|
- `block:add` создаёт файлы в выбранных форматах разметки и стилей.
|
|
45
50
|
- `block:rename` атомарно переименовывает все файлы блока и обновляет порядок.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coraltravelcenter/b2c-landing-builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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.7.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clack/prompts": "1.7.0",
|
package/package.json
CHANGED
package/src/cli/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ Commands:
|
|
|
8
8
|
dev Start the Vite development server
|
|
9
9
|
build Build CMS-ready HTML files
|
|
10
10
|
deploy Build, sync assets, and deploy CMS widgets
|
|
11
|
+
deploy --dry-run Show the exact deploy plan without changing CMS
|
|
11
12
|
deploy --update Update the current CMS version in place
|
|
12
13
|
deploy --assets-only Build and sync assets with the B2C CDN
|
|
13
14
|
check Validate project configuration
|
|
@@ -77,9 +78,11 @@ export async function runCli(args) {
|
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
80
|
if (command === "deploy") {
|
|
80
|
-
const allowed = new Set(["--assets-only", "--update"]);
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const allowed = new Set(["--assets-only", "--update", "--dry-run"]);
|
|
82
|
+
const flags = new Set(rest);
|
|
83
|
+
if (rest.some((argument) => !allowed.has(argument)) || flags.size !== rest.length ||
|
|
84
|
+
(flags.has("--assets-only") && flags.has("--update"))) {
|
|
85
|
+
throw new Error("Usage: b2c-landing-vite deploy [--assets-only | --update] [--dry-run]");
|
|
83
86
|
}
|
|
84
87
|
const config = await loadConfig();
|
|
85
88
|
validateProject(config);
|
|
@@ -87,7 +90,16 @@ export async function runCli(args) {
|
|
|
87
90
|
await runCli(["build"]);
|
|
88
91
|
const {getBackofficeToken} = await import("../deploy/auth.mjs");
|
|
89
92
|
const token = await getBackofficeToken({brand: preset.id});
|
|
90
|
-
const {deployAssetsOnly, deployProject} = await import("../deploy/deploy.mjs");
|
|
93
|
+
const {deployAssetsOnly, deployProject, planDeployment} = await import("../deploy/deploy.mjs");
|
|
94
|
+
if (flags.has("--dry-run")) {
|
|
95
|
+
return planDeployment({
|
|
96
|
+
config,
|
|
97
|
+
preset,
|
|
98
|
+
token,
|
|
99
|
+
update: flags.has("--update"),
|
|
100
|
+
assetsOnly: flags.has("--assets-only"),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
91
103
|
if (rest.includes("--assets-only")) return deployAssetsOnly({config, preset, token});
|
|
92
104
|
return deployProject({config, preset, token, update: rest.includes("--update")});
|
|
93
105
|
}
|
package/src/deploy/assets.mjs
CHANGED
|
@@ -4,22 +4,57 @@ export function changedAssets(manifest, state) {
|
|
|
4
4
|
return manifest.assets.filter((asset) => state.assets?.[asset.path]?.sha256 !== asset.sha256);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function filesFromListing(listing) {
|
|
8
|
+
const files = Array.isArray(listing) ? listing : listing?.files || [];
|
|
9
|
+
return new Set(files.map((file) => typeof file === "string" ? path.posix.basename(file) : file.name));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function planAssetChanges({manifest, state, listFiles, remoteRoot = "/content"}) {
|
|
13
|
+
const pending = changedAssets(manifest, state);
|
|
14
|
+
if (!listFiles) {
|
|
15
|
+
return {upload: pending, skip: [], unchanged: manifest.assets.length - pending.length};
|
|
16
|
+
}
|
|
17
|
+
const directories = new Map();
|
|
18
|
+
const namesInDirectory = (directory) => {
|
|
19
|
+
if (!directories.has(directory)) {
|
|
20
|
+
directories.set(directory, Promise.resolve(listFiles(directory)).then(filesFromListing));
|
|
21
|
+
}
|
|
22
|
+
return directories.get(directory);
|
|
23
|
+
};
|
|
24
|
+
const upload = [];
|
|
25
|
+
const skip = [];
|
|
26
|
+
await Promise.all(pending.map(async (asset) => {
|
|
27
|
+
const remotePath = path.posix.join("/", remoteRoot, manifest.assetsPrefix, asset.path);
|
|
28
|
+
const names = await namesInDirectory(path.posix.dirname(remotePath));
|
|
29
|
+
(names.has(path.posix.basename(remotePath)) ? skip : upload).push(asset);
|
|
30
|
+
}));
|
|
31
|
+
const byPath = (left, right) => left.path.localeCompare(right.path);
|
|
32
|
+
return {
|
|
33
|
+
upload: upload.sort(byPath),
|
|
34
|
+
skip: skip.sort(byPath),
|
|
35
|
+
unchanged: manifest.assets.length - pending.length,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
7
39
|
export async function uploadChangedAssets({
|
|
8
40
|
manifest,
|
|
9
41
|
state,
|
|
10
42
|
root = process.cwd(),
|
|
11
43
|
uploadFile,
|
|
44
|
+
listFiles,
|
|
12
45
|
remoteRoot = "/content",
|
|
13
46
|
concurrency = 5,
|
|
14
47
|
}) {
|
|
15
|
-
const
|
|
48
|
+
const plan = await planAssetChanges({manifest, state, listFiles, remoteRoot});
|
|
16
49
|
const assets = {...state.assets};
|
|
17
50
|
const uploaded = [];
|
|
51
|
+
const skipped = plan.skip.map((asset) => asset.path);
|
|
52
|
+
for (const asset of plan.skip) assets[asset.path] = {...asset, immutable: true};
|
|
18
53
|
let cursor = 0;
|
|
19
54
|
|
|
20
55
|
async function worker() {
|
|
21
|
-
while (cursor <
|
|
22
|
-
const asset =
|
|
56
|
+
while (cursor < plan.upload.length) {
|
|
57
|
+
const asset = plan.upload[cursor++];
|
|
23
58
|
const localPath = path.join(root, ...(manifest.assetsDirectory || "public").split("/"), ...asset.path.split("/"));
|
|
24
59
|
const remotePath = path.posix.join("/", remoteRoot, manifest.assetsPrefix, asset.path);
|
|
25
60
|
const result = await uploadFile(remotePath, localPath);
|
|
@@ -28,11 +63,12 @@ export async function uploadChangedAssets({
|
|
|
28
63
|
}
|
|
29
64
|
}
|
|
30
65
|
|
|
31
|
-
const workerCount = Math.min(Math.max(1, concurrency),
|
|
66
|
+
const workerCount = Math.min(Math.max(1, concurrency), plan.upload.length || 1);
|
|
32
67
|
await Promise.all(Array.from({length: workerCount}, () => worker()));
|
|
33
68
|
return {
|
|
34
69
|
state: {...state, assets},
|
|
35
70
|
uploaded: uploaded.sort(),
|
|
36
|
-
|
|
71
|
+
skipped: skipped.sort(),
|
|
72
|
+
unchanged: plan.unchanged,
|
|
37
73
|
};
|
|
38
74
|
}
|
|
@@ -100,10 +100,17 @@ export class BackofficeClient {
|
|
|
100
100
|
return this.request("PUT", "/Content/UpdateDocumentStatus", {body: {pageContentId, documentStatus}});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
async listFiles(remoteDirectory) {
|
|
104
|
+
const result = await this.request("GET", "/FileManagement/ListFiles", {
|
|
105
|
+
query: {path: remoteDirectory.replace(/^\/+/, "")},
|
|
106
|
+
});
|
|
107
|
+
return result?.result || result;
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
async uploadFile(remotePath, localPath) {
|
|
104
111
|
const parsed = path.posix.parse(remotePath);
|
|
105
112
|
const form = new FormData();
|
|
106
|
-
form.append("Body.OverWrite", "
|
|
113
|
+
form.append("Body.OverWrite", "false");
|
|
107
114
|
form.append("Body.Path", parsed.dir.replace(/^\/+/, ""));
|
|
108
115
|
form.append("Body.Files", new Blob([fs.readFileSync(localPath)]), parsed.base);
|
|
109
116
|
const result = await this.request("POST", "/FileManagement/UploadFile", {form});
|
package/src/deploy/deploy.mjs
CHANGED
|
@@ -1,9 +1,68 @@
|
|
|
1
|
-
import {uploadChangedAssets} from "./assets.mjs";
|
|
1
|
+
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 {createPagePlacement, prepareExistingPage} from "./page.mjs";
|
|
6
|
-
import {syncWidgets} from "./widgets.mjs";
|
|
5
|
+
import {createPagePlacement, prepareExistingPage, selectNewPagePlacement} from "./page.mjs";
|
|
6
|
+
import {planWidgetChanges, syncWidgets} from "./widgets.mjs";
|
|
7
|
+
|
|
8
|
+
function paths(items) {
|
|
9
|
+
return items.map((item) => item.path);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function logList(log, label, items) {
|
|
13
|
+
log(`[dry-run] ${label}: ${items.length ? items.join(", ") : "none"}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function planDeployment({
|
|
17
|
+
config,
|
|
18
|
+
preset,
|
|
19
|
+
root = process.cwd(),
|
|
20
|
+
token,
|
|
21
|
+
client,
|
|
22
|
+
update = false,
|
|
23
|
+
assetsOnly = false,
|
|
24
|
+
prompts,
|
|
25
|
+
log = console.log,
|
|
26
|
+
}) {
|
|
27
|
+
const manifest = readBuildManifest(root);
|
|
28
|
+
const api = client || new BackofficeClient({brand: preset.id, token});
|
|
29
|
+
const previous = readDeploymentState(preset.id, manifest.folder, root);
|
|
30
|
+
const assets = await planAssetChanges({
|
|
31
|
+
manifest,
|
|
32
|
+
state: previous,
|
|
33
|
+
remoteRoot: new URL(preset.assetsBase).pathname,
|
|
34
|
+
listFiles: api.listFiles?.bind(api),
|
|
35
|
+
});
|
|
36
|
+
log(`[dry-run] no CMS data will be changed`);
|
|
37
|
+
log(`[dry-run] target: ${preset.assetsBase}/${manifest.assetsPrefix}/`);
|
|
38
|
+
logList(log, "assets to upload", paths(assets.upload));
|
|
39
|
+
logList(log, "existing assets to skip", paths(assets.skip));
|
|
40
|
+
log(`[dry-run] unchanged assets: ${assets.unchanged}`);
|
|
41
|
+
if (assetsOnly) return {assets};
|
|
42
|
+
|
|
43
|
+
let placement;
|
|
44
|
+
let pageContent;
|
|
45
|
+
let page;
|
|
46
|
+
if (previous.placement) {
|
|
47
|
+
placement = previous.placement;
|
|
48
|
+
pageContent = await api.getContent(placement.pageContentId);
|
|
49
|
+
const checkout = !update && [1, 4].includes(pageContent.status);
|
|
50
|
+
page = {action: checkout ? "create checkout version" : "update current version", pageContentId: placement.pageContentId};
|
|
51
|
+
} else {
|
|
52
|
+
const selected = await selectNewPagePlacement({client: api, manifest, prompts});
|
|
53
|
+
placement = selected.placement;
|
|
54
|
+
pageContent = {pageContents: []};
|
|
55
|
+
page = {action: "create page", ...selected.draft};
|
|
56
|
+
}
|
|
57
|
+
const widgets = planWidgetChanges({manifest, placement, pageContent, root});
|
|
58
|
+
log(`[dry-run] page: ${page.action}${page.pageName ? ` “${page.pageName}”` : ""}`);
|
|
59
|
+
log(`[dry-run] area: ${placement.layoutAreaId}, insertion position: ${Math.max(0, placement.orderIndex) + 1}`);
|
|
60
|
+
logList(log, "widgets to add", widgets.add);
|
|
61
|
+
logList(log, "widgets to update", widgets.update);
|
|
62
|
+
logList(log, "widgets to remove", widgets.remove);
|
|
63
|
+
logList(log, "widgets to reorder", widgets.reorder.map((item) => `${item.title} (${item.from} → ${item.to})`));
|
|
64
|
+
return {assets, page, placement, widgets};
|
|
65
|
+
}
|
|
7
66
|
|
|
8
67
|
export async function deployAssetsOnly({
|
|
9
68
|
config,
|
|
@@ -30,9 +89,13 @@ export async function deployAssetsOnly({
|
|
|
30
89
|
root,
|
|
31
90
|
remoteRoot: new URL(preset.assetsBase).pathname,
|
|
32
91
|
uploadFile: api.uploadFile.bind(api),
|
|
92
|
+
listFiles: api.listFiles?.bind(api),
|
|
33
93
|
});
|
|
34
94
|
writeDeploymentState(preset.id, manifest.folder, result.state, root);
|
|
35
|
-
log(`[deploy] assets: uploaded=${result.uploaded.length}, unchanged=${result.unchanged}`);
|
|
95
|
+
log(`[deploy] assets: uploaded=${result.uploaded.length}, skipped=${result.skipped.length}, unchanged=${result.unchanged}`);
|
|
96
|
+
if (result.skipped.length) {
|
|
97
|
+
log(`[deploy] existing assets were not overwritten: ${result.skipped.join(", ")}. Rename changed files to publish new URLs.`);
|
|
98
|
+
}
|
|
36
99
|
return {...result, target: String(cdnUrl)};
|
|
37
100
|
}
|
|
38
101
|
|
|
@@ -56,6 +119,7 @@ export async function deployProject({
|
|
|
56
119
|
root,
|
|
57
120
|
remoteRoot: new URL(preset.assetsBase).pathname,
|
|
58
121
|
uploadFile: api.uploadFile.bind(api),
|
|
122
|
+
listFiles: api.listFiles?.bind(api),
|
|
59
123
|
});
|
|
60
124
|
const page = previous.placement
|
|
61
125
|
? await prepareExistingPage({client: api, placement: previous.placement, update})
|
|
@@ -72,7 +136,10 @@ export async function deployProject({
|
|
|
72
136
|
const preview = page.pageContent.uniqueId
|
|
73
137
|
? `https://${preset.domain}/preview/${page.pageContent.uniqueId}/`
|
|
74
138
|
: null;
|
|
75
|
-
log(`[deploy] assets: uploaded=${assets.uploaded.length}, unchanged=${assets.unchanged}`);
|
|
139
|
+
log(`[deploy] assets: uploaded=${assets.uploaded.length}, skipped=${assets.skipped.length}, unchanged=${assets.unchanged}`);
|
|
140
|
+
if (assets.skipped.length) {
|
|
141
|
+
log(`[deploy] existing assets were not overwritten: ${assets.skipped.join(", ")}. Rename changed files to publish new URLs.`);
|
|
142
|
+
}
|
|
76
143
|
log(`[deploy] widgets: added=${widgets.added}, updated=${widgets.updated}, removed=${widgets.removed}`);
|
|
77
144
|
if (preview) log(`[deploy] preview: ${preview}`);
|
|
78
145
|
return {assets, widgets, placement: page.placement, preview};
|
package/src/deploy/page.mjs
CHANGED
|
@@ -37,7 +37,7 @@ async function defaultPrompts() {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export async function
|
|
40
|
+
export async function selectNewPagePlacement({client, manifest, prompts}) {
|
|
41
41
|
prompts ||= await defaultPrompts();
|
|
42
42
|
const pageName = await prompts.text({
|
|
43
43
|
message: "New CMS page name",
|
|
@@ -88,11 +88,24 @@ export async function createPagePlacement({client, manifest, prompts}) {
|
|
|
88
88
|
value: index,
|
|
89
89
|
})),
|
|
90
90
|
});
|
|
91
|
-
|
|
91
|
+
return {
|
|
92
|
+
draft: {applicationId, layoutId, pageName: pageName.trim()},
|
|
93
|
+
placement: {pageContentId: null, layoutAreaId, orderIndex},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function createPagePlacement(options) {
|
|
98
|
+
const selected = await selectNewPagePlacement(options);
|
|
99
|
+
const {client} = options;
|
|
100
|
+
const created = await client.createPage({
|
|
101
|
+
applicationId: selected.draft.applicationId,
|
|
102
|
+
layoutId: selected.draft.layoutId,
|
|
103
|
+
name: selected.draft.pageName,
|
|
104
|
+
});
|
|
92
105
|
if (!created?.pageContentId) throw new Error("Backoffice did not return pageContentId for the new page");
|
|
93
106
|
return {
|
|
94
107
|
pageContent: await client.getContent(created.pageContentId),
|
|
95
|
-
placement: {pageContentId: created.pageContentId
|
|
108
|
+
placement: {...selected.placement, pageContentId: created.pageContentId},
|
|
96
109
|
};
|
|
97
110
|
}
|
|
98
111
|
|
package/src/deploy/widgets.mjs
CHANGED
|
@@ -23,6 +23,24 @@ function widgetsForPlacement(pageContent, layoutAreaId, folder) {
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function planWidgetChanges({manifest, placement, pageContent, root = process.cwd()}) {
|
|
27
|
+
const bundles = readDeployBundles(manifest, root);
|
|
28
|
+
const existing = widgetsForPlacement(pageContent || {}, placement.layoutAreaId, manifest.folder);
|
|
29
|
+
const bundleTitles = new Set(bundles.map((bundle) => bundle.title));
|
|
30
|
+
return {
|
|
31
|
+
add: bundles.filter((bundle) => !existing.some((widget) => widget.cmsTitle === bundle.title))
|
|
32
|
+
.map((bundle) => bundle.title),
|
|
33
|
+
update: bundles.filter((bundle) => existing.some((widget) => widget.cmsTitle === bundle.title))
|
|
34
|
+
.map((bundle) => bundle.title),
|
|
35
|
+
reorder: bundles.flatMap((bundle, index) => {
|
|
36
|
+
const widget = existing.find((candidate) => candidate.cmsTitle === bundle.title);
|
|
37
|
+
const to = Math.max(0, placement.orderIndex) + 1 + index;
|
|
38
|
+
return widget && widget.order !== to ? [{title: bundle.title, from: widget.order, to}] : [];
|
|
39
|
+
}),
|
|
40
|
+
remove: existing.filter((widget) => !bundleTitles.has(widget.cmsTitle)).map((widget) => widget.cmsTitle),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
export async function syncWidgets({client, manifest, placement, pageContent, root = process.cwd()}) {
|
|
27
45
|
const bundles = readDeployBundles(manifest, root);
|
|
28
46
|
const existing = widgetsForPlacement(pageContent, placement.layoutAreaId, manifest.folder);
|