@duffcloudservices/cms 0.7.1 → 0.9.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.
@@ -1,28 +1,66 @@
1
- import { buildSitemapXml, buildRobotsTxt, buildLlmsTxt, matchesExcludedGlob, breadcrumbTrailFromRoute, findReviewItemsForPage, buildHeadTags, spliceHeadHtml, loadPagesManifest } from '../chunk-FUNIALH6.js';
2
- export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-FUNIALH6.js';
3
- import fs2 from 'fs';
4
- import path2 from 'path';
5
- import yaml2 from 'js-yaml';
1
+ import { buildSitemapXml, isHandAuthoredRobotsAcceptable, buildRobotsTxt, buildLlmsTxt, matchesExcludedGlob, breadcrumbTrailFromRoute, findReviewItemsForPage, buildHeadTags, spliceHeadHtml, loadPagesManifest } from '../chunk-UPAMLKOQ.js';
2
+ export { buildVitePressSeoHead, createSeoTransformPageData, defaultRelativePathToRoute } from '../chunk-UPAMLKOQ.js';
3
+ import fs3 from 'fs';
4
+ import path3 from 'path';
5
+ import yaml3 from 'js-yaml';
6
6
  import { defineComponent, h } from 'vue';
7
7
 
8
+ function dcsSearchPaths(projectRoot, relPath) {
9
+ return [
10
+ path3.resolve(projectRoot, relPath),
11
+ path3.resolve(projectRoot, "..", relPath),
12
+ path3.resolve(process.cwd(), relPath)
13
+ ];
14
+ }
15
+ function resolveDcsFile(projectRoot, relPath) {
16
+ return dcsSearchPaths(projectRoot, relPath).find((p) => fs3.existsSync(p));
17
+ }
18
+ function readDcsYamlFresh(projectRoot, relPath, label, debug = false) {
19
+ const found = resolveDcsFile(projectRoot, relPath);
20
+ if (!found) {
21
+ if (debug) console.log(`[${label}] serve: no ${relPath} found; global left unset`);
22
+ return void 0;
23
+ }
24
+ try {
25
+ return yaml3.load(fs3.readFileSync(found, "utf8"));
26
+ } catch (error) {
27
+ console.warn(`[${label}] serve: failed to parse ${relPath}:`, error);
28
+ return void 0;
29
+ }
30
+ }
31
+ function buildGlobalAssignScript(globalName, value) {
32
+ const json = JSON.stringify(value ?? null).replace(/</g, "\\u003c");
33
+ return `globalThis.${globalName} = ${json};`;
34
+ }
35
+
36
+ // src/plugins/dcsContentPlugin.ts
8
37
  function dcsContentPlugin(options = {}) {
9
38
  const { contentPath = ".dcs/content.yaml", debug = false } = options;
10
39
  let resolvedConfig;
40
+ let isDev = false;
11
41
  return {
12
42
  name: "dcs-content",
13
43
  configResolved(config) {
14
44
  resolvedConfig = config;
45
+ isDev = config.command === "serve";
15
46
  },
16
- config(config) {
47
+ config(config, env) {
48
+ if (env.command === "serve") {
49
+ return {
50
+ define: {
51
+ __DCS_CONTENT__: "globalThis.__DCS_CONTENT__"
52
+ }
53
+ };
54
+ }
17
55
  const projectRoot = config.root || process.cwd();
18
56
  const possiblePaths = [
19
- path2.resolve(projectRoot, contentPath),
20
- path2.resolve(projectRoot, "..", contentPath),
21
- path2.resolve(process.cwd(), contentPath)
57
+ path3.resolve(projectRoot, contentPath),
58
+ path3.resolve(projectRoot, "..", contentPath),
59
+ path3.resolve(process.cwd(), contentPath)
22
60
  ];
23
61
  let foundPath;
24
62
  for (const testPath of possiblePaths) {
25
- if (fs2.existsSync(testPath)) {
63
+ if (fs3.existsSync(testPath)) {
26
64
  foundPath = testPath;
27
65
  break;
28
66
  }
@@ -40,8 +78,8 @@ function dcsContentPlugin(options = {}) {
40
78
  };
41
79
  }
42
80
  try {
43
- const fileContent = fs2.readFileSync(foundPath, "utf8");
44
- const content = yaml2.load(fileContent);
81
+ const fileContent = fs3.readFileSync(foundPath, "utf8");
82
+ const content = yaml3.load(fileContent);
45
83
  if (debug) {
46
84
  console.log(`[dcs-content] Loaded ${foundPath}`);
47
85
  console.log(`[dcs-content] Version: ${content.version}`);
@@ -62,42 +100,88 @@ function dcsContentPlugin(options = {}) {
62
100
  };
63
101
  }
64
102
  },
65
- // Watch for changes in development
103
+ /**
104
+ * Dev-only: inject the FRESH on-disk `content.yaml` as an inline
105
+ * `globalThis.__DCS_CONTENT__ = {…}` classic script at the top of `<head>`.
106
+ *
107
+ * Runs on every dev HTML request, so the portal's `?_hc` hard reload (or any
108
+ * full page reload) re-reads the YAML with no restart. A classic `<script>`
109
+ * in `<head>` executes during HTML parsing, before the deferred module app
110
+ * entry, so the global is set before the consumer's `useTextContent` setup
111
+ * reads it. Gated on `isDev` so it never runs in `vite`/`vitepress build`
112
+ * (the build path keeps the baked `define` literal).
113
+ */
114
+ transformIndexHtml: {
115
+ order: "pre",
116
+ handler(html) {
117
+ if (!isDev) return html;
118
+ const projectRoot = resolvedConfig?.root || process.cwd();
119
+ const content = readDcsYamlFresh(
120
+ projectRoot,
121
+ contentPath,
122
+ "dcs-content",
123
+ debug
124
+ );
125
+ if (content === void 0) return html;
126
+ if (debug) {
127
+ console.log("[dcs-content] serve: injecting fresh __DCS_CONTENT__ into <head>");
128
+ }
129
+ const tags = [
130
+ {
131
+ tag: "script",
132
+ children: buildGlobalAssignScript("__DCS_CONTENT__", content),
133
+ injectTo: "head-prepend"
134
+ }
135
+ ];
136
+ return { html, tags };
137
+ }
138
+ },
139
+ // Watch for changes in development.
140
+ //
141
+ // Downgraded from `server.restart()` to a debounced client `full-reload`:
142
+ // freshness already comes from `transformIndexHtml` re-reading the YAML on
143
+ // every HTML request, so all we need on change is to nudge the browser to
144
+ // reload. A `server.restart()` would rebuild VitePress's dev MiniSearch
145
+ // index and can throw "server restart failed" on a duplicate heading id —
146
+ // exactly the fragility this fix removes. A `full-reload` does neither.
66
147
  configureServer(server) {
67
148
  const projectRoot = resolvedConfig?.root || process.cwd();
68
149
  const watchPaths = [
69
- path2.resolve(projectRoot, contentPath),
70
- path2.resolve(projectRoot, "..", contentPath)
71
- ];
150
+ path3.resolve(projectRoot, contentPath),
151
+ path3.resolve(projectRoot, "..", contentPath)
152
+ ].map((p) => path3.normalize(p));
72
153
  watchPaths.forEach((watchPath) => {
73
- if (fs2.existsSync(watchPath)) {
154
+ if (fs3.existsSync(watchPath)) {
74
155
  server.watcher.add(watchPath);
75
- server.watcher.on("change", (changedPath) => {
76
- if (changedPath === watchPath) {
77
- if (debug) {
78
- console.log("[dcs-content] content.yaml changed, triggering reload");
79
- }
80
- server.restart();
81
- }
82
- });
83
156
  }
84
157
  });
158
+ let reloadTimer;
159
+ server.watcher.on("change", (changedPath) => {
160
+ if (!watchPaths.includes(path3.normalize(changedPath))) return;
161
+ if (reloadTimer) clearTimeout(reloadTimer);
162
+ reloadTimer = setTimeout(() => {
163
+ if (debug) {
164
+ console.log("[dcs-content] content.yaml changed, sending full-reload");
165
+ }
166
+ server.ws.send({ type: "full-reload" });
167
+ }, 100);
168
+ });
85
169
  }
86
170
  };
87
171
  }
88
172
  function resolveOutDir(config) {
89
173
  const out = config.build?.outDir || "dist";
90
- return path2.isAbsolute(out) ? out : path2.resolve(config.root || process.cwd(), out);
174
+ return path3.isAbsolute(out) ? out : path3.resolve(config.root || process.cwd(), out);
91
175
  }
92
176
  function loadSeoConfig(projectRoot, seoPath, debug) {
93
177
  const possiblePaths = [
94
- path2.resolve(projectRoot, seoPath),
95
- path2.resolve(projectRoot, "..", seoPath),
96
- path2.resolve(process.cwd(), seoPath)
178
+ path3.resolve(projectRoot, seoPath),
179
+ path3.resolve(projectRoot, "..", seoPath),
180
+ path3.resolve(process.cwd(), seoPath)
97
181
  ];
98
182
  let foundPath;
99
183
  for (const testPath of possiblePaths) {
100
- if (fs2.existsSync(testPath)) {
184
+ if (fs3.existsSync(testPath)) {
101
185
  foundPath = testPath;
102
186
  break;
103
187
  }
@@ -111,8 +195,8 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
111
195
  return null;
112
196
  }
113
197
  try {
114
- const fileContent = fs2.readFileSync(foundPath, "utf8");
115
- const config = yaml2.load(fileContent);
198
+ const fileContent = fs3.readFileSync(foundPath, "utf8");
199
+ const config = yaml3.load(fileContent);
116
200
  return { config, foundPath };
117
201
  } catch (error) {
118
202
  console.warn("[dcs-seo] Failed to parse seo.yaml:", error);
@@ -121,17 +205,17 @@ function loadSeoConfig(projectRoot, seoPath, debug) {
121
205
  }
122
206
  function loadContentConfig(projectRoot, contentRelPath, debug) {
123
207
  const possiblePaths = [
124
- path2.resolve(projectRoot, contentRelPath),
125
- path2.resolve(projectRoot, "..", contentRelPath),
126
- path2.resolve(process.cwd(), contentRelPath)
208
+ path3.resolve(projectRoot, contentRelPath),
209
+ path3.resolve(projectRoot, "..", contentRelPath),
210
+ path3.resolve(process.cwd(), contentRelPath)
127
211
  ];
128
- const foundPath = possiblePaths.find((p) => fs2.existsSync(p));
212
+ const foundPath = possiblePaths.find((p) => fs3.existsSync(p));
129
213
  if (!foundPath) {
130
214
  if (debug) console.log(`[dcs-seo] No content.yaml found (reviews disabled)`);
131
215
  return void 0;
132
216
  }
133
217
  try {
134
- const parsed = yaml2.load(fs2.readFileSync(foundPath, "utf8"));
218
+ const parsed = yaml3.load(fs3.readFileSync(foundPath, "utf8"));
135
219
  if (!parsed || typeof parsed !== "object") return void 0;
136
220
  return parsed;
137
221
  } catch (error) {
@@ -141,19 +225,19 @@ function loadContentConfig(projectRoot, contentRelPath, debug) {
141
225
  }
142
226
  function routeToOutputFile(outDir, routePath) {
143
227
  const trimmed = routePath.replace(/^\/+/, "").replace(/\/+$/, "");
144
- if (trimmed === "") return path2.join(outDir, "index.html");
145
- return path2.join(outDir, ...trimmed.split("/"), "index.html");
228
+ if (trimmed === "") return path3.join(outDir, "index.html");
229
+ return path3.join(outDir, ...trimmed.split("/"), "index.html");
146
230
  }
147
231
  function loadExcludedGlobs(projectRoot, pagesPath) {
148
232
  const possiblePaths = [
149
- path2.resolve(projectRoot, pagesPath),
150
- path2.resolve(projectRoot, "..", pagesPath),
151
- path2.resolve(process.cwd(), pagesPath)
233
+ path3.resolve(projectRoot, pagesPath),
234
+ path3.resolve(projectRoot, "..", pagesPath),
235
+ path3.resolve(process.cwd(), pagesPath)
152
236
  ];
153
- const found = possiblePaths.find((p) => fs2.existsSync(p));
237
+ const found = possiblePaths.find((p) => fs3.existsSync(p));
154
238
  if (!found) return [];
155
239
  try {
156
- const raw = yaml2.load(fs2.readFileSync(found, "utf8"));
240
+ const raw = yaml3.load(fs3.readFileSync(found, "utf8"));
157
241
  const excluded = raw?.excluded;
158
242
  if (!Array.isArray(excluded)) return [];
159
243
  return excluded.filter((e) => typeof e === "string");
@@ -165,14 +249,14 @@ function deriveLastmod(seoConfig, projectRoot, pagesPath) {
165
249
  const fromSeo = seoConfig?.lastUpdated;
166
250
  if (typeof fromSeo === "string" && fromSeo.length > 0) return fromSeo;
167
251
  const possiblePaths = [
168
- path2.resolve(projectRoot, pagesPath),
169
- path2.resolve(projectRoot, "..", pagesPath),
170
- path2.resolve(process.cwd(), pagesPath)
252
+ path3.resolve(projectRoot, pagesPath),
253
+ path3.resolve(projectRoot, "..", pagesPath),
254
+ path3.resolve(process.cwd(), pagesPath)
171
255
  ];
172
- const found = possiblePaths.find((p) => fs2.existsSync(p));
256
+ const found = possiblePaths.find((p) => fs3.existsSync(p));
173
257
  if (!found) return void 0;
174
258
  try {
175
- const raw = yaml2.load(fs2.readFileSync(found, "utf8"));
259
+ const raw = yaml3.load(fs3.readFileSync(found, "utf8"));
176
260
  const fromPages = raw?.lastUpdated;
177
261
  if (typeof fromPages === "string" && fromPages.length > 0) return fromPages;
178
262
  } catch {
@@ -210,7 +294,7 @@ function emitSiteFiles(params) {
210
294
  lastmod
211
295
  });
212
296
  if (xml) {
213
- fs2.writeFileSync(path2.join(outDir, "sitemap.xml"), xml, "utf8");
297
+ fs3.writeFileSync(path3.join(outDir, "sitemap.xml"), xml, "utf8");
214
298
  wroteSitemap = true;
215
299
  written.push("sitemap.xml");
216
300
  if (debug) console.log("[dcs-seo] wrote sitemap.xml");
@@ -219,9 +303,12 @@ function emitSiteFiles(params) {
219
303
  }
220
304
  }
221
305
  if (robots.enabled ?? true) {
222
- const robotsPath = path2.join(outDir, "robots.txt");
223
- if (fs2.existsSync(robotsPath) && !robots.force) {
224
- if (debug) console.log("[dcs-seo] dist/robots.txt exists and force not set; leaving it");
306
+ const robotsPath = path3.join(outDir, "robots.txt");
307
+ const existing = fs3.existsSync(robotsPath) && !robots.force ? fs3.readFileSync(robotsPath, "utf8") : null;
308
+ const keepHandAuthored = existing !== null && isHandAuthoredRobotsAcceptable(existing);
309
+ if (keepHandAuthored) {
310
+ if (debug)
311
+ console.log("[dcs-seo] dist/robots.txt is hand-authored and meets the floor; leaving it");
225
312
  } else {
226
313
  const txt = buildRobotsTxt({
227
314
  siteUrl: effectiveSiteUrl,
@@ -229,9 +316,13 @@ function emitSiteFiles(params) {
229
316
  robots,
230
317
  hasSitemap: wroteSitemap
231
318
  });
232
- fs2.writeFileSync(robotsPath, txt, "utf8");
319
+ fs3.writeFileSync(robotsPath, txt, "utf8");
233
320
  written.push("robots.txt");
234
- if (debug) console.log("[dcs-seo] wrote robots.txt");
321
+ if (debug) {
322
+ console.log(
323
+ existing !== null ? "[dcs-seo] dist/robots.txt failed the factory floor; overwrote with emitted robots" : "[dcs-seo] wrote robots.txt"
324
+ );
325
+ }
235
326
  }
236
327
  }
237
328
  if (llms && !preview) {
@@ -244,7 +335,7 @@ function emitSiteFiles(params) {
244
335
  excludedGlobs
245
336
  });
246
337
  if (txt) {
247
- fs2.writeFileSync(path2.join(outDir, "llms.txt"), txt, "utf8");
338
+ fs3.writeFileSync(path3.join(outDir, "llms.txt"), txt, "utf8");
248
339
  written.push("llms.txt");
249
340
  if (debug) console.log("[dcs-seo] wrote llms.txt");
250
341
  } else if (debug) {
@@ -309,12 +400,12 @@ function emitStaticSeoHtml(params) {
309
400
  });
310
401
  const html = spliceHeadHtml(shellHtml, tags);
311
402
  const outFile = routeToOutputFile(outDir, route.path);
312
- fs2.mkdirSync(path2.dirname(outFile), { recursive: true });
313
- fs2.writeFileSync(outFile, html, "utf8");
403
+ fs3.mkdirSync(path3.dirname(outFile), { recursive: true });
404
+ fs3.writeFileSync(outFile, html, "utf8");
314
405
  written++;
315
406
  if (debug) {
316
407
  console.log(
317
- `[dcs-seo] wrote ${path2.relative(outDir, outFile)} (title="${tags.title}"${forceNoindex ? ", noindex" : ""})`
408
+ `[dcs-seo] wrote ${path3.relative(outDir, outFile)} (title="${tags.title}"${forceNoindex ? ", noindex" : ""})`
318
409
  );
319
410
  }
320
411
  }
@@ -338,12 +429,21 @@ function dcsSeoPlugin(options = {}) {
338
429
  llms = true
339
430
  } = options;
340
431
  let resolvedConfig;
432
+ let isDev = false;
341
433
  return {
342
434
  name: "dcs-seo",
343
435
  configResolved(config) {
344
436
  resolvedConfig = config;
437
+ isDev = config.command === "serve";
345
438
  },
346
- config(config) {
439
+ config(config, env) {
440
+ if (env.command === "serve") {
441
+ return {
442
+ define: {
443
+ __DCS_SEO__: "globalThis.__DCS_SEO__"
444
+ }
445
+ };
446
+ }
347
447
  const projectRoot = config.root || process.cwd();
348
448
  const loaded = loadSeoConfig(projectRoot, seoPath, debug);
349
449
  if (!loaded) {
@@ -389,12 +489,12 @@ function dcsSeoPlugin(options = {}) {
389
489
  try {
390
490
  const projectRoot = resolvedConfig?.root || process.cwd();
391
491
  const outDir = resolveOutDir(resolvedConfig);
392
- if (!fs2.existsSync(outDir)) {
492
+ if (!fs3.existsSync(outDir)) {
393
493
  console.warn(`[dcs-seo] emitStaticHtml: outDir not found (${outDir}); skipping`);
394
494
  return;
395
495
  }
396
- const shellPath = path2.join(outDir, "index.html");
397
- if (!fs2.existsSync(shellPath)) {
496
+ const shellPath = path3.join(outDir, "index.html");
497
+ if (!fs3.existsSync(shellPath)) {
398
498
  console.warn(
399
499
  `[dcs-seo] emitStaticHtml: no index.html in ${outDir}; nothing to emit`
400
500
  );
@@ -413,7 +513,7 @@ function dcsSeoPlugin(options = {}) {
413
513
  `[dcs-seo] emitStaticHtml: ${seoPath} missing or unparseable; emitting with defaults only`
414
514
  );
415
515
  }
416
- const shellHtml = fs2.readFileSync(shellPath, "utf8");
516
+ const shellHtml = fs3.readFileSync(shellPath, "utf8");
417
517
  const excludedGlobs = loadExcludedGlobs(projectRoot, pagesPath);
418
518
  const contentConfig = loadContentConfig(projectRoot, contentPath, debug);
419
519
  const written = emitStaticSeoHtml({
@@ -455,26 +555,64 @@ function dcsSeoPlugin(options = {}) {
455
555
  console.warn("[dcs-seo] emitStaticHtml failed; build output left unchanged:", error);
456
556
  }
457
557
  },
458
- // Watch for changes in development
558
+ /**
559
+ * Dev-only: inject the FRESH on-disk `seo.yaml` as an inline
560
+ * `globalThis.__DCS_SEO__ = {…}` classic script at the top of `<head>`,
561
+ * mirroring dcsContentPlugin. Lets the runtime `useSEO` SPA path reflect
562
+ * on-disk SEO edits on the portal's `?_hc` hard reload with no restart.
563
+ * Gated on `isDev` so it never runs in `vite`/`vitepress build` (the build
564
+ * path keeps the baked `define` literal + the `writeBundle` emitter).
565
+ */
566
+ transformIndexHtml: {
567
+ order: "pre",
568
+ handler(html) {
569
+ if (!isDev) return html;
570
+ const projectRoot = resolvedConfig?.root || process.cwd();
571
+ const config = readDcsYamlFresh(projectRoot, seoPath, "dcs-seo", debug);
572
+ if (config === void 0) return html;
573
+ if (debug) {
574
+ console.log("[dcs-seo] serve: injecting fresh __DCS_SEO__ into <head>");
575
+ }
576
+ const tags = [
577
+ {
578
+ tag: "script",
579
+ children: buildGlobalAssignScript("__DCS_SEO__", config),
580
+ injectTo: "head-prepend"
581
+ }
582
+ ];
583
+ return { html, tags };
584
+ }
585
+ },
586
+ // Watch for changes in development.
587
+ //
588
+ // Downgraded from `server.restart()` to a debounced client `full-reload`:
589
+ // freshness already comes from `transformIndexHtml` re-reading the YAML on
590
+ // every HTML request, so all we need on change is a browser reload. A
591
+ // `server.restart()` would rebuild VitePress's dev MiniSearch index and can
592
+ // throw "server restart failed" on a duplicate heading id — the fragility
593
+ // this fix removes. A `full-reload` does neither.
459
594
  configureServer(server) {
460
595
  const projectRoot = resolvedConfig?.root || process.cwd();
461
596
  const watchPaths = [
462
- path2.resolve(projectRoot, seoPath),
463
- path2.resolve(projectRoot, "..", seoPath)
464
- ];
597
+ path3.resolve(projectRoot, seoPath),
598
+ path3.resolve(projectRoot, "..", seoPath)
599
+ ].map((p) => path3.normalize(p));
465
600
  watchPaths.forEach((watchPath) => {
466
- if (fs2.existsSync(watchPath)) {
601
+ if (fs3.existsSync(watchPath)) {
467
602
  server.watcher.add(watchPath);
468
- server.watcher.on("change", (changedPath) => {
469
- if (changedPath === watchPath) {
470
- if (debug) {
471
- console.log("[dcs-seo] seo.yaml changed, triggering reload");
472
- }
473
- server.restart();
474
- }
475
- });
476
603
  }
477
604
  });
605
+ let reloadTimer;
606
+ server.watcher.on("change", (changedPath) => {
607
+ if (!watchPaths.includes(path3.normalize(changedPath))) return;
608
+ if (reloadTimer) clearTimeout(reloadTimer);
609
+ reloadTimer = setTimeout(() => {
610
+ if (debug) {
611
+ console.log("[dcs-seo] seo.yaml changed, sending full-reload");
612
+ }
613
+ server.ws.send({ type: "full-reload" });
614
+ }, 100);
615
+ });
478
616
  }
479
617
  };
480
618
  }
@@ -486,11 +624,24 @@ function dcsEditorPlugin(options = {}) {
486
624
  const RIBBON_VIRTUAL_PATH = "/__dcs-preview-ribbon.js";
487
625
  let isDev = false;
488
626
  let basePath = "/";
627
+ let isPreview = false;
489
628
  return {
490
629
  name: "dcs-editor-bridge",
630
+ // Preview only: stop Vite running an unreachable HMR WebSocket server.
631
+ // Freshness comes from the cms content/seo plugins (fresh transformIndexHtml
632
+ // re-read) + the portal's ?_hc hard reload, not HMR. NOTE: this alone does
633
+ // NOT silence the browser — Vite still injects @vite/client and calls
634
+ // connect(); the client-side stub in transformIndexHtml is what removes the
635
+ // console errors. A normal local `vite dev` (no DCS_PREVIEW) keeps full HMR.
636
+ config(_config, env) {
637
+ if (env.command === "serve" && process.env.DCS_PREVIEW === "true") {
638
+ return { server: { hmr: false } };
639
+ }
640
+ },
491
641
  configResolved(config) {
492
642
  isDev = config.command === "serve";
493
643
  basePath = config.base ?? "/";
644
+ isPreview = process.env.DCS_PREVIEW === "true";
494
645
  },
495
646
  resolveId(id) {
496
647
  if (id === VIRTUAL_PATH || id === RIBBON_VIRTUAL_PATH) {
@@ -558,9 +709,15 @@ if (document.readyState === 'loading') {
558
709
  }
559
710
  const editorTag = `<script type="module" src="${VIRTUAL_PATH}"></script>`;
560
711
  const ribbonTag = `<script type="module" src="${RIBBON_VIRTUAL_PATH}"></script>`;
561
- return html.replace("</body>", `${editorTag}
712
+ const bodyHtml = html.replace("</body>", `${editorTag}
562
713
  ${ribbonTag}
563
714
  </body>`);
715
+ if (!isPreview) return bodyHtml;
716
+ const wsStub = `(function(){try{var N=window.WebSocket;if(!N)return;function hmr(p){return p==='vite-hmr'||(Array.isArray(p)&&p.indexOf('vite-hmr')!==-1)}function Inert(){this.readyState=0}Inert.prototype.send=function(){};Inert.prototype.close=function(){};Inert.prototype.addEventListener=function(){};Inert.prototype.removeEventListener=function(){};var W=function(u,p){if(hmr(p))return new Inert();return new N(u,p)};W.prototype=N.prototype;W.CONNECTING=N.CONNECTING;W.OPEN=N.OPEN;W.CLOSING=N.CLOSING;W.CLOSED=N.CLOSED;window.WebSocket=W}catch(e){}})()`;
717
+ return {
718
+ html: bodyHtml,
719
+ tags: [{ tag: "script", children: wsStub, injectTo: "head-prepend" }]
720
+ };
564
721
  }
565
722
  }
566
723
  };
@@ -604,14 +761,14 @@ function buildPictureElement(entry, alt, extraAttrs, sizes) {
604
761
  }
605
762
  function loadCdnImageMap(projectRoot, relativeMapPath, debug) {
606
763
  const possiblePaths = [
607
- path2.resolve(projectRoot, relativeMapPath),
608
- path2.resolve(projectRoot, "..", relativeMapPath),
609
- path2.resolve(process.cwd(), relativeMapPath)
764
+ path3.resolve(projectRoot, relativeMapPath),
765
+ path3.resolve(projectRoot, "..", relativeMapPath),
766
+ path3.resolve(process.cwd(), relativeMapPath)
610
767
  ];
611
768
  for (const testPath of possiblePaths) {
612
- if (fs2.existsSync(testPath)) {
769
+ if (fs3.existsSync(testPath)) {
613
770
  try {
614
- const raw = fs2.readFileSync(testPath, "utf8");
771
+ const raw = fs3.readFileSync(testPath, "utf8");
615
772
  const data = JSON.parse(raw);
616
773
  const map = /* @__PURE__ */ new Map();
617
774
  for (const entry of data.images) {
@@ -772,13 +929,13 @@ function dcsCdnBuildEnd(options = {}) {
772
929
  return;
773
930
  }
774
931
  const outDir = siteConfig.outDir;
775
- if (!fs2.existsSync(outDir)) return;
932
+ if (!fs3.existsSync(outDir)) return;
776
933
  let filesProcessed = 0;
777
934
  let refsReplaced = 0;
778
935
  function walkDir(dir) {
779
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
936
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
780
937
  for (const entry of entries) {
781
- const fullPath = path2.join(dir, entry.name);
938
+ const fullPath = path3.join(dir, entry.name);
782
939
  if (entry.isDirectory()) {
783
940
  walkDir(fullPath);
784
941
  } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -787,7 +944,7 @@ function dcsCdnBuildEnd(options = {}) {
787
944
  }
788
945
  }
789
946
  function processFile(filePath) {
790
- let content = fs2.readFileSync(filePath, "utf8");
947
+ let content = fs3.readFileSync(filePath, "utf8");
791
948
  if (!pathPrefixes.some((p) => content.includes(p))) return;
792
949
  let changed = false;
793
950
  if (filePath.endsWith(".html")) {
@@ -822,7 +979,7 @@ function dcsCdnBuildEnd(options = {}) {
822
979
  }
823
980
  }
824
981
  if (changed) {
825
- fs2.writeFileSync(filePath, content, "utf8");
982
+ fs3.writeFileSync(filePath, content, "utf8");
826
983
  filesProcessed++;
827
984
  }
828
985
  }