@coraltravelcenter/b2c-landing-builder 2.12.2 → 2.14.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 CHANGED
@@ -112,6 +112,9 @@ export default {
112
112
  markup: "pug",
113
113
  styles: "less",
114
114
  },
115
+ blocks: {
116
+ static: ["html-only"],
117
+ },
115
118
  };
116
119
  ```
117
120
 
@@ -134,6 +137,14 @@ builder распознать legacy-шаблон и предложить миг
134
137
  Builder загружает Vue-плагин из проекта, поэтому Vue-зависимости не
135
138
  устанавливаются вместе с глобальным builder.
136
139
 
140
+ `blocks.static` явно перечисляет блоки без JavaScript initializer. Команда
141
+ `b2c-landing-vite check --strict` требует initializer или такую декларацию.
142
+ HTML-разметка поддерживает вложенные `<include src="./partial.html"></include>`.
143
+
144
+ При первом запуске dev-сервер сохраняет выбранный свободный порт в
145
+ `.b2c/dev-port`; следующие запуски используют его строго. Новый порт можно
146
+ закрепить командой `B2C_PORT=5175 npm run dev`.
147
+
137
148
  Перед подготовкой deploy-артефактов builder рекурсивно проверяет `public/`.
138
149
  Изображения JPG, JPEG и PNG конвертируются в WebP с качеством 80, а ссылки в
139
150
  CMS-файлах автоматически переключаются на новые имена. Исходные файлы в
@@ -151,6 +162,12 @@ Dev-сервер начинает с `127.0.0.1:5173` и автоматичес
151
162
  задан `B2C_PORT`, порт считается обязательным и занятое значение завершает
152
163
  команду ошибкой.
153
164
 
165
+ После запуска dev-сервера builder выводит рядом с локальным адресом Target URL
166
+ и открывает его отдельной вкладкой: `https://www.coral.ru/monkey/` или
167
+ `https://www.sunmar.ru/monkey/`. Существующая вкладка установки Tampermonkey
168
+ userscript открывается как раньше. `B2C_NO_OPEN=1` отключает автоматическое
169
+ открытие обеих вкладок, но Target URL остаётся в терминале.
170
+
154
171
  Userscript ограничен доменом выбранного бренда и дополнительно проверяет hostname
155
172
  перед монтированием. Namespace также содержит бренд, поэтому Coral и Sunmar не
156
173
  конфликтуют даже при одинаковом названии проекта.
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@coraltravelcenter/b2c-landing-builder",
3
- "version": "2.12.2",
3
+ "version": "2.14.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@coraltravelcenter/b2c-landing-builder",
9
- "version": "2.12.2",
9
+ "version": "2.14.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@babel/parser": "7.28.6",
13
13
  "@clack/prompts": "1.7.0",
14
+ "open": "11.0.2",
14
15
  "sharp": "0.35.3",
15
16
  "typograf": "7.7.0",
16
17
  "vite": "8.2.1",
@@ -1618,9 +1619,9 @@
1618
1619
  }
1619
1620
  },
1620
1621
  "node_modules/default-browser": {
1621
- "version": "5.5.0",
1622
- "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
1623
- "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
1622
+ "version": "5.5.1",
1623
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz",
1624
+ "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==",
1624
1625
  "license": "MIT",
1625
1626
  "dependencies": {
1626
1627
  "bundle-name": "^4.1.0",
@@ -2634,17 +2635,17 @@
2634
2635
  }
2635
2636
  },
2636
2637
  "node_modules/open": {
2637
- "version": "11.0.0",
2638
- "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
2639
- "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
2638
+ "version": "11.0.2",
2639
+ "resolved": "https://registry.npmjs.org/open/-/open-11.0.2.tgz",
2640
+ "integrity": "sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==",
2640
2641
  "license": "MIT",
2641
2642
  "dependencies": {
2642
- "default-browser": "^5.4.0",
2643
+ "default-browser": "^5.5.1",
2643
2644
  "define-lazy-prop": "^3.0.0",
2644
2645
  "is-in-ssh": "^1.0.0",
2645
2646
  "is-inside-container": "^1.0.0",
2646
- "powershell-utils": "^0.1.0",
2647
- "wsl-utils": "^0.3.0"
2647
+ "powershell-utils": "^0.2.1",
2648
+ "wsl-utils": "^1.0.0"
2648
2649
  },
2649
2650
  "engines": {
2650
2651
  "node": ">=20"
@@ -2744,9 +2745,9 @@
2744
2745
  }
2745
2746
  },
2746
2747
  "node_modules/powershell-utils": {
2747
- "version": "0.1.0",
2748
- "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
2749
- "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
2748
+ "version": "0.2.1",
2749
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
2750
+ "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
2750
2751
  "license": "MIT",
2751
2752
  "engines": {
2752
2753
  "node": ">=20"
@@ -3457,9 +3458,9 @@
3457
3458
  }
3458
3459
  },
3459
3460
  "node_modules/wsl-utils": {
3460
- "version": "0.3.1",
3461
- "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
3462
- "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
3461
+ "version": "1.0.0",
3462
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz",
3463
+ "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==",
3463
3464
  "license": "MIT",
3464
3465
  "dependencies": {
3465
3466
  "is-wsl": "^3.1.0",
@@ -3472,6 +3473,18 @@
3472
3473
  "url": "https://github.com/sponsors/sindresorhus"
3473
3474
  }
3474
3475
  },
3476
+ "node_modules/wsl-utils/node_modules/powershell-utils": {
3477
+ "version": "0.1.0",
3478
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
3479
+ "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
3480
+ "license": "MIT",
3481
+ "engines": {
3482
+ "node": ">=20"
3483
+ },
3484
+ "funding": {
3485
+ "url": "https://github.com/sponsors/sindresorhus"
3486
+ }
3487
+ },
3475
3488
  "node_modules/xxhashjs": {
3476
3489
  "version": "0.2.2",
3477
3490
  "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coraltravelcenter/b2c-landing-builder",
3
- "version": "2.12.2",
3
+ "version": "2.14.0",
4
4
  "description": "CLI and build toolkit for B2C landing projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "dependencies": {
33
33
  "@babel/parser": "7.28.6",
34
34
  "@clack/prompts": "1.7.0",
35
+ "open": "11.0.2",
35
36
  "sharp": "0.35.3",
36
37
  "typograf": "7.7.0",
37
38
  "vite": "8.2.1",
package/src/cli/index.mjs CHANGED
@@ -15,10 +15,10 @@ Commands:
15
15
  block:rename <old> <new> Rename a block
16
16
  update Show the manual update command`;
17
17
 
18
- async function check() {
18
+ async function check({strict = false} = {}) {
19
19
  const config = await loadConfig();
20
20
  const preset = resolvePreset(config.site.preset);
21
- const project = validateProject(config);
21
+ const project = validateProject(config, process.cwd(), {strict});
22
22
  console.log(`[check] project: ${config.project.name}`);
23
23
  console.log(`[check] schemaVersion: ${config.schemaVersion}`);
24
24
  console.log(`[check] site: ${preset.id} (${preset.domain})`);
@@ -28,6 +28,12 @@ async function check() {
28
28
  console.log(`[check] assets: not configured for ${preset.id} (production build unavailable)`);
29
29
  }
30
30
  console.log(`[check] blocks: ${project.blocks.length}`);
31
+ for (const block of project.blockDetails) {
32
+ console.log(
33
+ `[check] block ${block.key}: markup=yes styles=yes initializer=${block.initializer ? "yes" : "no"}` +
34
+ `${block.static ? " (static)" : ""}`
35
+ );
36
+ }
31
37
  }
32
38
 
33
39
  export async function runCli(args) {
@@ -38,7 +44,12 @@ export async function runCli(args) {
38
44
  return;
39
45
  }
40
46
 
41
- if (command === "check") return check();
47
+ if (command === "check") {
48
+ if (rest.some((argument) => argument !== "--strict")) {
49
+ throw new Error("Usage: b2c-landing-vite check [--strict]");
50
+ }
51
+ return check({strict: rest.includes("--strict")});
52
+ }
42
53
  if (command === "dev") {
43
54
  const config = await loadConfig();
44
55
  validateProject(config);
@@ -42,6 +42,12 @@ export function validateConfig(config) {
42
42
  if (!SITE_PRESETS.has(config.site?.preset)) {
43
43
  throw new Error(`Unknown site preset: ${JSON.stringify(config.site?.preset)}`);
44
44
  }
45
+ if (config.blocks?.static !== undefined && !Array.isArray(config.blocks.static)) {
46
+ throw new Error("blocks.static must be an array of block keys");
47
+ }
48
+ if (config.blocks?.static?.some((key) => typeof key !== "string" || !key)) {
49
+ throw new Error("blocks.static must contain non-empty block keys");
50
+ }
45
51
  for (const [key, allowed] of Object.entries(STACK_VALUES)) {
46
52
  if (!allowed.has(config.stack?.[key])) {
47
53
  throw new Error(`Unsupported stack.${key}: ${JSON.stringify(config.stack?.[key])}`);
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import {createRequire} from "node:module";
3
3
  import path from "node:path";
4
4
 
5
+ import {processMarkup} from "../lib/processMarkup.mjs";
5
6
  import {resolveScriptStack} from "./script-stack.mjs";
6
7
  import {hasDefaultInitializer} from "./validate-script-contract.mjs";
7
8
 
@@ -11,6 +12,7 @@ const STACK_DEPENDENCIES = {
11
12
  markup: {pug: ["pug"]},
12
13
  styles: {less: ["less"], scss: ["sass"]},
13
14
  };
15
+ const SCRIPT_EXTENSIONS = ["js", "ts"];
14
16
 
15
17
  function validateLocalDependencies(config, root) {
16
18
  const projectRequire = createRequire(path.resolve(root, "package.json"));
@@ -46,7 +48,7 @@ function readJson(filePath, label) {
46
48
  }
47
49
  }
48
50
 
49
- export function validateProject(config, root = process.cwd()) {
51
+ export function validateProject(config, root = process.cwd(), {strict = false} = {}) {
50
52
  const scriptStack = resolveScriptStack(config.stack.script);
51
53
  const orderPath = path.resolve(root, "src/order.json");
52
54
  const order = readJson(orderPath, "src/order.json");
@@ -58,6 +60,8 @@ export function validateProject(config, root = process.cwd()) {
58
60
  }
59
61
 
60
62
  const seen = new Set();
63
+ const staticBlocks = new Set(config.blocks?.static || []);
64
+ const blockDetails = [];
61
65
  for (const key of order.blocks) {
62
66
  if (typeof key !== "string" || !BLOCK_KEY_RE.test(key)) {
63
67
  throw new Error(`Invalid block key in src/order.json: ${JSON.stringify(key)}`);
@@ -69,6 +73,9 @@ export function validateProject(config, root = process.cwd()) {
69
73
  if (!fs.existsSync(markupPath)) {
70
74
  throw new Error(`Missing configured markup for block ${key}: src/markup/${key}.${config.stack.markup}`);
71
75
  }
76
+ if (config.stack.markup === "html") {
77
+ processMarkup(fs.readFileSync(markupPath, "utf8"), {filePath: markupPath});
78
+ }
72
79
 
73
80
  const stylePath = path.resolve(root, "src/styles", `${key}.${config.stack.styles}`);
74
81
  if (!fs.existsSync(stylePath)) {
@@ -76,7 +83,31 @@ export function validateProject(config, root = process.cwd()) {
76
83
  }
77
84
 
78
85
  const scriptPath = path.resolve(root, "src/scripts", `${key}.${scriptStack.extension}`);
79
- if (!fs.existsSync(scriptPath)) continue;
86
+ const conflictingScriptPaths = SCRIPT_EXTENSIONS
87
+ .filter((extension) => extension !== scriptStack.extension)
88
+ .map((extension) => path.resolve(root, "src/scripts", `${key}.${extension}`))
89
+ .filter((candidate) => fs.existsSync(candidate));
90
+ if (conflictingScriptPaths.length) {
91
+ const conflictingEntries = conflictingScriptPaths
92
+ .map((candidate) => path.relative(root, candidate))
93
+ .join(", ");
94
+ throw new Error(
95
+ `Script entry for block ${key} conflicts with stack.script=${JSON.stringify(config.stack.script)}: ` +
96
+ `${conflictingEntries}. Change stack.script or rename/remove the conflicting entry.`
97
+ );
98
+ }
99
+ const hasInitializer = fs.existsSync(scriptPath);
100
+ if (hasInitializer && staticBlocks.has(key)) {
101
+ throw new Error(`Block ${key} is declared static but has initializer src/scripts/${key}.${scriptStack.extension}`);
102
+ }
103
+ if (!hasInitializer && strict && !staticBlocks.has(key)) {
104
+ throw new Error(
105
+ `Block ${key} has no initializer. Add src/scripts/${key}.${scriptStack.extension} ` +
106
+ `or declare it in blocks.static.`
107
+ );
108
+ }
109
+ blockDetails.push({key, markup: true, styles: true, initializer: hasInitializer, static: staticBlocks.has(key)});
110
+ if (!hasInitializer) continue;
80
111
  const source = fs.readFileSync(scriptPath, "utf8");
81
112
  if (!hasDefaultInitializer({source, filePath: scriptPath, extension: scriptStack.extension, root})) {
82
113
  throw new Error(
@@ -85,6 +116,10 @@ export function validateProject(config, root = process.cwd()) {
85
116
  }
86
117
  }
87
118
 
119
+ for (const key of staticBlocks) {
120
+ if (!seen.has(key)) throw new Error(`Unknown block in blocks.static: ${key}`);
121
+ }
122
+
88
123
  validateLocalDependencies(config, root);
89
- return {folder: order.folder, blocks: [...order.blocks]};
124
+ return {folder: order.folder, blocks: [...order.blocks], blockDetails};
90
125
  }
@@ -131,7 +131,7 @@ async function buildBlock(key, stack) {
131
131
  const markup = stack.markup === "pug"
132
132
  ? renderPugFile(markupPath)
133
133
  : normalizeHtml(fs.readFileSync(markupPath, "utf8"));
134
- const html = processMarkup(markup);
134
+ const html = processMarkup(markup, {filePath: markupPath});
135
135
 
136
136
  const configuredCssPath = path.join(STYLES_DIR, `${key}.${stack.styles}`);
137
137
  const cssPath = existsFile(configuredCssPath) ? configuredCssPath : null;
@@ -155,7 +155,10 @@ async function buildBlock(key, stack) {
155
155
 
156
156
  if (js) parts.push(`<script>\n${js}\n</script>`);
157
157
 
158
- return parts.join("\n\n") + "\n";
158
+ return {
159
+ content: parts.join("\n\n") + "\n",
160
+ composition: {markup: Boolean(html), styles: Boolean(mergedCss), initializer: Boolean(js)},
161
+ };
159
162
  }
160
163
 
161
164
  // ---------- run ----------
@@ -178,8 +181,13 @@ export async function buildCms({config}) {
178
181
  }
179
182
 
180
183
  const outFile = path.join(OUT_DIR, `${key}.html`);
181
- fs.writeFileSync(outFile, result);
184
+ fs.writeFileSync(outFile, result.content);
182
185
  console.log(`[CMS] wrote @CMS/${key}.html`);
186
+ console.log(
187
+ `[CMS] block ${key}: markup=${result.composition.markup ? "yes" : "no"} ` +
188
+ `styles=${result.composition.styles ? "yes" : "no"} ` +
189
+ `initializer=${result.composition.initializer ? "yes" : "no"}`
190
+ );
183
191
  }
184
192
 
185
193
  console.log("[CMS] done");
@@ -100,7 +100,10 @@ export function landingBlocksPlugin({root = process.cwd(), stack} = {}) {
100
100
  const source = stack.markup === "pug"
101
101
  ? renderPugFile(markupPath)
102
102
  : normalizeHtml(fs.readFileSync(markupPath, "utf8"));
103
- const html = processMarkup(source);
103
+ const html = processMarkup(source, {
104
+ filePath: markupPath,
105
+ onInclude: (includePath) => this.addWatchFile(includePath),
106
+ });
104
107
  entries.push(`{ key: ${JSON.stringify(key)}, html: ${JSON.stringify(html)}, init: ${init} }`);
105
108
  });
106
109
 
@@ -1,3 +1,6 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
1
4
  import Typograf from "typograf";
2
5
 
3
6
  const typograf = new Typograf({
@@ -5,10 +8,60 @@ const typograf = new Typograf({
5
8
  });
6
9
 
7
10
  const PROTECTED_TAGS = /<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1>/gi;
11
+ const HTML_INCLUDE = /<include\s+[^>]*\bsrc\s*=\s*(["'])([^"']+)\1[^>]*>(?:\s*<\/include\s*>)?/gi;
12
+
13
+ function protectContents(source) {
14
+ const contents = [];
15
+ const protectedSource = source.replace(PROTECTED_TAGS, (fragment) => {
16
+ const index = contents.push(fragment) - 1;
17
+ return `<b2c-markup-protected data-index="${index}"></b2c-markup-protected>`;
18
+ });
19
+ return {
20
+ source: protectedSource,
21
+ restore(value) {
22
+ return value.replace(
23
+ /<b2c-markup-protected data-index="(\d+)"><\/b2c-markup-protected>/g,
24
+ (_, index) => contents[Number(index)]
25
+ );
26
+ },
27
+ };
28
+ }
29
+
30
+ function expandHtmlIncludes(source, {filePath, onInclude, chain = []}) {
31
+ const absolutePath = path.resolve(filePath);
32
+ if (chain.includes(absolutePath)) {
33
+ const cycle = [...chain, absolutePath].map((item) => path.relative(process.cwd(), item)).join(" -> ");
34
+ throw new Error(`Recursive HTML include detected: ${cycle}`);
35
+ }
36
+
37
+ const protectedMarkup = protectContents(source);
38
+ const expanded = protectedMarkup.source.replace(HTML_INCLUDE, (_match, _quote, includeSource) => {
39
+ const includePath = path.resolve(path.dirname(absolutePath), includeSource);
40
+ let includeMarkup;
41
+ try {
42
+ includeMarkup = fs.readFileSync(includePath, "utf8");
43
+ } catch (error) {
44
+ if (error.code === "ENOENT") {
45
+ throw new Error(`HTML include not found: ${includePath} (included from ${absolutePath})`, {cause: error});
46
+ }
47
+ throw error;
48
+ }
49
+ onInclude?.(includePath);
50
+ return expandHtmlIncludes(includeMarkup, {
51
+ filePath: includePath,
52
+ onInclude,
53
+ chain: [...chain, absolutePath],
54
+ });
55
+ });
56
+ return protectedMarkup.restore(expanded);
57
+ }
8
58
 
9
- export function processMarkup(html) {
59
+ export function processMarkup(html, {filePath, onInclude} = {}) {
10
60
  const protectedContents = [];
11
- const source = String(html || "").replace(PROTECTED_TAGS, (fragment) => {
61
+ const markup = filePath
62
+ ? expandHtmlIncludes(String(html || ""), {filePath, onInclude})
63
+ : String(html || "");
64
+ const source = markup.replace(PROTECTED_TAGS, (fragment) => {
12
65
  const index = protectedContents.push(fragment) - 1;
13
66
  return `<b2c-typograf-protected data-index="${index}"></b2c-typograf-protected>`;
14
67
  });
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
2
3
  import {fileURLToPath} from "node:url";
3
4
 
4
5
  import {createServer} from "vite";
6
+ import open from "open";
5
7
 
6
8
  import {loadConfig} from "../config/load-config.mjs";
7
9
  import {resolvePreset} from "../config/resolve-preset.mjs";
@@ -18,10 +20,27 @@ export function toViteFsUrl(filePath) {
18
20
  const clientEntryUrl = toViteFsUrl(clientEntry);
19
21
  let monkeyPlugin;
20
22
 
21
- export function resolveDevServerPort(env = process.env) {
23
+ const DEFAULT_DEV_PORT = 5173;
24
+
25
+ function devPortPath(root) {
26
+ return path.join(root, ".b2c", "dev-port");
27
+ }
28
+
29
+ export function resolveDevServerPort(env = process.env, root = process.cwd()) {
22
30
  const explicitPort = env.B2C_PORT;
23
31
  if (explicitPort === undefined || explicitPort === "") {
24
- return {port: 5173, strictPort: false};
32
+ let savedPort;
33
+ try {
34
+ savedPort = fs.readFileSync(devPortPath(root), "utf8").trim();
35
+ } catch (error) {
36
+ if (error.code !== "ENOENT") throw error;
37
+ }
38
+ if (!savedPort) return {port: DEFAULT_DEV_PORT, strictPort: false};
39
+ const port = Number(savedPort);
40
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
41
+ throw new Error(`Invalid saved dev port in ${devPortPath(root)}. Remove the file or set B2C_PORT.`);
42
+ }
43
+ return {port, strictPort: true};
25
44
  }
26
45
  const port = Number(explicitPort);
27
46
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
@@ -30,6 +49,34 @@ export function resolveDevServerPort(env = process.env) {
30
49
  return {port, strictPort: true};
31
50
  }
32
51
 
52
+ export function saveDevServerPort(root, port) {
53
+ const filePath = devPortPath(root);
54
+ fs.mkdirSync(path.dirname(filePath), {recursive: true});
55
+ fs.writeFileSync(filePath, `${port}\n`);
56
+ }
57
+
58
+ export function createTargetUrl(preset) {
59
+ return `https://${preset.domain}/monkey/`;
60
+ }
61
+
62
+ export async function presentTargetPage({
63
+ preset,
64
+ shouldOpen = true,
65
+ opener = open,
66
+ log = console.log,
67
+ } = {}) {
68
+ const targetUrl = createTargetUrl(preset);
69
+ log(` ➜ Target: ${targetUrl}`);
70
+ if (!shouldOpen) return targetUrl;
71
+
72
+ try {
73
+ await opener(targetUrl);
74
+ } catch (error) {
75
+ log(` ⚠ Could not open Target automatically: ${error.message}`);
76
+ }
77
+ return targetUrl;
78
+ }
79
+
33
80
  async function loadMonkeyPlugin() {
34
81
  if (monkeyPlugin) return monkeyPlugin;
35
82
 
@@ -43,12 +90,12 @@ async function loadMonkeyPlugin() {
43
90
  }
44
91
  }
45
92
 
46
- export async function createViteConfig(root = process.cwd()) {
47
- const config = await loadConfig(root);
48
- const preset = resolvePreset(config.site.preset);
93
+ export async function createViteConfig(root = process.cwd(), options = {}) {
94
+ const config = options.config || await loadConfig(root);
95
+ const preset = options.preset || resolvePreset(config.site.preset);
49
96
  const monkey = await loadMonkeyPlugin();
50
97
  const shouldOpen = process.env.B2C_NO_OPEN !== "1";
51
- const devPort = resolveDevServerPort();
98
+ const devPort = resolveDevServerPort(process.env, root);
52
99
  const vue = resolveScriptStack(config.stack.script).vue
53
100
  ? await loadProjectVuePlugin(root)
54
101
  : null;
@@ -85,8 +132,26 @@ export async function createViteConfig(root = process.cwd()) {
85
132
  }
86
133
 
87
134
  export async function startDevServer(root = process.cwd()) {
88
- const server = await createServer(await createViteConfig(root));
89
- await server.listen();
135
+ const config = await loadConfig(root);
136
+ const preset = resolvePreset(config.site.preset);
137
+ const shouldOpen = process.env.B2C_NO_OPEN !== "1";
138
+ const viteConfig = await createViteConfig(root, {config, preset});
139
+ const server = await createServer(viteConfig);
140
+ try {
141
+ await server.listen();
142
+ } catch (error) {
143
+ if (viteConfig.server.strictPort && /port .*already in use/i.test(error.message)) {
144
+ throw new Error(
145
+ `${error.message}. Stop the process using this project's port or choose and save another one with ` +
146
+ `B2C_PORT=<free-port> npm run dev.`,
147
+ {cause: error}
148
+ );
149
+ }
150
+ throw error;
151
+ }
152
+ const address = server.httpServer?.address();
153
+ if (address && typeof address === "object") saveDevServerPort(root, address.port);
90
154
  server.printUrls();
155
+ await presentTargetPage({preset, shouldOpen});
91
156
  return server;
92
157
  }