@coraltravelcenter/b2c-landing-builder 2.10.1 → 2.12.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 +69 -52
- package/bin/b2c-landing-vite.mjs +3 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/index.mjs +20 -46
- package/src/config/validate-config.mjs +3 -0
- package/src/deploy/assets-command.mjs +124 -0
- package/src/deploy/assets.mjs +38 -4
- package/src/deploy/auth.mjs +12 -2
- package/src/deploy/deploy.mjs +190 -67
- package/src/deploy/page.mjs +60 -12
- package/src/deploy/state.mjs +29 -2
- package/src/presets/coral.mjs +1 -0
- package/src/presets/sunmar.mjs +1 -0
- package/src/update/check-update.mjs +34 -10
- package/src/vite/client.js +6 -3
- package/src/vite/config.mjs +18 -3
package/README.md
CHANGED
|
@@ -22,42 +22,27 @@ 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
|
|
26
|
-
b2c-landing-vite deploy --update
|
|
27
|
-
b2c-landing-vite deploy --relink
|
|
28
|
-
b2c-landing-vite deploy --publish
|
|
29
25
|
b2c-landing-vite deploy --section hero
|
|
30
|
-
b2c-landing-vite deploy
|
|
31
|
-
b2c-landing-vite deploy --force
|
|
32
|
-
b2c-landing-vite deploy --assets-only
|
|
33
|
-
b2c-landing-vite deploy --assets-only --prune-assets
|
|
26
|
+
b2c-landing-vite deploy assets
|
|
34
27
|
b2c-landing-vite block:add hero
|
|
35
28
|
b2c-landing-vite block:rename hero main-hero
|
|
36
29
|
b2c-landing-vite update
|
|
37
30
|
```
|
|
38
31
|
|
|
39
32
|
- `check` проверяет конфигурацию, `order.json` и контракт JS-блоков.
|
|
40
|
-
- `dev` запускает Vite и userscript для
|
|
33
|
+
- `dev` запускает Vite и отдельный userscript с названием проекта для
|
|
34
|
+
Tampermonkey.
|
|
41
35
|
- `build` создаёт CMS-файлы в `@CMS/` и переписывает пути ассетов на CDN.
|
|
42
|
-
- `deploy
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
- `deploy` собирает проект, проверяет ассеты на CDN и разворачивает CMS-виджеты.
|
|
37
|
+
- `deploy --section <key>` разворачивает только выбранную CMS-секцию, не изменяя
|
|
38
|
+
и не удаляя соседние секции.
|
|
39
|
+
- `deploy assets` собирает проект и синхронизирует ассеты с B2C CDN.
|
|
40
|
+
- При первом deploy builder запрашивает backoffice-логин и скрытый пароль через
|
|
41
|
+
ADFS. На диск сохраняется только токен. Для CI можно передать
|
|
46
42
|
`B2C_BACKOFFICE_TOKEN`.
|
|
47
43
|
- Backoffice-запросы ограничены 30 секундами. Безопасные GET-запросы повторяются
|
|
48
44
|
до двух раз при сетевом сбое, timeout, HTTP 429, 502, 503 или 504. Изменяющие
|
|
49
45
|
CMS запросы автоматически не повторяются, чтобы не создать дубликаты.
|
|
50
|
-
- `deploy` при первом запуске предлагает выбрать application, layout и HTML-зону,
|
|
51
|
-
создаёт CMS-страницу и размещает блоки в порядке `order.json`. При повторном
|
|
52
|
-
запуске для опубликованной страницы создаётся checkout-версия.
|
|
53
|
-
- `deploy --dry-run` выполняет свежую локальную сборку, читает состояние CMS и
|
|
54
|
-
показывает точный план загрузки ассетов, создания версии и изменений виджетов.
|
|
55
|
-
Страница, виджеты, ассеты и локальный deploy-state при этом не изменяются.
|
|
56
|
-
- `deploy --assets-only --dry-run` показывает только план синхронизации ассетов.
|
|
57
|
-
- `deploy --update` обновляет сохранённую версию страницы без создания новой.
|
|
58
|
-
- `deploy --section <key>` меняет только блок с ключом из `order.json`. Builder
|
|
59
|
-
берёт за основу актуальную remote-страницу, поэтому локальные копии соседних
|
|
60
|
-
секций не попадают в payload и не могут затереть изменения из CMS.
|
|
61
46
|
- После успешного deploy builder сохраняет снимок секций в `.b2c/deploy/` как
|
|
62
47
|
достоверный `base`. Следующий deploy структурно сравнивает секции `local`,
|
|
63
48
|
актуальный `remote` и `base`. Изменения разных секций объединяются; изменение
|
|
@@ -65,28 +50,13 @@ b2c-landing-vite update
|
|
|
65
50
|
- Если `base` ещё отсутствует, отличие local от remote считается потенциальным
|
|
66
51
|
конфликтом. Обычный deploy не выполняет silent overwrite и предлагает явно
|
|
67
52
|
сохранить CMS-версию, применить local или отменить операцию.
|
|
68
|
-
- `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
опубликованной версии; вместе с `--update` выбранная версия меняется на месте.
|
|
76
|
-
- `deploy --relink --dry-run` проходит тот же интерактивный выбор и показывает
|
|
77
|
-
план, но не сохраняет новую привязку и не изменяет CMS.
|
|
78
|
-
- `deploy --publish` после успешной синхронизации виджетов публикует выбранную
|
|
79
|
-
версию. Публикация блокируется, если в Backoffice не заполнены title, URL,
|
|
80
|
-
SEO title или SEO description; builder не придумывает метаданные сам.
|
|
81
|
-
- `deploy --assets-only --prune-assets` удаляет из CMS только отсутствующие в
|
|
82
|
-
текущей сборке файлы, которые этот builder сам загрузил и записал как
|
|
83
|
-
управляемые в deploy-state проекта. Уже существовавшие в CMS файлы не
|
|
84
|
-
считаются управляемыми и не удаляются. Команда
|
|
85
|
-
намеренно отделена от обычного deploy и никогда не удаляет неизвестные файлы.
|
|
86
|
-
Сначала рекомендуется запустить её с `--dry-run`.
|
|
87
|
-
- Перед любым deploy сохранённые pageContentId и layoutAreaId сверяются с CMS.
|
|
88
|
-
Устаревшая привязка останавливает операцию до загрузки файлов и предлагает
|
|
89
|
-
`deploy --relink`.
|
|
53
|
+
- Стабильные `pageId`, `layoutAreaId` и позиция сохраняются отдельно в
|
|
54
|
+
`.b2c/deploy/<site>--<folder>.binding.json`. Этот файл можно хранить в Git,
|
|
55
|
+
чтобы команда находила ту же CMS-страницу после нового клонирования проекта.
|
|
56
|
+
- Перед deploy builder получает список версий сохранённой страницы. Published
|
|
57
|
+
выбирается по умолчанию; из Published или Unpublished создаётся checkout,
|
|
58
|
+
существующий Checkout обновляется напрямую. Потерянную привязку можно
|
|
59
|
+
восстановить интерактивным выбором существующей страницы.
|
|
90
60
|
- Одновременные deploy одного проекта на компьютере блокируются lock-файлом в
|
|
91
61
|
`.b2c/deploy/`; аварийно оставленный lock автоматически признаётся устаревшим.
|
|
92
62
|
- Непосредственно перед первой записью CMS-состояние читается повторно. Если оно
|
|
@@ -95,14 +65,15 @@ b2c-landing-vite update
|
|
|
95
65
|
- `block:rename` атомарно переименовывает все файлы блока и обновляет порядок.
|
|
96
66
|
- `update` проверяет npm registry и показывает команду ручного обновления.
|
|
97
67
|
|
|
98
|
-
После успешного выполнения рабочих команд builder коротко
|
|
99
|
-
registry
|
|
100
|
-
|
|
101
|
-
|
|
68
|
+
После успешного выполнения рабочих команд builder коротко и независимо
|
|
69
|
+
проверяет в npm registry версии самого builder и шаблона проекта. Для проекта
|
|
70
|
+
без `template.version` шаблон помечается как `legacy`. Ошибки сети не влияют на
|
|
71
|
+
работу CLI; ничего не обновляется автоматически:
|
|
102
72
|
|
|
103
73
|
```bash
|
|
104
74
|
b2c-landing-vite update
|
|
105
75
|
# Update manually: npm install --global @coraltravelcenter/b2c-landing-builder@latest
|
|
76
|
+
# Update project template: npx @coraltravelcenter/create-b2c-landing-vite@latest migrate
|
|
106
77
|
```
|
|
107
78
|
|
|
108
79
|
## Создание проекта
|
|
@@ -126,6 +97,9 @@ export default {
|
|
|
126
97
|
builder: {
|
|
127
98
|
minVersion: "2.10.0",
|
|
128
99
|
},
|
|
100
|
+
template: {
|
|
101
|
+
version: "2.13.0",
|
|
102
|
+
},
|
|
129
103
|
project: {
|
|
130
104
|
name: "uae-promo",
|
|
131
105
|
},
|
|
@@ -140,6 +114,10 @@ export default {
|
|
|
140
114
|
};
|
|
141
115
|
```
|
|
142
116
|
|
|
117
|
+
`template.version` записывается генератором и показывает, какой версией шаблона
|
|
118
|
+
создан проект. Для старых проектов поле необязательно; его отсутствие позволяет
|
|
119
|
+
builder распознать legacy-шаблон и предложить миграцию в будущих версиях.
|
|
120
|
+
|
|
143
121
|
Имя проекта должно соответствовать `^[a-z0-9][a-z0-9-]*$`.
|
|
144
122
|
|
|
145
123
|
Поддерживаются:
|
|
@@ -167,6 +145,15 @@ Monkey URL, контейнер и CDN prefix не задаются вручну
|
|
|
167
145
|
- контейнер: `#monkey-app`;
|
|
168
146
|
- CDN prefix: `landing-pages/<project.name>`.
|
|
169
147
|
|
|
148
|
+
Dev-сервер начинает с `127.0.0.1:5173` и автоматически выбирает следующий
|
|
149
|
+
свободный порт, поэтому несколько проектов можно запускать одновременно. Если
|
|
150
|
+
задан `B2C_PORT`, порт считается обязательным и занятое значение завершает
|
|
151
|
+
команду ошибкой.
|
|
152
|
+
|
|
153
|
+
Userscript ограничен доменом выбранного бренда и дополнительно проверяет hostname
|
|
154
|
+
перед монтированием. Namespace также содержит бренд, поэтому Coral и Sunmar не
|
|
155
|
+
конфликтуют даже при одинаковом названии проекта.
|
|
156
|
+
|
|
170
157
|
Для Coral и Sunmar настроены отдельные production CDN. Команда `build`
|
|
171
158
|
автоматически выбирает CDN по `site.preset`.
|
|
172
159
|
|
|
@@ -242,11 +229,41 @@ build; дополнительные контейнеры builder не созда
|
|
|
242
229
|
}
|
|
243
230
|
```
|
|
244
231
|
|
|
245
|
-
В dev пути направляются на
|
|
246
|
-
|
|
232
|
+
В dev пути направляются на фактический origin текущего Vite-сервера. При
|
|
233
|
+
production build они
|
|
234
|
+
преобразуются в `<assetsBase>/landing-pages/<folder>/...`. Builder
|
|
247
235
|
переписывает только изображения `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif` и
|
|
248
236
|
`svg`; остальные root-relative ресурсы не изменяются.
|
|
249
237
|
|
|
238
|
+
### Deploy assets
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
b2c-landing-vite deploy assets
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Команда выполняет свежую сборку, сравнивает build manifest с deploy-state и CDN,
|
|
245
|
+
а затем показывает план загрузки. Перед отправкой файлов требуется явное
|
|
246
|
+
подтверждение; по умолчанию выбран отказ.
|
|
247
|
+
|
|
248
|
+
Вложенная структура `public/` сохраняется на CDN. Например,
|
|
249
|
+
`public/hotels/gallery/room.webp` загружается как
|
|
250
|
+
`landing-pages/<folder>/hotels/gallery/room.webp`.
|
|
251
|
+
|
|
252
|
+
Существующие CDN-файлы не перезаписываются и не удаляются. Если содержимое
|
|
253
|
+
изменилось, файл нужно переименовать, чтобы опубликовать его по новому URL.
|
|
254
|
+
Deploy-state обновляется атомарно только после успешной загрузки всех файлов.
|
|
255
|
+
В итоговом блоке команда также выводит корневой URL `CDN folder`, например
|
|
256
|
+
`https://b2ccdn.coral.ru/content/landing-pages/uae-promo/`.
|
|
257
|
+
|
|
258
|
+
Обычный `deploy` не загружает ассеты. Он проверяет наличие всех файлов build
|
|
259
|
+
manifest на CDN и останавливается до изменения CMS, если сначала требуется
|
|
260
|
+
выполнить `deploy assets`.
|
|
261
|
+
|
|
262
|
+
Перед изменением CMS команда показывает план добавления, обновления, удаления и
|
|
263
|
+
перестановки управляемых виджетов. Подтверждение и последующее предложение
|
|
264
|
+
публикации по умолчанию имеют значение `No`. После deploy выводятся Preview и
|
|
265
|
+
прямая ссылка на созданную checkout-версию в BackOffice.
|
|
266
|
+
|
|
250
267
|
## Совместимость
|
|
251
268
|
|
|
252
269
|
Builder проверяет `schemaVersion` до запуска команд. Несовместимая версия схемы
|
package/bin/b2c-landing-vite.mjs
CHANGED
|
@@ -9,7 +9,9 @@ runCli(args).then(async () => {
|
|
|
9
9
|
if (!command || ["help", "--help", "-h", "update"].includes(command)) return;
|
|
10
10
|
|
|
11
11
|
const {printUpdateNotice} = await import("../src/update/check-update.mjs");
|
|
12
|
-
await
|
|
12
|
+
const {loadConfig} = await import("../src/config/load-config.mjs");
|
|
13
|
+
const config = await loadConfig().catch(() => null);
|
|
14
|
+
await printUpdateNotice({templateVersion: config?.template?.version});
|
|
13
15
|
}).catch((error) => {
|
|
14
16
|
console.error(`[b2c-landing-vite] ${error.message}`);
|
|
15
17
|
process.exitCode = 1;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coraltravelcenter/b2c-landing-builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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.12.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
|
@@ -8,16 +8,8 @@ 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 --
|
|
12
|
-
deploy
|
|
13
|
-
deploy --relink Attach the project to an existing CMS page
|
|
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
|
|
18
|
-
deploy --assets-only --prune-assets
|
|
19
|
-
Remove obsolete project assets recorded in deploy-state
|
|
20
|
-
deploy --assets-only Build and sync assets with the B2C CDN
|
|
11
|
+
deploy --section <key> Deploy one CMS section
|
|
12
|
+
deploy assets Build and sync assets with the B2C CDN
|
|
21
13
|
check Validate project configuration
|
|
22
14
|
block:add <key> Create a block
|
|
23
15
|
block:rename <old> <new> Rename a block
|
|
@@ -85,56 +77,38 @@ export async function runCli(args) {
|
|
|
85
77
|
return;
|
|
86
78
|
}
|
|
87
79
|
if (command === "deploy") {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
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")) ||
|
|
95
|
-
(flags.has("--assets-only") && (flags.has("--update") || flags.has("--relink") || flags.has("--publish"))) ||
|
|
96
|
-
(flags.has("--prune-assets") && !flags.has("--assets-only"))) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
"Usage: b2c-landing-vite deploy [--section <key>] [--force | --remote] [--update] [--relink] [--publish] [--dry-run]"
|
|
99
|
-
);
|
|
80
|
+
const assetsOnly = rest.length === 1 && rest[0] === "assets";
|
|
81
|
+
const sectionOnly = rest.length === 2 && rest[0] === "--section" && Boolean(rest[1]);
|
|
82
|
+
if (rest.length && !assetsOnly && !sectionOnly) {
|
|
83
|
+
throw new Error("Usage: b2c-landing-vite deploy [assets | --section <key>]");
|
|
100
84
|
}
|
|
101
85
|
const config = await loadConfig();
|
|
102
86
|
const project = validateProject(config);
|
|
103
87
|
const preset = resolvePreset(config.site.preset, {requireAssets: true});
|
|
104
88
|
const {withDeploymentLock} = await import("../deploy/lock.mjs");
|
|
105
89
|
return withDeploymentLock({site: preset.id, folder: project.folder}, async () => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const {deployAssetsOnly, deployProject, planDeployment} = await import("../deploy/deploy.mjs");
|
|
110
|
-
if (flags.has("--dry-run")) {
|
|
111
|
-
return planDeployment({
|
|
90
|
+
if (assetsOnly) {
|
|
91
|
+
const {deployAssetsCommand} = await import("../deploy/assets-command.mjs");
|
|
92
|
+
return deployAssetsCommand({
|
|
112
93
|
config,
|
|
94
|
+
project,
|
|
113
95
|
preset,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
pruneAssets: flags.has("--prune-assets"),
|
|
96
|
+
build: () => runCli(["build"]),
|
|
97
|
+
getToken: async () => {
|
|
98
|
+
const {getBackofficeToken} = await import("../deploy/auth.mjs");
|
|
99
|
+
return getBackofficeToken({brand: preset.id});
|
|
100
|
+
},
|
|
120
101
|
});
|
|
121
102
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pruneAssets: flags.has("--prune-assets"),
|
|
127
|
-
});
|
|
103
|
+
await runCli(["build"]);
|
|
104
|
+
const {getBackofficeToken} = await import("../deploy/auth.mjs");
|
|
105
|
+
const token = await getBackofficeToken({brand: preset.id});
|
|
106
|
+
const {deployProject} = await import("../deploy/deploy.mjs");
|
|
128
107
|
return deployProject({
|
|
129
108
|
config,
|
|
130
109
|
preset,
|
|
131
110
|
token,
|
|
132
|
-
|
|
133
|
-
relink: flags.has("--relink"),
|
|
134
|
-
publish: flags.has("--publish"),
|
|
135
|
-
pruneAssets: flags.has("--prune-assets"),
|
|
136
|
-
section,
|
|
137
|
-
strategy: flags.has("--force") ? "force" : flags.has("--remote") ? "remote" : "ask",
|
|
111
|
+
section: sectionOnly ? rest[1] : null,
|
|
138
112
|
});
|
|
139
113
|
});
|
|
140
114
|
}
|
|
@@ -33,6 +33,9 @@ export function validateConfig(config) {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
if (config.template !== undefined && !VERSION_RE.test(config.template?.version || "")) {
|
|
37
|
+
throw new Error("template.version must be a semantic version such as 2.12.2");
|
|
38
|
+
}
|
|
36
39
|
if (!PROJECT_NAME_RE.test(config.project?.name || "")) {
|
|
37
40
|
throw new Error("project.name must match ^[a-z0-9][a-z0-9-]*$");
|
|
38
41
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {planAssetChanges, uploadChangedAssets} from "./assets.mjs";
|
|
2
|
+
import {BackofficeClient} from "./backoffice-client.mjs";
|
|
3
|
+
import {readBuildManifest} from "./manifest.mjs";
|
|
4
|
+
import {readDeploymentState, writeDeploymentState} from "./state.mjs";
|
|
5
|
+
|
|
6
|
+
function formatFiles(prefix, files) {
|
|
7
|
+
return files.map((file) => `${prefix} ${file}`).join("\n");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function terminalUi() {
|
|
11
|
+
const {cancel, confirm, intro, isCancel, log, note, outro, spinner} = await import("@clack/prompts");
|
|
12
|
+
let progress;
|
|
13
|
+
return {
|
|
14
|
+
start() {
|
|
15
|
+
intro("Deploy assets");
|
|
16
|
+
},
|
|
17
|
+
built() {
|
|
18
|
+
log.success("Project built");
|
|
19
|
+
},
|
|
20
|
+
checking() {
|
|
21
|
+
log.step("Checking CDN state");
|
|
22
|
+
},
|
|
23
|
+
plan({target, upload, skip, unchanged}) {
|
|
24
|
+
note(target, "CDN folder");
|
|
25
|
+
const groups = [];
|
|
26
|
+
if (upload.length) groups.push(`Upload · ${upload.length}\n${formatFiles("+", upload)}`);
|
|
27
|
+
if (skip.length) {
|
|
28
|
+
groups.push(
|
|
29
|
+
`Skipped · ${skip.length}\n${formatFiles("!", skip)}\n` +
|
|
30
|
+
"Existing CDN files are immutable. Rename them to publish new versions."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
groups.push(`Unchanged · ${unchanged}`);
|
|
34
|
+
note(groups.join("\n\n"), "Asset plan");
|
|
35
|
+
},
|
|
36
|
+
async confirm(count) {
|
|
37
|
+
const answer = await confirm({message: `Upload ${count} file${count === 1 ? "" : "s"}?`, initialValue: false});
|
|
38
|
+
return isCancel(answer) ? false : answer;
|
|
39
|
+
},
|
|
40
|
+
uploading(count) {
|
|
41
|
+
progress = spinner();
|
|
42
|
+
progress.start(`Uploading ${count} file${count === 1 ? "" : "s"}`);
|
|
43
|
+
},
|
|
44
|
+
uploaded(count) {
|
|
45
|
+
progress?.stop(`Uploaded ${count} file${count === 1 ? "" : "s"}`);
|
|
46
|
+
},
|
|
47
|
+
failed(error) {
|
|
48
|
+
progress?.stop("Upload failed");
|
|
49
|
+
const uploaded = error.uploaded?.length ? error.uploaded.join(", ") : "none";
|
|
50
|
+
const failed = error.failed?.length ? error.failed.join(", ") : error.message;
|
|
51
|
+
log.error(`Uploaded before failure: ${uploaded}\nFailed: ${failed}\nDeploy state was not updated.`);
|
|
52
|
+
},
|
|
53
|
+
cancelled() {
|
|
54
|
+
cancel("Deploy cancelled");
|
|
55
|
+
},
|
|
56
|
+
complete({uploaded, skipped, unchanged}) {
|
|
57
|
+
outro(uploaded || skipped
|
|
58
|
+
? `Complete · uploaded ${uploaded} · skipped ${skipped} · unchanged ${unchanged}`
|
|
59
|
+
: "No asset changes");
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function deployAssetsCommand({
|
|
65
|
+
config,
|
|
66
|
+
project,
|
|
67
|
+
preset,
|
|
68
|
+
root = process.cwd(),
|
|
69
|
+
build,
|
|
70
|
+
getToken,
|
|
71
|
+
client,
|
|
72
|
+
ui,
|
|
73
|
+
}) {
|
|
74
|
+
ui ||= await terminalUi();
|
|
75
|
+
ui.start();
|
|
76
|
+
await build();
|
|
77
|
+
ui.built();
|
|
78
|
+
|
|
79
|
+
const manifest = readBuildManifest(root);
|
|
80
|
+
if (manifest.project !== config.project.name || manifest.site !== preset.id || manifest.folder !== project.folder) {
|
|
81
|
+
throw new Error("Build manifest does not match the current project. Run build again.");
|
|
82
|
+
}
|
|
83
|
+
const token = client ? null : await getToken();
|
|
84
|
+
const api = client || new BackofficeClient({brand: preset.id, token});
|
|
85
|
+
const state = readDeploymentState(preset.id, manifest.folder, root);
|
|
86
|
+
const remoteRoot = new URL(preset.assetsBase).pathname;
|
|
87
|
+
const target = `${preset.assetsBase}/${manifest.assetsPrefix}/`;
|
|
88
|
+
|
|
89
|
+
ui.checking();
|
|
90
|
+
const plan = await planAssetChanges({
|
|
91
|
+
manifest,
|
|
92
|
+
state,
|
|
93
|
+
remoteRoot,
|
|
94
|
+
listFiles: api.listFiles.bind(api),
|
|
95
|
+
});
|
|
96
|
+
const upload = plan.upload.map((asset) => asset.path);
|
|
97
|
+
const skip = plan.skip.map((asset) => asset.path);
|
|
98
|
+
ui.plan({brand: preset.id, target, upload, skip, unchanged: plan.unchanged});
|
|
99
|
+
|
|
100
|
+
if (upload.length && !await ui.confirm(upload.length)) {
|
|
101
|
+
ui.cancelled();
|
|
102
|
+
return {cancelled: true, plan, target};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (upload.length) ui.uploading(upload.length);
|
|
106
|
+
let result;
|
|
107
|
+
try {
|
|
108
|
+
result = await uploadChangedAssets({
|
|
109
|
+
manifest,
|
|
110
|
+
state,
|
|
111
|
+
root,
|
|
112
|
+
remoteRoot,
|
|
113
|
+
uploadFile: api.uploadFile.bind(api),
|
|
114
|
+
plan,
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
ui.failed(error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
writeDeploymentState(preset.id, manifest.folder, result.state, root);
|
|
121
|
+
if (upload.length) ui.uploaded(result.uploaded.length);
|
|
122
|
+
ui.complete({uploaded: result.uploaded.length, skipped: result.skipped.length, unchanged: result.unchanged});
|
|
123
|
+
return {...result, cancelled: false, target};
|
|
124
|
+
}
|
package/src/deploy/assets.mjs
CHANGED
|
@@ -50,6 +50,23 @@ export async function planAssetChanges({manifest, state, listFiles, remoteRoot =
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export async function findMissingRemoteAssets({manifest, listFiles, remoteRoot = "/content"}) {
|
|
54
|
+
const directories = new Map();
|
|
55
|
+
const namesInDirectory = (directory) => {
|
|
56
|
+
if (!directories.has(directory)) {
|
|
57
|
+
directories.set(directory, Promise.resolve(listFiles(directory)).then(filesFromListing));
|
|
58
|
+
}
|
|
59
|
+
return directories.get(directory);
|
|
60
|
+
};
|
|
61
|
+
const missing = [];
|
|
62
|
+
await Promise.all(manifest.assets.map(async (asset) => {
|
|
63
|
+
const remotePath = path.posix.join("/", remoteRoot, manifest.assetsPrefix, asset.path);
|
|
64
|
+
const names = await namesInDirectory(path.posix.dirname(remotePath));
|
|
65
|
+
if (!names.has(path.posix.basename(remotePath))) missing.push(asset.path);
|
|
66
|
+
}));
|
|
67
|
+
return missing.sort();
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export async function uploadChangedAssets({
|
|
54
71
|
manifest,
|
|
55
72
|
state,
|
|
@@ -60,10 +77,12 @@ export async function uploadChangedAssets({
|
|
|
60
77
|
concurrency = 5,
|
|
61
78
|
prune = false,
|
|
62
79
|
deleteFile,
|
|
80
|
+
plan: preparedPlan,
|
|
63
81
|
}) {
|
|
64
|
-
const plan = await planAssetChanges({manifest, state, listFiles, remoteRoot, prune});
|
|
82
|
+
const plan = preparedPlan || await planAssetChanges({manifest, state, listFiles, remoteRoot, prune});
|
|
65
83
|
const assets = {...state.assets};
|
|
66
84
|
const uploaded = [];
|
|
85
|
+
const failures = [];
|
|
67
86
|
const skipped = plan.skip.map((asset) => asset.path);
|
|
68
87
|
for (const asset of plan.skip) assets[asset.path] = {...asset, immutable: true, managed: false};
|
|
69
88
|
let cursor = 0;
|
|
@@ -73,14 +92,29 @@ export async function uploadChangedAssets({
|
|
|
73
92
|
const asset = plan.upload[cursor++];
|
|
74
93
|
const localPath = path.join(root, ...(manifest.assetsDirectory || "public").split("/"), ...asset.path.split("/"));
|
|
75
94
|
const remotePath = path.posix.join("/", remoteRoot, manifest.assetsPrefix, asset.path);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
try {
|
|
96
|
+
const result = await uploadFile(remotePath, localPath);
|
|
97
|
+
assets[asset.path] = {...asset, url: result?.files?.[0]?.url, managed: true};
|
|
98
|
+
uploaded.push(asset.path);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
failures.push({path: asset.path, error});
|
|
101
|
+
}
|
|
79
102
|
}
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
const workerCount = Math.min(Math.max(1, concurrency), plan.upload.length || 1);
|
|
83
106
|
await Promise.all(Array.from({length: workerCount}, () => worker()));
|
|
107
|
+
if (failures.length) {
|
|
108
|
+
const reasons = [...new Set(failures.map((failure) => failure.error?.message).filter(Boolean))];
|
|
109
|
+
const error = new AggregateError(
|
|
110
|
+
failures.map((failure) => failure.error),
|
|
111
|
+
`Failed to upload ${failures.length} asset${failures.length === 1 ? "" : "s"}` +
|
|
112
|
+
(reasons.length ? `: ${reasons.join("; ")}` : "")
|
|
113
|
+
);
|
|
114
|
+
error.uploaded = uploaded.sort();
|
|
115
|
+
error.failed = failures.map((failure) => failure.path).sort();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
84
118
|
const removed = [];
|
|
85
119
|
if (prune && plan.remove.length) {
|
|
86
120
|
if (!deleteFile) throw new Error("CMS asset deletion is unavailable");
|
package/src/deploy/auth.mjs
CHANGED
|
@@ -89,8 +89,17 @@ async function responseJson(response, label) {
|
|
|
89
89
|
|
|
90
90
|
export async function authenticateAdfs({brand, login, password, fetchImpl = globalThis.fetch}) {
|
|
91
91
|
const apiBase = `https://b2capi.${brand}.ru/BackOffice/Adfs`;
|
|
92
|
+
let redirectResponse;
|
|
93
|
+
try {
|
|
94
|
+
redirectResponse = await fetchImpl(`${apiBase}/GetRedirectToAdfsUrl`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Backoffice is unavailable. Connect to the office network or VPN. ${error.message}`,
|
|
98
|
+
{cause: error}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
92
101
|
const redirectPayload = await responseJson(
|
|
93
|
-
|
|
102
|
+
redirectResponse,
|
|
94
103
|
"Getting ADFS login URL"
|
|
95
104
|
);
|
|
96
105
|
const loginUrl = redirectPayload?.result?.url;
|
|
@@ -139,11 +148,12 @@ export async function getBackofficeToken({
|
|
|
139
148
|
brand,
|
|
140
149
|
env = process.env,
|
|
141
150
|
fetchImpl = globalThis.fetch,
|
|
142
|
-
cachePath
|
|
151
|
+
cachePath,
|
|
143
152
|
credentialsProvider = promptCredentials,
|
|
144
153
|
now = Date.now(),
|
|
145
154
|
} = {}) {
|
|
146
155
|
if (env.B2C_BACKOFFICE_TOKEN) return env.B2C_BACKOFFICE_TOKEN;
|
|
156
|
+
cachePath ||= env.B2C_AUTH_CACHE || defaultCachePath();
|
|
147
157
|
const cache = readCache(cachePath);
|
|
148
158
|
const existing = cachedToken(cache, brand, now);
|
|
149
159
|
if (existing) return existing;
|
package/src/deploy/deploy.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {planAssetChanges, uploadChangedAssets} from "./assets.mjs";
|
|
1
|
+
import {findMissingRemoteAssets, planAssetChanges, uploadChangedAssets} from "./assets.mjs";
|
|
2
2
|
import {BackofficeClient} from "./backoffice-client.mjs";
|
|
3
3
|
import {readBuildManifest} from "./manifest.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
readDeploymentBinding,
|
|
6
|
+
readDeploymentState,
|
|
7
|
+
writeDeploymentBinding,
|
|
8
|
+
writeDeploymentState,
|
|
9
|
+
} from "./state.mjs";
|
|
5
10
|
import {resolveSections, summarizeChanges} from "./reconcile.mjs";
|
|
6
11
|
import {
|
|
7
12
|
createPagePlacement,
|
|
@@ -10,8 +15,10 @@ import {
|
|
|
10
15
|
publicationIssues,
|
|
11
16
|
publishPage,
|
|
12
17
|
selectExistingPagePlacement,
|
|
18
|
+
selectBoundPagePlacement,
|
|
13
19
|
selectNewPagePlacement,
|
|
14
20
|
validateSavedPlacement,
|
|
21
|
+
versionLabel,
|
|
15
22
|
} from "./page.mjs";
|
|
16
23
|
import {localSections, planWidgetChanges, remoteSections, syncWidgets} from "./widgets.mjs";
|
|
17
24
|
|
|
@@ -23,6 +30,61 @@ function logList(log, label, items) {
|
|
|
23
30
|
log(`[dry-run] ${label}: ${items.length ? items.join(", ") : "none"}`);
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
async function terminalDeployUi() {
|
|
34
|
+
const {confirm, intro, isCancel, log, note, outro, select, text} = await import("@clack/prompts");
|
|
35
|
+
const answer = async (promise) => {
|
|
36
|
+
const value = await promise;
|
|
37
|
+
if (isCancel(value)) throw new Error("Deploy cancelled");
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
const list = (prefix, items) => items.map((item) => `${prefix} ${item}`).join("\n");
|
|
41
|
+
return {
|
|
42
|
+
start: () => intro("Deploy"),
|
|
43
|
+
text: (options) => answer(text(options)),
|
|
44
|
+
select: (options) => answer(select(options)),
|
|
45
|
+
plan({page, version, add, update, remove, reorder, section}) {
|
|
46
|
+
note(`${page}\n${version}${section ? `\nSection: ${section}` : ""}`, "CMS target");
|
|
47
|
+
const groups = [];
|
|
48
|
+
if (add.length) groups.push(`Add · ${add.length}\n${list("+", add)}`);
|
|
49
|
+
if (update.length) groups.push(`Update · ${update.length}\n${list("~", update)}`);
|
|
50
|
+
if (remove.length) groups.push(`Remove · ${remove.length}\n${list("-", remove)}`);
|
|
51
|
+
if (reorder.length) groups.push(`Reorder · ${reorder.length}\n${list("↕", reorder)}`);
|
|
52
|
+
if (!groups.length) groups.push("No widget changes");
|
|
53
|
+
note(groups.join("\n\n"), "Widget plan");
|
|
54
|
+
},
|
|
55
|
+
confirmApply: () => answer(confirm({message: "Apply these changes?", initialValue: false})),
|
|
56
|
+
confirmPublish: () => answer(confirm({message: "Publish this page version?", initialValue: false})),
|
|
57
|
+
info: (message) => log.info(message),
|
|
58
|
+
complete({widgets, preview, backoffice, live}) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`Added ${widgets.added} · updated ${widgets.updated} · removed ${widgets.removed} · reordered ${widgets.reordered}`,
|
|
61
|
+
preview ? `\nPreview\n${preview}` : null,
|
|
62
|
+
backoffice ? `\nBackOffice\n${backoffice}` : null,
|
|
63
|
+
live ? `\nLive\n${live}` : null,
|
|
64
|
+
].filter(Boolean);
|
|
65
|
+
outro(lines.join("\n"));
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scopedWidgetPlan(plan, section) {
|
|
71
|
+
if (!section) return plan;
|
|
72
|
+
const matches = (title) => title.endsWith(` | ${section}`);
|
|
73
|
+
return {
|
|
74
|
+
add: plan.add.filter(matches),
|
|
75
|
+
update: plan.update.filter(matches),
|
|
76
|
+
remove: plan.remove.filter(matches),
|
|
77
|
+
reorder: plan.reorder.filter((item) => matches(item.title)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function backofficePageUrl(preset, pageContentId) {
|
|
82
|
+
if (!preset.backofficeBase || !pageContentId) return null;
|
|
83
|
+
const url = new URL("/ru-RU/pagemanagement/pages", preset.backofficeBase);
|
|
84
|
+
url.searchParams.set("pageContentId", pageContentId);
|
|
85
|
+
return String(url);
|
|
86
|
+
}
|
|
87
|
+
|
|
26
88
|
export async function planDeployment({
|
|
27
89
|
config,
|
|
28
90
|
preset,
|
|
@@ -135,58 +197,94 @@ export async function deployProject({
|
|
|
135
197
|
root = process.cwd(),
|
|
136
198
|
token,
|
|
137
199
|
client,
|
|
138
|
-
update = false,
|
|
139
|
-
relink = false,
|
|
140
|
-
pruneAssets = false,
|
|
141
|
-
publish = false,
|
|
142
200
|
prompts,
|
|
143
201
|
section,
|
|
144
202
|
strategy = "ask",
|
|
145
203
|
log = console.log,
|
|
204
|
+
ui,
|
|
146
205
|
}) {
|
|
147
206
|
const manifest = readBuildManifest(root);
|
|
148
207
|
const api = client || new BackofficeClient({brand: preset.id, token});
|
|
149
208
|
const previous = readDeploymentState(preset.id, manifest.folder, root);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
209
|
+
const binding = readDeploymentBinding(preset.id, manifest.folder, root);
|
|
210
|
+
if (section && !manifest.blocks.some((block) => block.path === `${section}.html`)) {
|
|
211
|
+
const available = manifest.blocks.map((block) => block.path.replace(/\.html$/, ""));
|
|
212
|
+
throw new Error(`Section ${JSON.stringify(section)} not found. Available sections: ${available.join(", ") || "none"}`);
|
|
213
|
+
}
|
|
214
|
+
if (!ui) {
|
|
215
|
+
if (prompts) {
|
|
216
|
+
ui = {
|
|
217
|
+
...prompts,
|
|
218
|
+
start() {},
|
|
219
|
+
plan() {},
|
|
220
|
+
confirmApply: prompts.confirmApply || (async () => true),
|
|
221
|
+
confirmPublish: prompts.confirmPublish || (async () => false),
|
|
222
|
+
info: (message) => log(`[deploy] ${message}`),
|
|
223
|
+
complete() {},
|
|
224
|
+
};
|
|
225
|
+
} else ui = await terminalDeployUi();
|
|
226
|
+
}
|
|
227
|
+
ui.start();
|
|
228
|
+
ui.info(`Checking ${manifest.assets.length} CDN asset${manifest.assets.length === 1 ? "" : "s"}`);
|
|
229
|
+
const missingAssets = await findMissingRemoteAssets({
|
|
230
|
+
manifest,
|
|
231
|
+
remoteRoot: new URL(preset.assetsBase).pathname,
|
|
232
|
+
listFiles: api.listFiles.bind(api),
|
|
233
|
+
});
|
|
234
|
+
if (missingAssets.length) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Deploy stopped: ${missingAssets.length} asset${missingAssets.length === 1 ? " is" : "s are"} missing on CDN: ` +
|
|
237
|
+
`${missingAssets.join(", ")}. Run b2c-landing-vite deploy assets first.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
154
241
|
let selectedPage = null;
|
|
242
|
+
let selectedDraft = null;
|
|
243
|
+
let nextBinding = binding;
|
|
155
244
|
let remoteBefore = null;
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
245
|
+
if (binding) {
|
|
246
|
+
try {
|
|
247
|
+
selectedPage = await selectBoundPagePlacement({client: api, binding, prompts: ui});
|
|
248
|
+
nextBinding = selectedPage.binding;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const stale = error.code === "NOT_FOUND" || error.status === 404 ||
|
|
251
|
+
/no versions|missing CMS page|area that no longer exists/i.test(error.message);
|
|
252
|
+
if (!stale) throw error;
|
|
253
|
+
ui.info("Saved CMS binding is no longer valid; select a page again");
|
|
254
|
+
nextBinding = null;
|
|
255
|
+
}
|
|
162
256
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
257
|
+
if (!selectedPage) {
|
|
258
|
+
const mode = await ui.select({
|
|
259
|
+
message: "CMS page",
|
|
260
|
+
initialValue: "create",
|
|
261
|
+
options: [
|
|
262
|
+
{label: "Create a new CMS page", value: "create"},
|
|
263
|
+
{label: "Use an existing CMS page", value: "existing"},
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
if (mode === "existing") {
|
|
267
|
+
selectedPage = await selectExistingPagePlacement({client: api, domain: preset.domain, prompts: ui});
|
|
268
|
+
nextBinding = selectedPage.binding;
|
|
269
|
+
} else {
|
|
270
|
+
selectedDraft = await selectNewPagePlacement({client: api, manifest, prompts: ui});
|
|
271
|
+
}
|
|
166
272
|
}
|
|
273
|
+
|
|
167
274
|
let reconciliation = null;
|
|
168
275
|
if (selectedPage) {
|
|
169
|
-
log(`[deploy] scope: ${section ? `section ${JSON.stringify(section)}` : "page"}`);
|
|
170
276
|
const local = localSections(manifest, root);
|
|
171
277
|
const remote = remoteSections(selectedPage.pageContent, selectedPage.placement, manifest.folder);
|
|
172
278
|
remoteBefore = remote;
|
|
173
279
|
const base = previous.cms?.sections || null;
|
|
174
280
|
let chosen = strategy;
|
|
175
|
-
|
|
281
|
+
reconciliation = resolveSections({local, remote, base, scope: section ? [section] : undefined, strategy: chosen === "ask" ? "merge" : chosen});
|
|
176
282
|
if (reconciliation.changes.length) {
|
|
177
283
|
const summary = summarizeChanges(reconciliation.changes);
|
|
178
|
-
for (const [kind, ids] of Object.entries(summary)) if (ids.length)
|
|
284
|
+
for (const [kind, ids] of Object.entries(summary)) if (ids.length) ui.info(`${kind}: ${ids.join(", ")}`);
|
|
179
285
|
}
|
|
180
286
|
if (reconciliation.unsafe.length && chosen === "ask") {
|
|
181
|
-
|
|
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({
|
|
287
|
+
chosen = await ui.select({
|
|
190
288
|
message: "Remote CMS changes detected",
|
|
191
289
|
options: [
|
|
192
290
|
{label: "Keep remote changes", value: "remote"},
|
|
@@ -197,9 +295,22 @@ export async function deployProject({
|
|
|
197
295
|
if (chosen === "cancel") throw new Error("Deploy cancelled; CMS was not modified");
|
|
198
296
|
reconciliation = resolveSections({local, remote, base, scope: section ? [section] : undefined, strategy: chosen});
|
|
199
297
|
} else if (reconciliation.unsafe.length) {
|
|
200
|
-
throw new Error(`Deploy stopped: remote changes
|
|
298
|
+
throw new Error(`Deploy stopped: unresolved remote changes (${reconciliation.unsafe.map((item) => item.id).join(", ")})`);
|
|
201
299
|
}
|
|
202
300
|
}
|
|
301
|
+
|
|
302
|
+
const placement = selectedPage?.placement || selectedDraft.placement;
|
|
303
|
+
const pageContent = selectedPage?.pageContent || {pageContents: []};
|
|
304
|
+
const widgetPlan = scopedWidgetPlan(planWidgetChanges({manifest, placement, pageContent, root}), section);
|
|
305
|
+
ui.plan({
|
|
306
|
+
page: nextBinding?.pageId || selectedDraft.draft.pageName,
|
|
307
|
+
version: selectedPage ? versionLabel(selectedPage.pageContent) : "New page",
|
|
308
|
+
...widgetPlan,
|
|
309
|
+
reorder: widgetPlan.reorder.map((item) => `${item.title} (${item.from} → ${item.to})`),
|
|
310
|
+
section,
|
|
311
|
+
});
|
|
312
|
+
if (!await ui.confirmApply()) throw new Error("Deploy cancelled; CMS was not modified");
|
|
313
|
+
|
|
203
314
|
// No remote mutation happens until the page state and the user's resolution are known.
|
|
204
315
|
if (selectedPage) {
|
|
205
316
|
const verified = await validateSavedPlacement({client: api, placement: selectedPage.placement});
|
|
@@ -207,63 +318,75 @@ export async function deployProject({
|
|
|
207
318
|
throw new Error("CMS page changed during deploy. Nothing was modified; run deploy again.");
|
|
208
319
|
}
|
|
209
320
|
}
|
|
210
|
-
log("[deploy] synchronizing CDN assets...");
|
|
211
|
-
const assets = await uploadChangedAssets({
|
|
212
|
-
manifest,
|
|
213
|
-
state: previous,
|
|
214
|
-
root,
|
|
215
|
-
remoteRoot: new URL(preset.assetsBase).pathname,
|
|
216
|
-
uploadFile: api.uploadFile.bind(api),
|
|
217
|
-
listFiles: api.listFiles?.bind(api),
|
|
218
|
-
prune: pruneAssets,
|
|
219
|
-
deleteFile: api.deleteFile?.bind(api),
|
|
220
|
-
});
|
|
221
321
|
const page = selectedPage
|
|
222
|
-
? await prepareExistingPage({client: api, placement: selectedPage.placement
|
|
223
|
-
: await createPagePlacement({client: api, manifest, prompts});
|
|
322
|
+
? await prepareExistingPage({client: api, placement: selectedPage.placement})
|
|
323
|
+
: await createPagePlacement({client: api, manifest, prompts: ui, selected: selectedDraft});
|
|
324
|
+
nextBinding = {...(page.binding || nextBinding), lastPageContentId: page.placement.pageContentId};
|
|
224
325
|
if (selectedPage && page.placement.pageContentId === selectedPage.placement.pageContentId &&
|
|
225
326
|
pageRevision(page.pageContent) !== pageRevision(selectedPage.pageContent)) {
|
|
226
327
|
throw new Error("CMS page changed during deploy. No widgets were modified; run deploy again.");
|
|
227
328
|
}
|
|
329
|
+
let sectionIds = null;
|
|
330
|
+
if (section) {
|
|
331
|
+
sectionIds = reconciliation?.sections?.[section] === remoteBefore?.[section] ? [] : [section];
|
|
332
|
+
} else if (reconciliation) {
|
|
333
|
+
sectionIds = [...new Set([
|
|
334
|
+
...Object.keys(remoteBefore || {}),
|
|
335
|
+
...Object.keys(reconciliation.sections || {}),
|
|
336
|
+
])].filter((id) => reconciliation.sections?.[id] !== remoteBefore?.[id]);
|
|
337
|
+
}
|
|
228
338
|
const widgets = await syncWidgets({
|
|
229
339
|
client: api,
|
|
230
340
|
manifest,
|
|
231
341
|
placement: page.placement,
|
|
232
342
|
pageContent: page.pageContent,
|
|
233
343
|
root,
|
|
234
|
-
sectionIds
|
|
235
|
-
...Object.keys(remoteBefore || {}),
|
|
236
|
-
...Object.keys(reconciliation.sections || {}),
|
|
237
|
-
])].filter((id) => reconciliation.sections?.[id] !== remoteBefore?.[id]) : null,
|
|
344
|
+
sectionIds,
|
|
238
345
|
});
|
|
346
|
+
const verifiedContent = await api.getContent(page.placement.pageContentId);
|
|
347
|
+
const remoteAfter = remoteSections(verifiedContent, page.placement, manifest.folder);
|
|
348
|
+
const storedSections = section ? {...(previous.cms?.sections || {})} : remoteAfter;
|
|
349
|
+
if (section) {
|
|
350
|
+
if (remoteAfter[section] === undefined) delete storedSections[section];
|
|
351
|
+
else storedSections[section] = remoteAfter[section];
|
|
352
|
+
}
|
|
239
353
|
const state = {
|
|
240
|
-
...
|
|
241
|
-
placement: page.placement,
|
|
354
|
+
...previous,
|
|
242
355
|
cms: {
|
|
243
356
|
verifiedAt: new Date().toISOString(),
|
|
244
|
-
sections:
|
|
357
|
+
sections: storedSections,
|
|
245
358
|
},
|
|
246
359
|
};
|
|
360
|
+
writeDeploymentState(preset.id, manifest.folder, state, root);
|
|
361
|
+
writeDeploymentBinding(preset.id, manifest.folder, nextBinding, root);
|
|
362
|
+
|
|
363
|
+
let finalPageContent = verifiedContent;
|
|
247
364
|
let published = false;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
365
|
+
if (await ui.confirmPublish()) {
|
|
366
|
+
const issues = publicationIssues(finalPageContent);
|
|
367
|
+
if (issues.length) {
|
|
368
|
+
ui.info(`Publication blocked; fill in ${issues.join(", ")} in Backoffice`);
|
|
369
|
+
} else {
|
|
370
|
+
await publishPage({client: api, pageContent: finalPageContent});
|
|
371
|
+
published = true;
|
|
372
|
+
finalPageContent = await api.getContent(page.placement.pageContentId);
|
|
373
|
+
}
|
|
254
374
|
}
|
|
255
|
-
writeDeploymentState(preset.id, manifest.folder, state, root);
|
|
256
375
|
const preview = finalPageContent.uniqueId
|
|
257
376
|
? `https://${preset.domain}/preview/${finalPageContent.uniqueId}/`
|
|
258
377
|
: null;
|
|
259
378
|
const ru = finalPageContent.pageContents?.find((content) => String(content.languageId) === "1049");
|
|
260
379
|
const live = published && ru?.perma ? `https://${preset.domain}/${ru.perma.replace(/^\/+/, "")}` : null;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
380
|
+
const backoffice = backofficePageUrl(preset, page.placement.pageContentId);
|
|
381
|
+
ui.complete({widgets, preview, backoffice, live});
|
|
382
|
+
return {
|
|
383
|
+
assets: {verified: manifest.assets.length, missing: []},
|
|
384
|
+
widgets,
|
|
385
|
+
binding: nextBinding,
|
|
386
|
+
placement: page.placement,
|
|
387
|
+
preview,
|
|
388
|
+
backoffice,
|
|
389
|
+
live,
|
|
390
|
+
published,
|
|
391
|
+
};
|
|
269
392
|
}
|
package/src/deploy/page.mjs
CHANGED
|
@@ -47,12 +47,35 @@ function pageLabel(page, domain) {
|
|
|
47
47
|
return `${page.pageTitle || page.name || page.pageId} — ${link}`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function versionLabel(content) {
|
|
50
|
+
export function versionLabel(content) {
|
|
51
51
|
const details = [content.statusName, content.version != null ? `v${content.version}` : null, content.author]
|
|
52
52
|
.filter(Boolean).join(", ");
|
|
53
53
|
return `${details || content.pageContentId}${content.modifiedTime ? ` — ${content.modifiedTime}` : ""}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function contentId(content) {
|
|
57
|
+
return content.pageContentId || content.contentId || content._id;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isPublishedVersion(content) {
|
|
61
|
+
return Number(content.statusId ?? content.status) === STATUS.published ||
|
|
62
|
+
String(content.statusName || "").toLowerCase() === "published";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function selectPageVersion({client, pageId, versions, prompts}) {
|
|
66
|
+
prompts ||= await defaultPrompts();
|
|
67
|
+
versions ||= await client.getContentVersions(pageId);
|
|
68
|
+
if (!versions?.length) throw new Error("The selected CMS page has no versions");
|
|
69
|
+
if (versions.length === 1) return client.getContent(contentId(versions[0]));
|
|
70
|
+
const published = versions.find(isPublishedVersion);
|
|
71
|
+
const pageContentId = await prompts.select({
|
|
72
|
+
message: "Source page version",
|
|
73
|
+
initialValue: contentId(published || versions[0]),
|
|
74
|
+
options: versions.map((content) => ({label: versionLabel(content), value: contentId(content)})),
|
|
75
|
+
});
|
|
76
|
+
return client.getContent(pageContentId);
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
async function defaultPrompts() {
|
|
57
80
|
const {isCancel, select, text} = await import("@clack/prompts");
|
|
58
81
|
const answer = async (promise) => {
|
|
@@ -148,13 +171,13 @@ export async function selectExistingPagePlacement({client, domain, prompts}) {
|
|
|
148
171
|
options: pages.map((page) => ({label: pageLabel(page, domain), value: page.pageId || page.id, page})),
|
|
149
172
|
});
|
|
150
173
|
const page = pages.find((candidate) => (candidate.pageId || candidate.id) === pageId);
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
const pageContent = await selectPageVersion({
|
|
175
|
+
client,
|
|
176
|
+
pageId,
|
|
177
|
+
versions: page.contents?.length ? page.contents : undefined,
|
|
178
|
+
prompts,
|
|
156
179
|
});
|
|
157
|
-
const
|
|
180
|
+
const pageContentId = contentId(pageContent);
|
|
158
181
|
const areas = suitableAreas(pageContent.layout?.schema || []);
|
|
159
182
|
if (!areas.length) throw new Error("The selected page layout has no HTML widget area");
|
|
160
183
|
const widgets = pageWidgets(pageContent);
|
|
@@ -176,11 +199,15 @@ export async function selectExistingPagePlacement({client, domain, prompts}) {
|
|
|
176
199
|
value: index,
|
|
177
200
|
})),
|
|
178
201
|
});
|
|
179
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
pageContent,
|
|
204
|
+
placement: {pageContentId, layoutAreaId, orderIndex},
|
|
205
|
+
binding: {pageId, applicationId, layoutAreaId, orderIndex, lastPageContentId: pageContentId},
|
|
206
|
+
};
|
|
180
207
|
}
|
|
181
208
|
|
|
182
209
|
export async function createPagePlacement(options) {
|
|
183
|
-
const selected = await selectNewPagePlacement(options);
|
|
210
|
+
const selected = options.selected || await selectNewPagePlacement(options);
|
|
184
211
|
const {client} = options;
|
|
185
212
|
const created = await client.createPage({
|
|
186
213
|
applicationId: selected.draft.applicationId,
|
|
@@ -188,9 +215,19 @@ export async function createPagePlacement(options) {
|
|
|
188
215
|
name: selected.draft.pageName,
|
|
189
216
|
});
|
|
190
217
|
if (!created?.pageContentId) throw new Error("Backoffice did not return pageContentId for the new page");
|
|
218
|
+
const pageContent = await client.getContent(created.pageContentId);
|
|
219
|
+
const pageId = created.pageId || pageContent.pageId;
|
|
220
|
+
if (!pageId) throw new Error("Backoffice did not return pageId for the new page");
|
|
191
221
|
return {
|
|
192
|
-
pageContent
|
|
222
|
+
pageContent,
|
|
193
223
|
placement: {...selected.placement, pageContentId: created.pageContentId},
|
|
224
|
+
binding: {
|
|
225
|
+
pageId,
|
|
226
|
+
applicationId: selected.draft.applicationId,
|
|
227
|
+
layoutAreaId: selected.placement.layoutAreaId,
|
|
228
|
+
orderIndex: selected.placement.orderIndex,
|
|
229
|
+
lastPageContentId: created.pageContentId,
|
|
230
|
+
},
|
|
194
231
|
};
|
|
195
232
|
}
|
|
196
233
|
|
|
@@ -234,18 +271,29 @@ export async function validateSavedPlacement({client, placement}) {
|
|
|
234
271
|
pageContent = await client.getContent(placement.pageContentId);
|
|
235
272
|
} catch (error) {
|
|
236
273
|
if (error.code === "NOT_FOUND" || error.status === 404) {
|
|
237
|
-
throw new Error("Saved deploy-state points to a missing CMS page version.
|
|
274
|
+
throw new Error("Saved deploy-state points to a missing CMS page version.", {cause: error});
|
|
238
275
|
}
|
|
239
276
|
throw error;
|
|
240
277
|
}
|
|
241
278
|
const schema = pageContent.layout?.schema;
|
|
242
279
|
if (Array.isArray(schema) && !schema.flatMap((row) => row.children || [])
|
|
243
280
|
.some((area) => (area.layoutAreaId || area.id) === placement.layoutAreaId)) {
|
|
244
|
-
throw new Error("Saved deploy-state points to an HTML area that no longer exists.
|
|
281
|
+
throw new Error("Saved deploy-state points to an HTML area that no longer exists.");
|
|
245
282
|
}
|
|
246
283
|
return pageContent;
|
|
247
284
|
}
|
|
248
285
|
|
|
286
|
+
export async function selectBoundPagePlacement({client, binding, prompts}) {
|
|
287
|
+
const pageContent = await selectPageVersion({client, pageId: binding.pageId, prompts});
|
|
288
|
+
const placement = {
|
|
289
|
+
pageContentId: contentId(pageContent),
|
|
290
|
+
layoutAreaId: binding.layoutAreaId,
|
|
291
|
+
orderIndex: binding.orderIndex,
|
|
292
|
+
};
|
|
293
|
+
await validateSavedPlacement({client, placement});
|
|
294
|
+
return {pageContent, placement, binding: {...binding, lastPageContentId: placement.pageContentId}};
|
|
295
|
+
}
|
|
296
|
+
|
|
249
297
|
export function pageRevision(pageContent) {
|
|
250
298
|
return JSON.stringify({
|
|
251
299
|
id: pageContent._id || pageContent.pageContentId,
|
package/src/deploy/state.mjs
CHANGED
|
@@ -2,13 +2,20 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
const STATE_VERSION = 1;
|
|
5
|
+
const BINDING_VERSION = 1;
|
|
5
6
|
|
|
6
7
|
export function deploymentStatePath(site, folder, root = process.cwd()) {
|
|
7
|
-
return path.join(root, ".b2c", "deploy", `${site}--${folder}.json`);
|
|
8
|
+
return path.join(root, ".b2c", "deploy", `${site}--${folder}.state.json`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deploymentBindingPath(site, folder, root = process.cwd()) {
|
|
12
|
+
return path.join(root, ".b2c", "deploy", `${site}--${folder}.binding.json`);
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
export function readDeploymentState(site, folder, root = process.cwd()) {
|
|
11
|
-
|
|
16
|
+
let filePath = deploymentStatePath(site, folder, root);
|
|
17
|
+
const legacyPath = path.join(root, ".b2c", "deploy", `${site}--${folder}.json`);
|
|
18
|
+
if (!fs.existsSync(filePath) && fs.existsSync(legacyPath)) filePath = legacyPath;
|
|
12
19
|
if (!fs.existsSync(filePath)) return {schemaVersion: STATE_VERSION, assets: {}};
|
|
13
20
|
const state = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
14
21
|
if (state.schemaVersion !== STATE_VERSION || !state.assets || typeof state.assets !== "object") {
|
|
@@ -17,6 +24,26 @@ export function readDeploymentState(site, folder, root = process.cwd()) {
|
|
|
17
24
|
return state;
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
export function readDeploymentBinding(site, folder, root = process.cwd()) {
|
|
28
|
+
const filePath = deploymentBindingPath(site, folder, root);
|
|
29
|
+
if (!fs.existsSync(filePath)) return null;
|
|
30
|
+
const binding = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
31
|
+
if (binding.schemaVersion !== BINDING_VERSION || !binding.pageId || !binding.layoutAreaId) {
|
|
32
|
+
throw new Error(`Invalid deployment binding: ${filePath}`);
|
|
33
|
+
}
|
|
34
|
+
return binding;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeDeploymentBinding(site, folder, binding, root = process.cwd()) {
|
|
38
|
+
const filePath = deploymentBindingPath(site, folder, root);
|
|
39
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
40
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
41
|
+
const next = {...binding, schemaVersion: BINDING_VERSION};
|
|
42
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
43
|
+
fs.renameSync(temporaryPath, filePath);
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
export function writeDeploymentState(site, folder, state, root = process.cwd()) {
|
|
21
48
|
const filePath = deploymentStatePath(site, folder, root);
|
|
22
49
|
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
package/src/presets/coral.mjs
CHANGED
package/src/presets/sunmar.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const packageJson = JSON.parse(
|
|
|
6
6
|
|
|
7
7
|
export const CURRENT_VERSION = packageJson.version;
|
|
8
8
|
export const PACKAGE_NAME = packageJson.name;
|
|
9
|
+
export const TEMPLATE_PACKAGE_NAME = "@coraltravelcenter/create-b2c-landing-vite";
|
|
9
10
|
|
|
10
11
|
function parseVersion(version) {
|
|
11
12
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version || "");
|
|
@@ -32,11 +33,12 @@ export function compareVersions(left, right) {
|
|
|
32
33
|
export async function checkForUpdate({
|
|
33
34
|
fetchImpl = globalThis.fetch,
|
|
34
35
|
currentVersion = CURRENT_VERSION,
|
|
36
|
+
packageName = PACKAGE_NAME,
|
|
35
37
|
timeoutMs = 3000,
|
|
36
38
|
} = {}) {
|
|
37
39
|
if (typeof fetchImpl !== "function") throw new Error("Fetch API is unavailable");
|
|
38
40
|
const response = await fetchImpl(
|
|
39
|
-
`https://registry.npmjs.org/${encodeURIComponent(
|
|
41
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
40
42
|
{headers: {accept: "application/json"}, signal: AbortSignal.timeout(timeoutMs)}
|
|
41
43
|
);
|
|
42
44
|
if (!response.ok) throw new Error(`npm registry responded with HTTP ${response.status}`);
|
|
@@ -49,6 +51,15 @@ export async function checkForUpdate({
|
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
export async function checkTemplateUpdate({templateVersion, ...options} = {}) {
|
|
55
|
+
const status = await checkForUpdate({
|
|
56
|
+
...options,
|
|
57
|
+
packageName: TEMPLATE_PACKAGE_NAME,
|
|
58
|
+
currentVersion: templateVersion || "0.0.0",
|
|
59
|
+
});
|
|
60
|
+
return {...status, legacy: !templateVersion};
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
export async function printUpdateStatus(options) {
|
|
53
64
|
try {
|
|
54
65
|
const status = await checkForUpdate(options);
|
|
@@ -65,15 +76,28 @@ export async function printUpdateStatus(options) {
|
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
export async function printUpdateNotice({
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
export async function printUpdateNotice({
|
|
80
|
+
log = console.warn,
|
|
81
|
+
timeoutMs = 1000,
|
|
82
|
+
templateVersion,
|
|
83
|
+
...options
|
|
84
|
+
} = {}) {
|
|
85
|
+
const [builder, template] = await Promise.all([
|
|
86
|
+
checkForUpdate({...options, timeoutMs}).catch(() => null),
|
|
87
|
+
checkTemplateUpdate({...options, timeoutMs, templateVersion}).catch(() => null),
|
|
88
|
+
]);
|
|
89
|
+
const builderUpdate = builder?.updateAvailable;
|
|
90
|
+
const templateUpdate = template?.updateAvailable;
|
|
91
|
+
if (!builderUpdate && !templateUpdate) return {builder, template};
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
log("Updates available:");
|
|
94
|
+
if (builderUpdate) {
|
|
95
|
+
log(`Builder: ${builder.currentVersion} → ${builder.latestVersion}`);
|
|
96
|
+
log(`npm install --global ${PACKAGE_NAME}@latest`);
|
|
97
|
+
}
|
|
98
|
+
if (templateUpdate) {
|
|
99
|
+
log(`Project template: ${template.legacy ? "legacy" : template.currentVersion} → ${template.latestVersion}`);
|
|
100
|
+
log(`npx ${TEMPLATE_PACKAGE_NAME}@latest migrate`);
|
|
78
101
|
}
|
|
102
|
+
return {builder, template};
|
|
79
103
|
}
|
package/src/vite/client.js
CHANGED
|
@@ -30,6 +30,9 @@ function waitForContainer() {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if (window.location.hostname === __B2C_TARGET_HOST__) {
|
|
34
|
+
const container = await waitForContainer();
|
|
35
|
+
mount(container, parts);
|
|
36
|
+
const devOrigin = new URL("/", import.meta.url).origin;
|
|
37
|
+
setupLocalCdnAssetRewrite({root: container, cdnBase: devOrigin});
|
|
38
|
+
}
|
package/src/vite/config.mjs
CHANGED
|
@@ -18,6 +18,18 @@ export function toViteFsUrl(filePath) {
|
|
|
18
18
|
const clientEntryUrl = toViteFsUrl(clientEntry);
|
|
19
19
|
let monkeyPlugin;
|
|
20
20
|
|
|
21
|
+
export function resolveDevServerPort(env = process.env) {
|
|
22
|
+
const explicitPort = env.B2C_PORT;
|
|
23
|
+
if (explicitPort === undefined || explicitPort === "") {
|
|
24
|
+
return {port: 5173, strictPort: false};
|
|
25
|
+
}
|
|
26
|
+
const port = Number(explicitPort);
|
|
27
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
28
|
+
throw new Error("B2C_PORT must be an integer between 1 and 65535");
|
|
29
|
+
}
|
|
30
|
+
return {port, strictPort: true};
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
async function loadMonkeyPlugin() {
|
|
22
34
|
if (monkeyPlugin) return monkeyPlugin;
|
|
23
35
|
|
|
@@ -36,6 +48,7 @@ export async function createViteConfig(root = process.cwd()) {
|
|
|
36
48
|
const preset = resolvePreset(config.site.preset);
|
|
37
49
|
const monkey = await loadMonkeyPlugin();
|
|
38
50
|
const shouldOpen = process.env.B2C_NO_OPEN !== "1";
|
|
51
|
+
const devPort = resolveDevServerPort();
|
|
39
52
|
const vue = resolveScriptStack(config.stack.script).vue
|
|
40
53
|
? await loadProjectVuePlugin(root)
|
|
41
54
|
: null;
|
|
@@ -45,8 +58,7 @@ export async function createViteConfig(root = process.cwd()) {
|
|
|
45
58
|
configFile: false,
|
|
46
59
|
server: {
|
|
47
60
|
host: "127.0.0.1",
|
|
48
|
-
|
|
49
|
-
strictPort: true,
|
|
61
|
+
...devPort,
|
|
50
62
|
cors: true,
|
|
51
63
|
open: shouldOpen ? "/__vite-plugin-monkey.install.user.js" : false,
|
|
52
64
|
fs: {allow: [root, builderRoot]},
|
|
@@ -59,13 +71,16 @@ export async function createViteConfig(root = process.cwd()) {
|
|
|
59
71
|
server: {open: shouldOpen, prefix: false},
|
|
60
72
|
userscript: {
|
|
61
73
|
name: config.project.name,
|
|
62
|
-
namespace:
|
|
74
|
+
namespace: `b2c-landing-builder:${preset.id}`,
|
|
63
75
|
match: [`https://${preset.domain}/monkey/*`],
|
|
64
76
|
grant: "none",
|
|
65
77
|
"run-at": "document-idle",
|
|
66
78
|
},
|
|
67
79
|
}),
|
|
68
80
|
].filter(Boolean),
|
|
81
|
+
define: {
|
|
82
|
+
__B2C_TARGET_HOST__: JSON.stringify(preset.domain),
|
|
83
|
+
},
|
|
69
84
|
};
|
|
70
85
|
}
|
|
71
86
|
|