@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 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(container) {
133
- const block = container.querySelector(".hero");
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
- dev, HMR и CMS build. Блок без JS допустим.
145
+ Builder вставляет разметку, затем вызывает функцию без аргументов. Скрипт блока
146
+ сам находит свою разметку в `document`. Этот контракт одинаков для dev и CMS
147
+ build; дополнительные контейнеры builder не создаёт. Блок без JS допустим.
142
148
 
143
- ## Ассеты
149
+ ## Изображения
144
150
 
145
- Файлы хранятся в `public/` и используются от корня:
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.0.0",
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": {
@@ -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}(container) {\n const block = container.querySelector(".${key}");\n if (!block) return;\n}\n`;
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
  }
@@ -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
- resolve: {alias: builderAliases},
92
- plugins: [vue(), virtualJs],
92
+ plugins: [vue && vue(), virtualJs].filter(Boolean),
93
93
  build: {
94
94
  write: false,
95
95
  minify: "oxc",
96
- cssCodeSplit: true,
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 watchedDirs = [markupDir, stylesDir, scriptsDir];
23
-
24
- const affectsBlocks = (filePath) =>
27
+ const requiresFullReload = (filePath) =>
25
28
  filePath === orderFile ||
26
29
  filePath === pugConfigFile ||
27
- watchedDirs.some((dir) => {
28
- const relative = path.relative(dir, filePath);
29
- return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
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, modules}) {
88
- if (!affectsBlocks(file)) return;
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.moduleGraph.invalidateModule(virtualModule);
94
- return [virtualModule, ...modules.filter((module) => module !== virtualModule)];
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
+ }
@@ -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
- function normalizeBase(p) {
39
- return String(p || "").trim().replace(/\/+$/, "");
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 = rewriteHtmlImages(prev);
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
- export async function setupLocalCdnAssetRewrite({
2
- root, // HTMLElement
3
- cdnBase = "http://127.0.0.1:5173",
4
- enabled = true,
5
- } = {}) {
6
- if (!enabled || !root) return;
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
- const isAbs = (u) =>
9
- /^https?:\/\//i.test(u) || u.startsWith("data:") || u.startsWith("blob:");
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
- const toCdn = (u) => (isRootRel(u) ? cdnBase.replace(/\/$/, "") + u : u);
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
- const rewriteCssUrls = (cssText) => {
16
- if (!cssText || typeof cssText !== "string" || !cssText.includes("url(")) {
17
- return cssText;
18
- }
19
- return cssText.replace(/url\((['"]?)(\/[^'")]+)\1\)/g, (m, q, p) => {
20
- return `url(${q}${toCdn(p)}${q})`;
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 = rewriteCssUrls(css);
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
- if (el.hasAttribute?.("src")) {
54
- const src = el.getAttribute("src");
55
- if (src && !isAbs(src) && isRootRel(src)) el.setAttribute("src", toCdn(src));
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 = rewriteSrcset(srcset);
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 = rewriteCssUrls(style);
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
+ }
@@ -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(container);
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, activeParts);
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
- }
@@ -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 {builderAliases} from "./dependencies.mjs";
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
- config.stack.script === "vue" && vue(),
36
+ vue && vue(),
36
37
  landingBlocksPlugin({root, stack: config.stack}),
37
38
  monkey({
38
39
  entry: path.normalize(clientEntryUrl),
@@ -1,7 +0,0 @@
1
- import {fileURLToPath} from "node:url";
2
-
3
- export const vueEntry = fileURLToPath(import.meta.resolve("vue"));
4
-
5
- export const builderAliases = {
6
- vue: vueEntry,
7
- };