@coraltravelcenter/b2c-landing-builder 2.0.0 → 2.1.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 +15 -7
- package/package.json +6 -5
- package/src/blocks/add.mjs +1 -1
- package/src/cli/index.mjs +3 -1
- package/src/config/validate-project.mjs +24 -0
- package/src/lib/build-cms.mjs +8 -8
- package/src/lib/landingBlocksPlugin.mjs +32 -14
- package/src/lib/loadProjectVuePlugin.mjs +10 -0
- package/src/lib/pugMarkup.mjs +11 -2
- package/src/lib/rewriteAssetsBuild.mjs +8 -111
- package/src/lib/rewriteAssetsDev.js +50 -41
- package/src/lib/rewriteImageAssets.mjs +85 -0
- package/src/vite/client.js +2 -15
- package/src/vite/config.mjs +5 -4
- package/src/vite/dependencies.mjs +0 -7
package/README.md
CHANGED
|
@@ -81,6 +81,11 @@ export default {
|
|
|
81
81
|
- стили: `css`, `scss`, `less`;
|
|
82
82
|
- сайты: `coral`, `sunmar`.
|
|
83
83
|
|
|
84
|
+
Зависимости выбранного стека устанавливаются локально в проект: `vue` и
|
|
85
|
+
`@vitejs/plugin-vue` для Vue, `pug` для Pug, `less` для Less и `sass` для SCSS.
|
|
86
|
+
Builder загружает Vue-плагин из проекта, поэтому Vue-зависимости не
|
|
87
|
+
устанавливаются вместе с глобальным builder.
|
|
88
|
+
|
|
84
89
|
Monkey URL, контейнер и CDN prefix не задаются вручную:
|
|
85
90
|
|
|
86
91
|
- URL: `https://<домен-пресета>/monkey/*`;
|
|
@@ -129,20 +134,21 @@ package.json
|
|
|
129
134
|
инициализации по умолчанию:
|
|
130
135
|
|
|
131
136
|
```js
|
|
132
|
-
export default function hero(
|
|
133
|
-
const block =
|
|
137
|
+
export default function hero() {
|
|
138
|
+
const block = document.querySelector(".hero");
|
|
134
139
|
if (!block) return;
|
|
135
140
|
|
|
136
141
|
// Инициализация блока.
|
|
137
142
|
}
|
|
138
143
|
```
|
|
139
144
|
|
|
140
|
-
Builder вставляет разметку, затем вызывает
|
|
141
|
-
|
|
145
|
+
Builder вставляет разметку, затем вызывает функцию без аргументов. Скрипт блока
|
|
146
|
+
сам находит свою разметку в `document`. Этот контракт одинаков для dev и CMS
|
|
147
|
+
build; дополнительные контейнеры builder не создаёт. Блок без JS допустим.
|
|
142
148
|
|
|
143
|
-
##
|
|
149
|
+
## Изображения
|
|
144
150
|
|
|
145
|
-
|
|
151
|
+
Изображения хранятся в `public/` и используются от корня:
|
|
146
152
|
|
|
147
153
|
```html
|
|
148
154
|
<img src="/hero.webp" alt="">
|
|
@@ -155,7 +161,9 @@ dev, HMR и CMS build. Блок без JS допустим.
|
|
|
155
161
|
```
|
|
156
162
|
|
|
157
163
|
В dev пути направляются на `http://127.0.0.1:5173`. При production build они
|
|
158
|
-
преобразуются в `<assetsBase>/landing-pages/<project.name>/...`.
|
|
164
|
+
преобразуются в `<assetsBase>/landing-pages/<project.name>/...`. Builder
|
|
165
|
+
переписывает только изображения `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif` и
|
|
166
|
+
`svg`; остальные root-relative ресурсы не изменяются.
|
|
159
167
|
|
|
160
168
|
## Совместимость
|
|
161
169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coraltravelcenter/b2c-landing-builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "CLI and build toolkit for B2C landing projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"pack:check": "npm pack --dry-run"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"typograf": "^7.7.0",
|
|
32
|
+
"vite": "^8.1.4",
|
|
33
|
+
"vite-plugin-monkey": "^8.0.6"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
31
36
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
32
37
|
"less": "^4.6.7",
|
|
33
38
|
"pug": "^3.0.4",
|
|
34
|
-
"sass": "^1.101.0",
|
|
35
|
-
"typograf": "^7.7.0",
|
|
36
|
-
"vite": "^8.1.4",
|
|
37
|
-
"vite-plugin-monkey": "^8.0.6",
|
|
38
39
|
"vue": "^3.5.39"
|
|
39
40
|
},
|
|
40
41
|
"repository": {
|
package/src/blocks/add.mjs
CHANGED
|
@@ -13,7 +13,7 @@ function styleTemplate(key) {
|
|
|
13
13
|
|
|
14
14
|
function scriptTemplate(key) {
|
|
15
15
|
const functionName = key.replace(/-+/g, "_");
|
|
16
|
-
return `export default function ${functionName}(
|
|
16
|
+
return `export default function ${functionName}() {\n const block = document.querySelector(".${key}");\n if (!block) return;\n}\n`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function addBlock(key, config, root = process.cwd()) {
|
package/src/cli/index.mjs
CHANGED
|
@@ -37,12 +37,14 @@ export async function runCli(args) {
|
|
|
37
37
|
|
|
38
38
|
if (command === "check") return check();
|
|
39
39
|
if (command === "dev") {
|
|
40
|
-
await loadConfig();
|
|
40
|
+
const config = await loadConfig();
|
|
41
|
+
validateProject(config);
|
|
41
42
|
const {startDevServer} = await import("../vite/config.mjs");
|
|
42
43
|
return startDevServer();
|
|
43
44
|
}
|
|
44
45
|
if (command === "build") {
|
|
45
46
|
const config = await loadConfig();
|
|
47
|
+
validateProject(config);
|
|
46
48
|
const preset = resolvePreset(config.site.preset, {requireAssets: true});
|
|
47
49
|
const {buildCms} = await import("../lib/build-cms.mjs");
|
|
48
50
|
await buildCms({config});
|
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import {createRequire} from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
const BLOCK_KEY_RE = /^[a-z0-9][a-z0-9-_]*$/i;
|
|
5
6
|
const DEFAULT_EXPORT_RE = /\bexport\s+default\s+(?:async\s+)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>|[A-Za-z_$][\w$]*\s*;?)/;
|
|
7
|
+
const STACK_DEPENDENCIES = {
|
|
8
|
+
script: {vue: ["vue", "@vitejs/plugin-vue"]},
|
|
9
|
+
markup: {pug: ["pug"]},
|
|
10
|
+
styles: {less: ["less"], scss: ["sass"]},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function validateLocalDependencies(config, root) {
|
|
14
|
+
const projectRequire = createRequire(path.resolve(root, "package.json"));
|
|
15
|
+
const dependencies = Object.entries(STACK_DEPENDENCIES)
|
|
16
|
+
.flatMap(([section, values]) => values[config.stack[section]] || []);
|
|
17
|
+
|
|
18
|
+
for (const dependency of dependencies) {
|
|
19
|
+
try {
|
|
20
|
+
projectRequire.resolve(dependency);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Missing local dependency ${JSON.stringify(dependency)} for the configured stack. Run npm install in ${root}`,
|
|
24
|
+
{cause: error}
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
6
29
|
|
|
7
30
|
function readJson(filePath, label) {
|
|
8
31
|
let source;
|
|
@@ -48,5 +71,6 @@ export function validateProject(config, root = process.cwd()) {
|
|
|
48
71
|
}
|
|
49
72
|
}
|
|
50
73
|
|
|
74
|
+
validateLocalDependencies(config, root);
|
|
51
75
|
return {blocks: [...order.blocks]};
|
|
52
76
|
}
|
package/src/lib/build-cms.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import {build} from "vite";
|
|
4
|
-
import vue from "@vitejs/plugin-vue";
|
|
5
4
|
|
|
6
5
|
import {cleanDir, ensureDir, existsFile, normalizeHtml, r, readBlocks} from "./_utils.mjs";
|
|
6
|
+
import {loadProjectVuePlugin} from "./loadProjectVuePlugin.mjs";
|
|
7
7
|
import {processMarkup} from "./processMarkup.mjs";
|
|
8
8
|
import {renderPugFile} from "./pugMarkup.mjs";
|
|
9
|
-
import {builderAliases} from "../vite/dependencies.mjs";
|
|
10
9
|
|
|
11
10
|
const ORDER_FILE = r("src/order.json");
|
|
12
11
|
|
|
@@ -38,7 +37,6 @@ async function bundleCssInline(absCssPath) {
|
|
|
38
37
|
const res = await build({
|
|
39
38
|
configFile: false,
|
|
40
39
|
logLevel: "silent",
|
|
41
|
-
resolve: {alias: builderAliases},
|
|
42
40
|
plugins: [virtualCss],
|
|
43
41
|
build: {
|
|
44
42
|
write: false,
|
|
@@ -62,7 +60,7 @@ async function bundleCssInline(absCssPath) {
|
|
|
62
60
|
|
|
63
61
|
// ---------- JS (+ CSS extracted from Vue/JS imports) ----------
|
|
64
62
|
|
|
65
|
-
async function bundleJsInline(absJsPath) {
|
|
63
|
+
async function bundleJsInline(absJsPath, stack) {
|
|
66
64
|
const rel = "/" + path.relative(process.cwd(), absJsPath).replaceAll("\\", "/");
|
|
67
65
|
|
|
68
66
|
const V_ID = "virtual:cms-js";
|
|
@@ -85,15 +83,17 @@ try { if (typeof init === "function") init(); } catch (e) { console.warn(e); }
|
|
|
85
83
|
},
|
|
86
84
|
};
|
|
87
85
|
|
|
86
|
+
const vue = stack.script === "vue"
|
|
87
|
+
? await loadProjectVuePlugin(process.cwd())
|
|
88
|
+
: null;
|
|
88
89
|
const res = await build({
|
|
89
90
|
configFile: false,
|
|
90
91
|
logLevel: "silent",
|
|
91
|
-
|
|
92
|
-
plugins: [vue(), virtualJs],
|
|
92
|
+
plugins: [vue && vue(), virtualJs].filter(Boolean),
|
|
93
93
|
build: {
|
|
94
94
|
write: false,
|
|
95
95
|
minify: "oxc",
|
|
96
|
-
cssCodeSplit:
|
|
96
|
+
cssCodeSplit: false,
|
|
97
97
|
rollupOptions: {
|
|
98
98
|
input: V_ID,
|
|
99
99
|
output: {format: "iife"},
|
|
@@ -139,7 +139,7 @@ async function buildBlock(key, stack) {
|
|
|
139
139
|
let js = "";
|
|
140
140
|
let jsCss = "";
|
|
141
141
|
if (existsFile(jsPath)) {
|
|
142
|
-
const out = await bundleJsInline(jsPath);
|
|
142
|
+
const out = await bundleJsInline(jsPath, stack);
|
|
143
143
|
js = out.js || "";
|
|
144
144
|
jsCss = out.css || "";
|
|
145
145
|
}
|
|
@@ -13,26 +13,47 @@ function toVitePath(filePath) {
|
|
|
13
13
|
return "/" + path.relative(process.cwd(), filePath).replaceAll("\\", "/");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function isInside(directory, filePath) {
|
|
17
|
+
const relative = path.relative(directory, filePath);
|
|
18
|
+
return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export function landingBlocksPlugin({root = process.cwd(), stack} = {}) {
|
|
17
22
|
const orderFile = path.resolve(root, "src/order.json");
|
|
18
23
|
const markupDir = path.resolve(root, "src/markup");
|
|
19
24
|
const stylesDir = path.resolve(root, "src/styles");
|
|
20
25
|
const scriptsDir = path.resolve(root, "src/scripts");
|
|
21
26
|
const pugConfigFile = getPugConfigFile();
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const affectsBlocks = (filePath) =>
|
|
27
|
+
const requiresFullReload = (filePath) =>
|
|
25
28
|
filePath === orderFile ||
|
|
26
29
|
filePath === pugConfigFile ||
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
isInside(markupDir, filePath) ||
|
|
31
|
+
isInside(scriptsDir, filePath);
|
|
32
|
+
|
|
33
|
+
const reloadPage = (server) => {
|
|
34
|
+
const virtualModule = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);
|
|
35
|
+
if (virtualModule) server.moduleGraph.invalidateModule(virtualModule);
|
|
36
|
+
server.ws.send({type: "full-reload", path: "*"});
|
|
37
|
+
};
|
|
31
38
|
|
|
32
39
|
return {
|
|
33
40
|
name: "landing-blocks",
|
|
34
41
|
enforce: "pre",
|
|
35
42
|
|
|
43
|
+
configureServer(server) {
|
|
44
|
+
const reloadAddedOrRemovedFile = (filePath) => {
|
|
45
|
+
if (requiresFullReload(filePath)) reloadPage(server);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
server.watcher.on("add", reloadAddedOrRemovedFile);
|
|
49
|
+
server.watcher.on("unlink", reloadAddedOrRemovedFile);
|
|
50
|
+
|
|
51
|
+
server.httpServer?.once("close", () => {
|
|
52
|
+
server.watcher.off("add", reloadAddedOrRemovedFile);
|
|
53
|
+
server.watcher.off("unlink", reloadAddedOrRemovedFile);
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
|
|
36
57
|
resolveId(id) {
|
|
37
58
|
if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID;
|
|
38
59
|
},
|
|
@@ -84,14 +105,11 @@ export function landingBlocksPlugin({root = process.cwd(), stack} = {}) {
|
|
|
84
105
|
return `${imports.join("\n")}\n\nexport default [${entries.join(", ")}];\n`;
|
|
85
106
|
},
|
|
86
107
|
|
|
87
|
-
handleHotUpdate({file, server
|
|
88
|
-
if (!
|
|
89
|
-
|
|
90
|
-
const virtualModule = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);
|
|
91
|
-
if (!virtualModule) return;
|
|
108
|
+
handleHotUpdate({file, server}) {
|
|
109
|
+
if (!requiresFullReload(file)) return;
|
|
92
110
|
|
|
93
|
-
server
|
|
94
|
-
return [
|
|
111
|
+
reloadPage(server);
|
|
112
|
+
return [];
|
|
95
113
|
},
|
|
96
114
|
};
|
|
97
115
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {createRequire} from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {pathToFileURL} from "node:url";
|
|
4
|
+
|
|
5
|
+
export async function loadProjectVuePlugin(root = process.cwd()) {
|
|
6
|
+
const projectRequire = createRequire(path.resolve(root, "package.json"));
|
|
7
|
+
const pluginEntry = projectRequire.resolve("@vitejs/plugin-vue");
|
|
8
|
+
const pluginModule = await import(pathToFileURL(pluginEntry).href);
|
|
9
|
+
return pluginModule.default;
|
|
10
|
+
}
|
package/src/lib/pugMarkup.mjs
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import {createRequire} from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
|
-
import pug from "pug";
|
|
5
|
-
|
|
6
5
|
import {existsFile, normalizeHtml, r} from "./_utils.mjs";
|
|
7
6
|
|
|
8
7
|
const CONFIG_FILE = r("pug.rc");
|
|
8
|
+
const projectRequire = createRequire(r("package.json"));
|
|
9
|
+
|
|
10
|
+
function loadPug() {
|
|
11
|
+
try {
|
|
12
|
+
return projectRequire("pug");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw new Error("Pug markup requires local dependency \"pug\". Run npm install.", {cause: error});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
export function getPugConfigFile() {
|
|
11
19
|
return CONFIG_FILE;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export function renderPugFile(filePath) {
|
|
23
|
+
const pug = loadPug();
|
|
15
24
|
let config = {};
|
|
16
25
|
|
|
17
26
|
if (existsFile(CONFIG_FILE)) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import {rewriteHtmlImageAssets} from "./rewriteImageAssets.mjs";
|
|
5
|
+
|
|
4
6
|
const ROOT = process.cwd();
|
|
5
7
|
const CMS_DIR = path.resolve(ROOT, "@CMS");
|
|
6
8
|
const ORDER_FILE = path.resolve(ROOT, "src/order.json");
|
|
@@ -35,116 +37,8 @@ if (/<[^>]+>/.test(rawPrefix)) {
|
|
|
35
37
|
process.exit(0);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeRelPrefix(p) {
|
|
43
|
-
let s = String(p || "").trim();
|
|
44
|
-
s = s.replace(/^\/+/, ""); // убрать leading /
|
|
45
|
-
s = s.replace(/\/+$/, ""); // убрать trailing /
|
|
46
|
-
return s;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const BASE = normalizeBase(CDN_BASE);
|
|
50
|
-
const REL_PREFIX = normalizeRelPrefix(rawPrefix);
|
|
51
|
-
const FULL_PREFIX = REL_PREFIX ? `${BASE}/${REL_PREFIX}` : BASE;
|
|
52
|
-
|
|
53
|
-
function isSkippable(u) {
|
|
54
|
-
return (
|
|
55
|
-
/^https?:\/\//i.test(u) ||
|
|
56
|
-
u.startsWith("//") ||
|
|
57
|
-
u.startsWith("data:") ||
|
|
58
|
-
u.startsWith("blob:") ||
|
|
59
|
-
u.startsWith("mailto:") ||
|
|
60
|
-
u.startsWith("tel:") ||
|
|
61
|
-
u.startsWith("#")
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isRootRel(u) {
|
|
66
|
-
return typeof u === "string" && u.startsWith("/") && !u.startsWith("//");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function joinPrefix(u) {
|
|
70
|
-
if (!isRootRel(u) || isSkippable(u)) return u;
|
|
71
|
-
if (!REL_PREFIX) return BASE + u;
|
|
72
|
-
if (u === `/${REL_PREFIX}` || u.startsWith(`/${REL_PREFIX}/`)) return BASE + u;
|
|
73
|
-
return FULL_PREFIX + u; // u уже начинается с /
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function rewriteSrcset(srcset) {
|
|
77
|
-
if (!srcset || typeof srcset !== "string") return srcset;
|
|
78
|
-
return srcset
|
|
79
|
-
.split(",")
|
|
80
|
-
.map((part) => part.trim())
|
|
81
|
-
.filter(Boolean)
|
|
82
|
-
.map((item) => {
|
|
83
|
-
const [url, ...rest] = item.split(/\s+/);
|
|
84
|
-
if (isSkippable(url)) return item;
|
|
85
|
-
return [joinPrefix(url), ...rest].join(" ");
|
|
86
|
-
})
|
|
87
|
-
.join(", ");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// --- CSS url(...) переписываем ТОЛЬКО для картинок по расширениям
|
|
91
|
-
// поддержка query/hash: /a.webp?v=1#x
|
|
92
|
-
const IMG_EXT_RE = /\.(?:jpe?g|png|webp)(?:[?#][^'")]*)?(?=['")\s]|$)/i;
|
|
93
|
-
|
|
94
|
-
function rewriteCssUrls(cssText) {
|
|
95
|
-
if (!cssText || typeof cssText !== "string") return cssText;
|
|
96
|
-
|
|
97
|
-
return cssText.replace(
|
|
98
|
-
/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi,
|
|
99
|
-
(m, q, u) => {
|
|
100
|
-
if (isSkippable(u)) return m;
|
|
101
|
-
if (!isRootRel(u)) return m; // трогаем только "/..."
|
|
102
|
-
if (!IMG_EXT_RE.test(u)) return m; // трогаем только jpg/png/webp
|
|
103
|
-
const next = joinPrefix(u);
|
|
104
|
-
return `url(${q}${next}${q})`;
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function rewriteHtmlImages(html) {
|
|
110
|
-
let out = html;
|
|
111
|
-
|
|
112
|
-
// img src
|
|
113
|
-
out = out.replace(/\bsrc=(['"])([^'"]+)\1/gi, (m, q, v) => {
|
|
114
|
-
if (isSkippable(v)) return m;
|
|
115
|
-
if (!isRootRel(v)) return m;
|
|
116
|
-
// тут можно НЕ проверять расширение — img обычно и так картинка,
|
|
117
|
-
// но оставим правило "любое root-rel"
|
|
118
|
-
return `src=${q}${joinPrefix(v)}${q}`;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// img/srcset + source/srcset
|
|
122
|
-
out = out.replace(/\bsrcset=(['"])([\s\S]*?)\1/gi, (m, q, v) => {
|
|
123
|
-
return `srcset=${q}${rewriteSrcset(v)}${q}`;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// video poster
|
|
127
|
-
out = out.replace(/\bposter=(['"])([^'"]+)\1/gi, (m, q, v) => {
|
|
128
|
-
if (isSkippable(v)) return m;
|
|
129
|
-
if (!isRootRel(v)) return m;
|
|
130
|
-
// poster — картинка, можно без проверки расширения
|
|
131
|
-
return `poster=${q}${joinPrefix(v)}${q}`;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// inline style="...url('/x.webp')..."
|
|
135
|
-
out = out.replace(/\bstyle=(['"])([\s\S]*?)\1/gi, (m, q, v) => {
|
|
136
|
-
const next = rewriteCssUrls(v);
|
|
137
|
-
return `style=${q}${next}${q}`;
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// <style>...</style>
|
|
141
|
-
out = out.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, (m, css) => {
|
|
142
|
-
const nextCss = rewriteCssUrls(css);
|
|
143
|
-
return m.replace(css, nextCss);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
return out;
|
|
147
|
-
}
|
|
40
|
+
const BASE = String(CDN_BASE || "").trim().replace(/\/+$/, "");
|
|
41
|
+
const REL_PREFIX = String(rawPrefix || "").trim().replace(/^\/+|\/+$/g, "");
|
|
148
42
|
|
|
149
43
|
function listCmsHtmlFiles() {
|
|
150
44
|
if (!fs.existsSync(CMS_DIR)) return [];
|
|
@@ -164,7 +58,10 @@ let changed = 0;
|
|
|
164
58
|
|
|
165
59
|
for (const file of files) {
|
|
166
60
|
const prev = fs.readFileSync(file, "utf8");
|
|
167
|
-
const next =
|
|
61
|
+
const next = rewriteHtmlImageAssets(prev, {
|
|
62
|
+
assetsBase: BASE,
|
|
63
|
+
assetsPrefix: REL_PREFIX,
|
|
64
|
+
});
|
|
168
65
|
|
|
169
66
|
if (next !== prev) {
|
|
170
67
|
fs.writeFileSync(file, next);
|
|
@@ -1,25 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {isImageAssetUrl} from "./rewriteImageAssets.mjs";
|
|
2
|
+
|
|
3
|
+
export function toLocalAssetUrl(url, cdnBase = "http://127.0.0.1:5173") {
|
|
4
|
+
if (
|
|
5
|
+
typeof url !== "string" ||
|
|
6
|
+
!url.startsWith("/") ||
|
|
7
|
+
url.startsWith("//") ||
|
|
8
|
+
!isImageAssetUrl(url)
|
|
9
|
+
) {
|
|
10
|
+
return url;
|
|
11
|
+
}
|
|
12
|
+
return `${cdnBase.replace(/\/$/, "")}${url}`;
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const isProtocolRelative = (u) => u.startsWith("//");
|
|
11
|
-
const isRootRel = (u) => u.startsWith("/") && !isProtocolRelative(u);
|
|
15
|
+
export function rewriteLocalCssUrls(cssText, cdnBase = "http://127.0.0.1:5173") {
|
|
16
|
+
if (!cssText || typeof cssText !== "string" || !cssText.includes("url(")) return cssText;
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
return cssText.replace(
|
|
19
|
+
/url\(\s*(['"]?)(\/(?!\/)[^'")]+)\1\s*\)/g,
|
|
20
|
+
(match, quote, url) => `url(${quote}${toLocalAssetUrl(url, cdnBase)}${quote})`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
export function rewriteLocalSrcset(srcset, cdnBase = "http://127.0.0.1:5173") {
|
|
25
|
+
if (!srcset || typeof srcset !== "string") return srcset;
|
|
26
|
+
|
|
27
|
+
return srcset
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((part) => part.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map((item) => {
|
|
32
|
+
const [url, ...rest] = item.split(/\s+/);
|
|
33
|
+
return [toLocalAssetUrl(url, cdnBase), ...rest].join(" ");
|
|
34
|
+
})
|
|
35
|
+
.join(", ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function setupLocalCdnAssetRewrite({
|
|
39
|
+
root,
|
|
40
|
+
cdnBase = "http://127.0.0.1:5173",
|
|
41
|
+
enabled = true,
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (!enabled || !root) return;
|
|
23
44
|
|
|
24
45
|
const rewriteStyleTag = (el) => {
|
|
25
46
|
if (
|
|
@@ -29,49 +50,37 @@ export async function setupLocalCdnAssetRewrite({
|
|
|
29
50
|
return;
|
|
30
51
|
}
|
|
31
52
|
const css = el.textContent || "";
|
|
32
|
-
const nextCss =
|
|
53
|
+
const nextCss = rewriteLocalCssUrls(css, cdnBase);
|
|
33
54
|
if (nextCss !== css) el.textContent = nextCss;
|
|
34
55
|
};
|
|
35
56
|
|
|
36
|
-
const rewriteSrcset = (srcset) => {
|
|
37
|
-
if (!srcset || typeof srcset !== "string") return srcset;
|
|
38
|
-
return srcset
|
|
39
|
-
.split(",")
|
|
40
|
-
.map((part) => part.trim())
|
|
41
|
-
.filter(Boolean)
|
|
42
|
-
.map((item) => {
|
|
43
|
-
const [url, ...rest] = item.split(/\s+/);
|
|
44
|
-
const nextUrl = toCdn(url);
|
|
45
|
-
return [nextUrl, ...rest].join(" ");
|
|
46
|
-
})
|
|
47
|
-
.join(", ");
|
|
48
|
-
};
|
|
49
|
-
|
|
50
57
|
const rewriteEl = (el) => {
|
|
51
58
|
if (!el || el.nodeType !== 1) return;
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
for (const attribute of ["src", "poster"]) {
|
|
61
|
+
if (!el.hasAttribute?.(attribute)) continue;
|
|
62
|
+
const value = el.getAttribute(attribute);
|
|
63
|
+
const nextValue = toLocalAssetUrl(value, cdnBase);
|
|
64
|
+
if (nextValue !== value) el.setAttribute(attribute, nextValue);
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
if (el.hasAttribute?.("srcset")) {
|
|
59
68
|
const srcset = el.getAttribute("srcset");
|
|
60
|
-
const next =
|
|
69
|
+
const next = rewriteLocalSrcset(srcset, cdnBase);
|
|
61
70
|
if (next !== srcset) el.setAttribute("srcset", next);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
// inline style url("/...")
|
|
65
74
|
const style = el.getAttribute?.("style");
|
|
66
75
|
if (style && style.includes("url(")) {
|
|
67
|
-
const nextStyle =
|
|
76
|
+
const nextStyle = rewriteLocalCssUrls(style, cdnBase);
|
|
68
77
|
if (nextStyle !== style) el.setAttribute("style", nextStyle);
|
|
69
78
|
}
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
const rewriteTree = (node) => {
|
|
73
82
|
rewriteEl(node);
|
|
74
|
-
node?.querySelectorAll?.("[src],[srcset],[style]").forEach(rewriteEl);
|
|
83
|
+
node?.querySelectorAll?.("[src],[srcset],[poster],[style]").forEach(rewriteEl);
|
|
75
84
|
};
|
|
76
85
|
|
|
77
86
|
// 1) переписываем HTML внутри root
|
|
@@ -95,7 +104,7 @@ export async function setupLocalCdnAssetRewrite({
|
|
|
95
104
|
childList: true,
|
|
96
105
|
subtree: true,
|
|
97
106
|
attributes: true,
|
|
98
|
-
attributeFilter: ["src", "srcset", "style"],
|
|
107
|
+
attributeFilter: ["src", "srcset", "poster", "style"],
|
|
99
108
|
});
|
|
100
109
|
|
|
101
110
|
// 4) наблюдаем за появлением новых style-тегов (HMR)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const IMAGE_EXTENSION_RE = /\.(?:avif|gif|jpe?g|png|svg|webp)(?:[?#][^\s]*)?$/i;
|
|
2
|
+
|
|
3
|
+
function normalizeBase(value) {
|
|
4
|
+
return String(value || "").trim().replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizePrefix(value) {
|
|
8
|
+
return String(value || "").trim().replace(/^\/+|\/+$/g, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isImageAssetUrl(url) {
|
|
12
|
+
return typeof url === "string" && IMAGE_EXTENSION_RE.test(url.trim());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function rewriteImageAssetUrl(url, {assetsBase, assetsPrefix} = {}) {
|
|
16
|
+
if (
|
|
17
|
+
typeof url !== "string" ||
|
|
18
|
+
!url.startsWith("/") ||
|
|
19
|
+
url.startsWith("//") ||
|
|
20
|
+
!isImageAssetUrl(url)
|
|
21
|
+
) {
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const base = normalizeBase(assetsBase);
|
|
26
|
+
const prefix = normalizePrefix(assetsPrefix);
|
|
27
|
+
if (!base) return url;
|
|
28
|
+
if (!prefix) return `${base}${url}`;
|
|
29
|
+
if (url === `/${prefix}` || url.startsWith(`/${prefix}/`)) return `${base}${url}`;
|
|
30
|
+
return `${base}/${prefix}${url}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function rewriteImageSrcset(srcset, options) {
|
|
34
|
+
if (!srcset || typeof srcset !== "string") return srcset;
|
|
35
|
+
|
|
36
|
+
return srcset.replace(
|
|
37
|
+
/(^|,\s*)(\/(?!\/)[^\s,]+)(?=\s|,|$)/g,
|
|
38
|
+
(match, separator, url) => `${separator}${rewriteImageAssetUrl(url, options)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function rewriteCssImageUrls(cssText, options) {
|
|
43
|
+
if (!cssText || typeof cssText !== "string") return cssText;
|
|
44
|
+
|
|
45
|
+
return cssText.replace(
|
|
46
|
+
/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi,
|
|
47
|
+
(match, quote, url) => {
|
|
48
|
+
const nextUrl = rewriteImageAssetUrl(url, options);
|
|
49
|
+
return nextUrl === url ? match : `url(${quote}${nextUrl}${quote})`;
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rewriteHtmlImageAssets(html, options) {
|
|
55
|
+
if (!html || typeof html !== "string") return html;
|
|
56
|
+
|
|
57
|
+
let output = html;
|
|
58
|
+
|
|
59
|
+
output = output.replace(/\bsrc=(['"])([^'"]+)\1/gi, (match, quote, url) => {
|
|
60
|
+
const nextUrl = rewriteImageAssetUrl(url, options);
|
|
61
|
+
return nextUrl === url ? match : `src=${quote}${nextUrl}${quote}`;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
output = output.replace(/\bsrcset=(['"])([\s\S]*?)\1/gi, (match, quote, srcset) => {
|
|
65
|
+
const nextSrcset = rewriteImageSrcset(srcset, options);
|
|
66
|
+
return nextSrcset === srcset ? match : `srcset=${quote}${nextSrcset}${quote}`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
output = output.replace(/\bposter=(['"])([^'"]+)\1/gi, (match, quote, url) => {
|
|
70
|
+
const nextUrl = rewriteImageAssetUrl(url, options);
|
|
71
|
+
return nextUrl === url ? match : `poster=${quote}${nextUrl}${quote}`;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
output = output.replace(/\bstyle=(['"])([\s\S]*?)\1/gi, (match, quote, css) => {
|
|
75
|
+
const nextCss = rewriteCssImageUrls(css, options);
|
|
76
|
+
return nextCss === css ? match : `style=${quote}${nextCss}${quote}`;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
output = output.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, (match, css) => {
|
|
80
|
+
const nextCss = rewriteCssImageUrls(css, options);
|
|
81
|
+
return nextCss === css ? match : match.replace(css, nextCss);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return output;
|
|
85
|
+
}
|
package/src/vite/client.js
CHANGED
|
@@ -2,7 +2,6 @@ import parts from "virtual:landing-blocks";
|
|
|
2
2
|
import {setupLocalCdnAssetRewrite} from "../lib/rewriteAssetsDev.js";
|
|
3
3
|
|
|
4
4
|
const FLAG = "monkeyMounted";
|
|
5
|
-
let activeParts = parts;
|
|
6
5
|
|
|
7
6
|
function mount(container, blocks) {
|
|
8
7
|
if (container.dataset[FLAG] === "1") return;
|
|
@@ -13,7 +12,7 @@ function mount(container, blocks) {
|
|
|
13
12
|
}
|
|
14
13
|
container.dataset[FLAG] = "1";
|
|
15
14
|
for (const part of blocks) {
|
|
16
|
-
if (typeof part?.init === "function") part.init(
|
|
15
|
+
if (typeof part?.init === "function") part.init();
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
|
|
@@ -32,17 +31,5 @@ function waitForContainer() {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
const container = await waitForContainer();
|
|
35
|
-
mount(container,
|
|
34
|
+
mount(container, parts);
|
|
36
35
|
setupLocalCdnAssetRewrite({root: container, cdnBase: "http://127.0.0.1:5173"});
|
|
37
|
-
|
|
38
|
-
if (import.meta.hot) {
|
|
39
|
-
import.meta.hot.accept("virtual:landing-blocks", (module) => {
|
|
40
|
-
if (!module?.default) return;
|
|
41
|
-
activeParts = module.default;
|
|
42
|
-
const host = document.querySelector("#monkey-app");
|
|
43
|
-
if (!host) return;
|
|
44
|
-
host.replaceChildren();
|
|
45
|
-
delete host.dataset[FLAG];
|
|
46
|
-
mount(host, activeParts);
|
|
47
|
-
});
|
|
48
|
-
}
|
package/src/vite/config.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import {fileURLToPath} from "node:url";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import vue from "@vitejs/plugin-vue";
|
|
5
4
|
import {createServer} from "vite";
|
|
6
5
|
import monkey from "vite-plugin-monkey";
|
|
7
6
|
|
|
8
7
|
import {loadConfig} from "../config/load-config.mjs";
|
|
9
8
|
import {resolvePreset} from "../config/resolve-preset.mjs";
|
|
10
9
|
import {landingBlocksPlugin} from "../lib/landingBlocksPlugin.mjs";
|
|
11
|
-
import {
|
|
10
|
+
import {loadProjectVuePlugin} from "../lib/loadProjectVuePlugin.mjs";
|
|
12
11
|
|
|
13
12
|
const clientEntry = fileURLToPath(new URL("./client.js", import.meta.url));
|
|
14
13
|
const builderRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
@@ -18,11 +17,13 @@ export async function createViteConfig(root = process.cwd()) {
|
|
|
18
17
|
const config = await loadConfig(root);
|
|
19
18
|
const preset = resolvePreset(config.site.preset);
|
|
20
19
|
const shouldOpen = process.env.B2C_NO_OPEN !== "1";
|
|
20
|
+
const vue = config.stack.script === "vue"
|
|
21
|
+
? await loadProjectVuePlugin(root)
|
|
22
|
+
: null;
|
|
21
23
|
|
|
22
24
|
return {
|
|
23
25
|
root,
|
|
24
26
|
configFile: false,
|
|
25
|
-
resolve: {alias: builderAliases},
|
|
26
27
|
server: {
|
|
27
28
|
host: "127.0.0.1",
|
|
28
29
|
port: 5173,
|
|
@@ -32,7 +33,7 @@ export async function createViteConfig(root = process.cwd()) {
|
|
|
32
33
|
fs: {allow: [root, builderRoot]},
|
|
33
34
|
},
|
|
34
35
|
plugins: [
|
|
35
|
-
|
|
36
|
+
vue && vue(),
|
|
36
37
|
landingBlocksPlugin({root, stack: config.stack}),
|
|
37
38
|
monkey({
|
|
38
39
|
entry: path.normalize(clientEntryUrl),
|