@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 +17 -0
- package/npm-shrinkwrap.json +30 -17
- package/package.json +2 -1
- package/src/cli/index.mjs +14 -3
- package/src/config/validate-config.mjs +6 -0
- package/src/config/validate-project.mjs +38 -3
- package/src/lib/build-cms.mjs +11 -3
- package/src/lib/landingBlocksPlugin.mjs +4 -1
- package/src/lib/processMarkup.mjs +55 -2
- package/src/vite/config.mjs +73 -8
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
|
конфликтуют даже при одинаковом названии проекта.
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coraltravelcenter/b2c-landing-builder",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
1622
|
-
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.
|
|
1623
|
-
"integrity": "sha512-
|
|
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.
|
|
2638
|
-
"resolved": "https://registry.npmjs.org/open/-/open-11.0.
|
|
2639
|
-
"integrity": "sha512-
|
|
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.
|
|
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
|
|
2647
|
-
"wsl-utils": "^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
|
|
2748
|
-
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.
|
|
2749
|
-
"integrity": "sha512-
|
|
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.
|
|
3461
|
-
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.
|
|
3462
|
-
"integrity": "sha512-
|
|
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.
|
|
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")
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/build-cms.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
});
|
package/src/vite/config.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
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
|
}
|