@coraltravelcenter/b2c-landing-builder 2.13.0 → 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 +11 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -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 +43 -5
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-файлах автоматически переключаются на новые имена. Исходные файлы в
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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",
|
package/package.json
CHANGED
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,4 +1,5 @@
|
|
|
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";
|
|
@@ -19,10 +20,27 @@ export function toViteFsUrl(filePath) {
|
|
|
19
20
|
const clientEntryUrl = toViteFsUrl(clientEntry);
|
|
20
21
|
let monkeyPlugin;
|
|
21
22
|
|
|
22
|
-
|
|
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()) {
|
|
23
30
|
const explicitPort = env.B2C_PORT;
|
|
24
31
|
if (explicitPort === undefined || explicitPort === "") {
|
|
25
|
-
|
|
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};
|
|
26
44
|
}
|
|
27
45
|
const port = Number(explicitPort);
|
|
28
46
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
@@ -31,6 +49,12 @@ export function resolveDevServerPort(env = process.env) {
|
|
|
31
49
|
return {port, strictPort: true};
|
|
32
50
|
}
|
|
33
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
|
+
|
|
34
58
|
export function createTargetUrl(preset) {
|
|
35
59
|
return `https://${preset.domain}/monkey/`;
|
|
36
60
|
}
|
|
@@ -71,7 +95,7 @@ export async function createViteConfig(root = process.cwd(), options = {}) {
|
|
|
71
95
|
const preset = options.preset || resolvePreset(config.site.preset);
|
|
72
96
|
const monkey = await loadMonkeyPlugin();
|
|
73
97
|
const shouldOpen = process.env.B2C_NO_OPEN !== "1";
|
|
74
|
-
const devPort = resolveDevServerPort();
|
|
98
|
+
const devPort = resolveDevServerPort(process.env, root);
|
|
75
99
|
const vue = resolveScriptStack(config.stack.script).vue
|
|
76
100
|
? await loadProjectVuePlugin(root)
|
|
77
101
|
: null;
|
|
@@ -111,8 +135,22 @@ export async function startDevServer(root = process.cwd()) {
|
|
|
111
135
|
const config = await loadConfig(root);
|
|
112
136
|
const preset = resolvePreset(config.site.preset);
|
|
113
137
|
const shouldOpen = process.env.B2C_NO_OPEN !== "1";
|
|
114
|
-
const
|
|
115
|
-
await
|
|
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);
|
|
116
154
|
server.printUrls();
|
|
117
155
|
await presentTargetPage({preset, shouldOpen});
|
|
118
156
|
return server;
|