@deot/docs-dever 1.1.1 → 1.1.3

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
@@ -54,6 +54,15 @@ development 和 preview 会保持服务运行;build 在 Vite 构建结束后
54
54
  - 为客户端路由提供 HTML history fallback;静态托管若只能配置自定义 404 页,请改用 Client 的 [`@deot/docs:redirect`](../client/README.md) 约定。
55
55
  - 在 build 模式复制 Markdown、JSON(含 `.page.json`)、SFC 等静态内容资源;
56
56
  - 当当前仓库源码存在时,将 locale、renderer、markdown、playground、theme 包映射到本地入口。
57
+ - 在 development 模式提供 `GET /__docs/module/<包名>`,把工作区源码编成一份浏览器 ESM。preview 与 build 对该地址返回 404。
58
+
59
+ ## 本地模块
60
+
61
+ `doc dev` 扫描项目 `packages/*/package.json`。存在 `src/index.ts` 或 `index.ts`,且真实路径仍在项目内时,可以用 `/__docs/module/<包名>` 取到编译结果。例如 `@deot/docs-locale` 对应 `/__docs/module/@deot/docs-locale`。
62
+
63
+ 当前包内的相对 `.vue`、`.ts`、`.tsx` 和样式会打进这一份 ESM,样式写入 iframe 的 `document.head`。`vue`、其它工作区包,以及其余裸导入保持原样,交给 Playground import map。`vue/jsx-runtime` 会打进包。
64
+
65
+ 站点自己决定是否使用这些地址。本仓库的 `index.html` 在 `window.__DOCS_RUNTIME__?.mode !== 'development'` 时把 `modules` 留空,development 时改指本地地址。直接打开 HTML 没有 runtime,因此仍走内置 CDN。管理页里对同名包的覆盖会压过 `index.html`。改完源码后刷新文档页;已打开的 Playground 不会热更新这份外部模块。
57
66
 
58
67
  ### `getDeverMode(options)`
59
68
 
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ _vitejs_plugin_vue_jsx = __toESM(_vitejs_plugin_vue_jsx, 1);
35
35
  let node_fs = require("node:fs");
36
36
  node_fs = __toESM(node_fs, 1);
37
37
  let node_crypto = require("node:crypto");
38
+ let node_module = require("node:module");
38
39
  let node_http = require("node:http");
39
40
  node_http = __toESM(node_http, 1);
40
41
  //#region packages/dever/src/workspace.ts
@@ -99,6 +100,272 @@ var resolveDocsWorkspace = (cwd = process.cwd(), workspace) => {
99
100
  node_path.join(projectRoot, "index.html")
100
101
  ].join(" "));
101
102
  };
103
+ var BUNDLED_SPECIFIERS = /* @__PURE__ */ new Set(["vue/jsx-runtime", "vue/jsx-dev-runtime"]);
104
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
105
+ "coverage",
106
+ "dist",
107
+ "node_modules"
108
+ ]);
109
+ var cache = /* @__PURE__ */ new Map();
110
+ var pending = /* @__PURE__ */ new Map();
111
+ var buildCount = 0;
112
+ var readPackageName = (manifestPath) => {
113
+ try {
114
+ const manifest = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
115
+ return typeof manifest.name === "string" && manifest.name ? manifest.name : "";
116
+ } catch {
117
+ return "";
118
+ }
119
+ };
120
+ var resolveSourceEntry = (directory) => {
121
+ for (const relative of ["src/index.ts", "index.ts"]) {
122
+ const candidate = node_path.resolve(directory, relative);
123
+ if (!node_fs.existsSync(candidate) || !node_fs.statSync(candidate).isFile()) continue;
124
+ const realEntry = node_fs.realpathSync(candidate);
125
+ if (!isInside(directory, realEntry)) continue;
126
+ return realEntry;
127
+ }
128
+ return "";
129
+ };
130
+ /**
131
+ * 收集项目 `packages/*` 里能编成浏览器模块的包。只接受留在项目内的真实路径。
132
+ * @param projectRoot 文档项目根目录。
133
+ * @returns 包名到源码入口的映射。同名包保留先扫描到的一项。
134
+ */
135
+ var resolveWorkspaceModuleEntries = (projectRoot) => {
136
+ const realRoot = node_fs.existsSync(projectRoot) ? node_fs.realpathSync(projectRoot) : node_path.resolve(projectRoot);
137
+ const packagesDir = node_path.resolve(realRoot, "packages");
138
+ if (!node_fs.existsSync(packagesDir) || !node_fs.statSync(packagesDir).isDirectory()) return [];
139
+ const realPackages = node_fs.realpathSync(packagesDir);
140
+ if (!isInside(realRoot, realPackages)) return [];
141
+ const seen = /* @__PURE__ */ new Set();
142
+ const entries = [];
143
+ for (const item of node_fs.readdirSync(packagesDir, { withFileTypes: true })) {
144
+ if (!item.isDirectory() && !item.isSymbolicLink()) continue;
145
+ const directory = node_path.resolve(packagesDir, item.name);
146
+ if (!node_fs.existsSync(directory) || !node_fs.statSync(directory).isDirectory()) continue;
147
+ const realDirectory = node_fs.realpathSync(directory);
148
+ if (!isInside(realPackages, realDirectory)) continue;
149
+ const name = readPackageName(node_path.join(realDirectory, "package.json"));
150
+ const entry = name ? resolveSourceEntry(realDirectory) : "";
151
+ if (!name || !entry || seen.has(name)) continue;
152
+ seen.add(name);
153
+ entries.push({
154
+ name,
155
+ entry,
156
+ directory: realDirectory
157
+ });
158
+ }
159
+ return entries;
160
+ };
161
+ var directorySignature = (directory) => {
162
+ const files = [];
163
+ const walk = (current) => {
164
+ const realCurrent = node_fs.realpathSync(current);
165
+ if (!isInside(directory, realCurrent)) return;
166
+ for (const item of node_fs.readdirSync(realCurrent, { withFileTypes: true })) {
167
+ if (item.name === ".git" || IGNORED_DIRECTORIES.has(item.name)) continue;
168
+ const filename = node_path.join(realCurrent, item.name);
169
+ if (item.isSymbolicLink()) {
170
+ if (!node_fs.existsSync(filename)) continue;
171
+ const realName = node_fs.realpathSync(filename);
172
+ if (!isInside(directory, realName)) continue;
173
+ const stat = node_fs.statSync(realName);
174
+ if (stat.isDirectory()) walk(realName);
175
+ else if (stat.isFile()) files.push(`${realName}:${stat.mtimeMs}`);
176
+ continue;
177
+ }
178
+ if (item.isDirectory()) {
179
+ walk(filename);
180
+ continue;
181
+ }
182
+ if (item.isFile()) files.push(`${filename}:${node_fs.statSync(filename).mtimeMs}`);
183
+ }
184
+ };
185
+ walk(directory);
186
+ files.sort();
187
+ return files.join("\n");
188
+ };
189
+ var isWorkspaceModuleExternal = (id) => {
190
+ if (!id || id.startsWith("\0") || id.startsWith(".") || node_path.isAbsolute(id)) return false;
191
+ if (BUNDLED_SPECIFIERS.has(id)) return false;
192
+ return true;
193
+ };
194
+ var readAssetSource = (source) => typeof source === "string" ? source : Buffer.from(source).toString("utf8");
195
+ var toBundleResult = (result) => {
196
+ const output = Array.isArray(result) ? result[0] : result;
197
+ if (!output || !("output" in output) || !Array.isArray(output.output)) throw new Error("Module build did not produce a bundle");
198
+ return output;
199
+ };
200
+ var injectStyles = (name, code, css) => {
201
+ if (!css) return code;
202
+ return [
203
+ "if (typeof document !== \"undefined\") {",
204
+ " const style = document.createElement(\"style\");",
205
+ `\tstyle.setAttribute("data-docs-module", ${JSON.stringify(name)});`,
206
+ `\tstyle.textContent = ${JSON.stringify(css)};`,
207
+ " document.head.appendChild(style);",
208
+ "}",
209
+ code
210
+ ].join("\n");
211
+ };
212
+ var buildWorkspaceModule = async (projectRoot, entry) => {
213
+ buildCount += 1;
214
+ const bundle = toBundleResult(await (0, vite.build)({
215
+ configFile: false,
216
+ envFile: false,
217
+ root: projectRoot,
218
+ publicDir: false,
219
+ logLevel: "silent",
220
+ mode: "development",
221
+ appType: "custom",
222
+ define: { "process.env.NODE_ENV": JSON.stringify("development") },
223
+ plugins: [(0, _vitejs_plugin_vue.default)(), (0, _vitejs_plugin_vue_jsx.default)()],
224
+ build: {
225
+ write: false,
226
+ minify: false,
227
+ cssMinify: false,
228
+ cssCodeSplit: false,
229
+ sourcemap: false,
230
+ emptyOutDir: false,
231
+ copyPublicDir: false,
232
+ modulePreload: false,
233
+ reportCompressedSize: false,
234
+ assetsInlineLimit: 1e8,
235
+ chunkSizeWarningLimit: 1e5,
236
+ rolldownOptions: {
237
+ input: entry.entry,
238
+ preserveEntrySignatures: "strict",
239
+ treeshake: false,
240
+ external: isWorkspaceModuleExternal,
241
+ output: {
242
+ format: "es",
243
+ codeSplitting: false,
244
+ entryFileNames: "module.js"
245
+ }
246
+ }
247
+ }
248
+ }));
249
+ const chunks = bundle.output.filter((item) => item.type === "chunk");
250
+ if (chunks.length !== 1) throw new Error(`Expected a single module chunk, got ${chunks.length}`);
251
+ const css = bundle.output.filter((item) => item.type === "asset" && item.fileName.endsWith(".css")).map((item) => readAssetSource(item.source)).join("\n");
252
+ return injectStyles(entry.name, chunks[0].code, css);
253
+ };
254
+ /**
255
+ * 把工作区包编成一份浏览器 ESM。`vue` 和其它工作区包保持裸导入。
256
+ * @param projectRoot 用于解析 node_modules 的项目根。
257
+ * @param entry 已通过边界检查的包入口。
258
+ * @returns 可直接交给浏览器执行的模块源码。
259
+ */
260
+ var compileWorkspaceModule = (projectRoot, entry) => {
261
+ const signature = directorySignature(entry.directory);
262
+ const cached = cache.get(entry.name);
263
+ if (cached?.signature === signature) return Promise.resolve(cached.code);
264
+ const inflight = pending.get(entry.name);
265
+ if (inflight) return inflight;
266
+ const task = buildWorkspaceModule(projectRoot, entry).then((code) => {
267
+ cache.set(entry.name, {
268
+ signature: directorySignature(entry.directory),
269
+ code
270
+ });
271
+ return code;
272
+ }).finally(() => {
273
+ if (pending.get(entry.name) === task) pending.delete(entry.name);
274
+ });
275
+ pending.set(entry.name, task);
276
+ return task;
277
+ };
278
+ var moduleFailure = (message) => `throw new Error(${JSON.stringify(message)});`;
279
+ /**
280
+ * 响应 `/__docs/module/<包名>`。未知包返回 404;编译失败返回会抛错的 ESM。
281
+ * @param req 开发服务请求。
282
+ * @param res 开发服务响应。
283
+ * @param projectRoot 文档项目根目录。
284
+ * @returns 是否已处理该请求。路径不匹配时为 false。
285
+ */
286
+ var respondWorkspaceModule = async (req, res, projectRoot) => {
287
+ const pathname = (req.url || "/").split(/[?#]/u, 1)[0] || "/";
288
+ if (!pathname.startsWith("/__docs/module/")) return false;
289
+ if (req.method !== "GET" && req.method !== "HEAD") {
290
+ res.statusCode = 405;
291
+ res.setHeader("Allow", "GET, HEAD");
292
+ res.end("Method Not Allowed");
293
+ return true;
294
+ }
295
+ let name = "";
296
+ try {
297
+ name = decodeURIComponent(pathname.slice(15));
298
+ } catch {
299
+ res.statusCode = 400;
300
+ res.end("Bad Request");
301
+ return true;
302
+ }
303
+ if (!name || name.split("/").includes("..") || name.includes("\\") || name.includes("\0")) {
304
+ res.statusCode = 404;
305
+ res.end("Not Found");
306
+ return true;
307
+ }
308
+ const entry = resolveWorkspaceModuleEntries(projectRoot).find((item) => item.name === name);
309
+ if (!entry) {
310
+ res.statusCode = 404;
311
+ res.end("Not Found");
312
+ return true;
313
+ }
314
+ let code;
315
+ try {
316
+ code = await compileWorkspaceModule(projectRoot, entry);
317
+ } catch (reason) {
318
+ code = moduleFailure(reason instanceof Error ? reason.message : "Module compile failed");
319
+ }
320
+ res.statusCode = 200;
321
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
322
+ res.setHeader("Cache-Control", "no-store");
323
+ if (req.method === "HEAD") res.end();
324
+ else res.end(code);
325
+ return true;
326
+ };
327
+ //#endregion
328
+ //#region packages/dever/src/plugins/optional-dependencies.ts
329
+ var resolveOptionalDependency = (id, importer) => {
330
+ if (!importer || !node_path.isAbsolute(importer) || /^[./]|:/.test(id)) return;
331
+ const filename = importer.split("?")[0];
332
+ const name = id.startsWith("@") ? id.split("/").slice(0, 2).join("/") : id.split("/")[0];
333
+ let directory = node_path.dirname(filename);
334
+ while (true) {
335
+ const manifest = node_path.join(directory, "package.json");
336
+ if (node_fs.existsSync(manifest)) {
337
+ const pkg = JSON.parse(node_fs.readFileSync(manifest, "utf8"));
338
+ if (!pkg.peerDependencies?.[name] || !pkg.peerDependenciesMeta?.[name]?.optional) return;
339
+ try {
340
+ (0, node_module.createRequire)(filename).resolve(id);
341
+ } catch (reason) {
342
+ if (reason.code === "MODULE_NOT_FOUND") return {
343
+ id: `https://cdn.jsdelivr.net/npm/${id}/+esm`,
344
+ external: true
345
+ };
346
+ }
347
+ return;
348
+ }
349
+ const parent = node_path.dirname(directory);
350
+ if (parent === directory) return;
351
+ directory = parent;
352
+ }
353
+ };
354
+ /**
355
+ * 本地包未安装的 optional peer 由浏览器按需从 CDN 加载。
356
+ * @returns 同时用于 Vite 模块转换与依赖预打包的解析插件。
357
+ */
358
+ var createOptionalDependenciesPlugin = () => {
359
+ const resolver = {
360
+ name: "docs-optional-dependencies",
361
+ resolveId: resolveOptionalDependency
362
+ };
363
+ return {
364
+ ...resolver,
365
+ enforce: "pre",
366
+ config: () => ({ optimizeDeps: { rolldownOptions: { plugins: [resolver] } } })
367
+ };
368
+ };
102
369
  //#endregion
103
370
  //#region packages/dever/src/plugins/index.ts
104
371
  var innerPathRegex = /^\/(@|__)/;
@@ -298,6 +565,15 @@ var configureWorkspaceServer = (options, resolvedWorkspace) => (server) => {
298
565
  let realResourceRoot = realWorkspace;
299
566
  try {
300
567
  const rawPathname = getRawPathname(req.url || "/");
568
+ if (/^\/__docs\/preview(?:-config-generate)?$/.test(rawPathname) && String(req.headers.accept || "").includes("text/html")) return next();
569
+ if (!options.preview && rawPathname.startsWith("/__docs/module/")) {
570
+ respondWorkspaceModule(req, res, resolved.projectRoot).catch((reason) => {
571
+ if (res.writableEnded) return;
572
+ res.statusCode = 500;
573
+ res.end(reason instanceof Error ? reason.message : "Module compile failed");
574
+ });
575
+ return;
576
+ }
301
577
  if (rawPathname.startsWith("/__docs/") && rawPathname !== "/__docs/events") {
302
578
  res.statusCode = 404;
303
579
  res.end("Not Found");
@@ -469,15 +745,17 @@ var createHistoryPlugin = (options, resolvedWorkspace) => ({
469
745
  server.middlewares.use(async (req, res, next) => {
470
746
  const requestUrl = req.url || "/";
471
747
  const url = requestUrl.replace(/\/+/g, "/").replace(/[?#].*$/s, "") || "";
748
+ const accept = String(req.headers?.accept || "text/html");
749
+ const isPreviewPage = /^\/__docs\/preview(?:-config-generate)?$/.test(url) && accept.includes("text/html");
472
750
  const candidate = node_path.resolve(root, `.${url}`);
473
751
  const isFile = node_fs.existsSync(candidate) && node_fs.statSync(candidate).isFile();
474
- if (options.preview && url.startsWith("/__docs/")) {
752
+ if (options.preview && url.startsWith("/__docs/") && !isPreviewPage) {
475
753
  res.statusCode = 404;
476
754
  res.end("Not Found");
477
755
  return;
478
756
  }
479
- if (res.writableEnded || requestUrl.includes("html-proxy&") || innerPathRegex.test(url) || isFile) return next();
480
- if (!String(req.headers?.accept || "text/html").includes("text/html")) return next();
757
+ if (res.writableEnded || requestUrl.includes("html-proxy&") || innerPathRegex.test(url) && !isPreviewPage || isFile) return next();
758
+ if (!accept.includes("text/html")) return next();
481
759
  const indexFile = resolvedWorkspace?.entry || resolveDocsWorkspace(options.preview ? root : process.cwd(), options.preview ? "." : options.workspace).entry;
482
760
  if (!node_fs.existsSync(indexFile)) {
483
761
  res.statusCode = 404;
@@ -542,6 +820,7 @@ var createStaticCopyPlugin = (options, resolvedWorkspace) => ({
542
820
  }
543
821
  });
544
822
  var plugins_default = (options = {}, resolvedWorkspace) => (0, vite.defineConfig)({ plugins: [
823
+ createOptionalDependenciesPlugin(),
545
824
  createRuntimePlugin(options, resolvedWorkspace),
546
825
  createWorkspacePlugin(options, resolvedWorkspace),
547
826
  createHistoryPlugin(options, resolvedWorkspace),
@@ -706,7 +985,8 @@ var createPreviewRequestHandler = (options) => {
706
985
  return;
707
986
  }
708
987
  const pathname = decodeRequestPath(req.url || "/");
709
- if (pathname.startsWith("/__docs/")) {
988
+ const isPreviewPage = /^\/__docs\/preview(?:-config-generate)?$/.test(pathname) && String(req.headers.accept || "").includes("text/html");
989
+ if (pathname.startsWith("/__docs/") && !isPreviewPage) {
710
990
  res.statusCode = 404;
711
991
  res.end("Not Found");
712
992
  return;
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import vue from "@vitejs/plugin-vue";
7
7
  import vueJsx from "@vitejs/plugin-vue-jsx";
8
8
  import * as fs$1 from "node:fs";
9
9
  import { createHash } from "node:crypto";
10
+ import { createRequire } from "node:module";
10
11
  import * as http from "node:http";
11
12
  //#region packages/dever/src/workspace.ts
12
13
  /**
@@ -70,6 +71,272 @@ var resolveDocsWorkspace = (cwd = process.cwd(), workspace) => {
70
71
  path.join(projectRoot, "index.html")
71
72
  ].join(" "));
72
73
  };
74
+ var BUNDLED_SPECIFIERS = /* @__PURE__ */ new Set(["vue/jsx-runtime", "vue/jsx-dev-runtime"]);
75
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
76
+ "coverage",
77
+ "dist",
78
+ "node_modules"
79
+ ]);
80
+ var cache = /* @__PURE__ */ new Map();
81
+ var pending = /* @__PURE__ */ new Map();
82
+ var buildCount = 0;
83
+ var readPackageName = (manifestPath) => {
84
+ try {
85
+ const manifest = JSON.parse(fs$1.readFileSync(manifestPath, "utf8"));
86
+ return typeof manifest.name === "string" && manifest.name ? manifest.name : "";
87
+ } catch {
88
+ return "";
89
+ }
90
+ };
91
+ var resolveSourceEntry = (directory) => {
92
+ for (const relative of ["src/index.ts", "index.ts"]) {
93
+ const candidate = path.resolve(directory, relative);
94
+ if (!fs$1.existsSync(candidate) || !fs$1.statSync(candidate).isFile()) continue;
95
+ const realEntry = fs$1.realpathSync(candidate);
96
+ if (!isInside(directory, realEntry)) continue;
97
+ return realEntry;
98
+ }
99
+ return "";
100
+ };
101
+ /**
102
+ * 收集项目 `packages/*` 里能编成浏览器模块的包。只接受留在项目内的真实路径。
103
+ * @param projectRoot 文档项目根目录。
104
+ * @returns 包名到源码入口的映射。同名包保留先扫描到的一项。
105
+ */
106
+ var resolveWorkspaceModuleEntries = (projectRoot) => {
107
+ const realRoot = fs$1.existsSync(projectRoot) ? fs$1.realpathSync(projectRoot) : path.resolve(projectRoot);
108
+ const packagesDir = path.resolve(realRoot, "packages");
109
+ if (!fs$1.existsSync(packagesDir) || !fs$1.statSync(packagesDir).isDirectory()) return [];
110
+ const realPackages = fs$1.realpathSync(packagesDir);
111
+ if (!isInside(realRoot, realPackages)) return [];
112
+ const seen = /* @__PURE__ */ new Set();
113
+ const entries = [];
114
+ for (const item of fs$1.readdirSync(packagesDir, { withFileTypes: true })) {
115
+ if (!item.isDirectory() && !item.isSymbolicLink()) continue;
116
+ const directory = path.resolve(packagesDir, item.name);
117
+ if (!fs$1.existsSync(directory) || !fs$1.statSync(directory).isDirectory()) continue;
118
+ const realDirectory = fs$1.realpathSync(directory);
119
+ if (!isInside(realPackages, realDirectory)) continue;
120
+ const name = readPackageName(path.join(realDirectory, "package.json"));
121
+ const entry = name ? resolveSourceEntry(realDirectory) : "";
122
+ if (!name || !entry || seen.has(name)) continue;
123
+ seen.add(name);
124
+ entries.push({
125
+ name,
126
+ entry,
127
+ directory: realDirectory
128
+ });
129
+ }
130
+ return entries;
131
+ };
132
+ var directorySignature = (directory) => {
133
+ const files = [];
134
+ const walk = (current) => {
135
+ const realCurrent = fs$1.realpathSync(current);
136
+ if (!isInside(directory, realCurrent)) return;
137
+ for (const item of fs$1.readdirSync(realCurrent, { withFileTypes: true })) {
138
+ if (item.name === ".git" || IGNORED_DIRECTORIES.has(item.name)) continue;
139
+ const filename = path.join(realCurrent, item.name);
140
+ if (item.isSymbolicLink()) {
141
+ if (!fs$1.existsSync(filename)) continue;
142
+ const realName = fs$1.realpathSync(filename);
143
+ if (!isInside(directory, realName)) continue;
144
+ const stat = fs$1.statSync(realName);
145
+ if (stat.isDirectory()) walk(realName);
146
+ else if (stat.isFile()) files.push(`${realName}:${stat.mtimeMs}`);
147
+ continue;
148
+ }
149
+ if (item.isDirectory()) {
150
+ walk(filename);
151
+ continue;
152
+ }
153
+ if (item.isFile()) files.push(`${filename}:${fs$1.statSync(filename).mtimeMs}`);
154
+ }
155
+ };
156
+ walk(directory);
157
+ files.sort();
158
+ return files.join("\n");
159
+ };
160
+ var isWorkspaceModuleExternal = (id) => {
161
+ if (!id || id.startsWith("\0") || id.startsWith(".") || path.isAbsolute(id)) return false;
162
+ if (BUNDLED_SPECIFIERS.has(id)) return false;
163
+ return true;
164
+ };
165
+ var readAssetSource = (source) => typeof source === "string" ? source : Buffer.from(source).toString("utf8");
166
+ var toBundleResult = (result) => {
167
+ const output = Array.isArray(result) ? result[0] : result;
168
+ if (!output || !("output" in output) || !Array.isArray(output.output)) throw new Error("Module build did not produce a bundle");
169
+ return output;
170
+ };
171
+ var injectStyles = (name, code, css) => {
172
+ if (!css) return code;
173
+ return [
174
+ "if (typeof document !== \"undefined\") {",
175
+ " const style = document.createElement(\"style\");",
176
+ `\tstyle.setAttribute("data-docs-module", ${JSON.stringify(name)});`,
177
+ `\tstyle.textContent = ${JSON.stringify(css)};`,
178
+ " document.head.appendChild(style);",
179
+ "}",
180
+ code
181
+ ].join("\n");
182
+ };
183
+ var buildWorkspaceModule = async (projectRoot, entry) => {
184
+ buildCount += 1;
185
+ const bundle = toBundleResult(await build({
186
+ configFile: false,
187
+ envFile: false,
188
+ root: projectRoot,
189
+ publicDir: false,
190
+ logLevel: "silent",
191
+ mode: "development",
192
+ appType: "custom",
193
+ define: { "process.env.NODE_ENV": JSON.stringify("development") },
194
+ plugins: [vue(), vueJsx()],
195
+ build: {
196
+ write: false,
197
+ minify: false,
198
+ cssMinify: false,
199
+ cssCodeSplit: false,
200
+ sourcemap: false,
201
+ emptyOutDir: false,
202
+ copyPublicDir: false,
203
+ modulePreload: false,
204
+ reportCompressedSize: false,
205
+ assetsInlineLimit: 1e8,
206
+ chunkSizeWarningLimit: 1e5,
207
+ rolldownOptions: {
208
+ input: entry.entry,
209
+ preserveEntrySignatures: "strict",
210
+ treeshake: false,
211
+ external: isWorkspaceModuleExternal,
212
+ output: {
213
+ format: "es",
214
+ codeSplitting: false,
215
+ entryFileNames: "module.js"
216
+ }
217
+ }
218
+ }
219
+ }));
220
+ const chunks = bundle.output.filter((item) => item.type === "chunk");
221
+ if (chunks.length !== 1) throw new Error(`Expected a single module chunk, got ${chunks.length}`);
222
+ const css = bundle.output.filter((item) => item.type === "asset" && item.fileName.endsWith(".css")).map((item) => readAssetSource(item.source)).join("\n");
223
+ return injectStyles(entry.name, chunks[0].code, css);
224
+ };
225
+ /**
226
+ * 把工作区包编成一份浏览器 ESM。`vue` 和其它工作区包保持裸导入。
227
+ * @param projectRoot 用于解析 node_modules 的项目根。
228
+ * @param entry 已通过边界检查的包入口。
229
+ * @returns 可直接交给浏览器执行的模块源码。
230
+ */
231
+ var compileWorkspaceModule = (projectRoot, entry) => {
232
+ const signature = directorySignature(entry.directory);
233
+ const cached = cache.get(entry.name);
234
+ if (cached?.signature === signature) return Promise.resolve(cached.code);
235
+ const inflight = pending.get(entry.name);
236
+ if (inflight) return inflight;
237
+ const task = buildWorkspaceModule(projectRoot, entry).then((code) => {
238
+ cache.set(entry.name, {
239
+ signature: directorySignature(entry.directory),
240
+ code
241
+ });
242
+ return code;
243
+ }).finally(() => {
244
+ if (pending.get(entry.name) === task) pending.delete(entry.name);
245
+ });
246
+ pending.set(entry.name, task);
247
+ return task;
248
+ };
249
+ var moduleFailure = (message) => `throw new Error(${JSON.stringify(message)});`;
250
+ /**
251
+ * 响应 `/__docs/module/<包名>`。未知包返回 404;编译失败返回会抛错的 ESM。
252
+ * @param req 开发服务请求。
253
+ * @param res 开发服务响应。
254
+ * @param projectRoot 文档项目根目录。
255
+ * @returns 是否已处理该请求。路径不匹配时为 false。
256
+ */
257
+ var respondWorkspaceModule = async (req, res, projectRoot) => {
258
+ const pathname = (req.url || "/").split(/[?#]/u, 1)[0] || "/";
259
+ if (!pathname.startsWith("/__docs/module/")) return false;
260
+ if (req.method !== "GET" && req.method !== "HEAD") {
261
+ res.statusCode = 405;
262
+ res.setHeader("Allow", "GET, HEAD");
263
+ res.end("Method Not Allowed");
264
+ return true;
265
+ }
266
+ let name = "";
267
+ try {
268
+ name = decodeURIComponent(pathname.slice(15));
269
+ } catch {
270
+ res.statusCode = 400;
271
+ res.end("Bad Request");
272
+ return true;
273
+ }
274
+ if (!name || name.split("/").includes("..") || name.includes("\\") || name.includes("\0")) {
275
+ res.statusCode = 404;
276
+ res.end("Not Found");
277
+ return true;
278
+ }
279
+ const entry = resolveWorkspaceModuleEntries(projectRoot).find((item) => item.name === name);
280
+ if (!entry) {
281
+ res.statusCode = 404;
282
+ res.end("Not Found");
283
+ return true;
284
+ }
285
+ let code;
286
+ try {
287
+ code = await compileWorkspaceModule(projectRoot, entry);
288
+ } catch (reason) {
289
+ code = moduleFailure(reason instanceof Error ? reason.message : "Module compile failed");
290
+ }
291
+ res.statusCode = 200;
292
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
293
+ res.setHeader("Cache-Control", "no-store");
294
+ if (req.method === "HEAD") res.end();
295
+ else res.end(code);
296
+ return true;
297
+ };
298
+ //#endregion
299
+ //#region packages/dever/src/plugins/optional-dependencies.ts
300
+ var resolveOptionalDependency = (id, importer) => {
301
+ if (!importer || !path.isAbsolute(importer) || /^[./]|:/.test(id)) return;
302
+ const filename = importer.split("?")[0];
303
+ const name = id.startsWith("@") ? id.split("/").slice(0, 2).join("/") : id.split("/")[0];
304
+ let directory = path.dirname(filename);
305
+ while (true) {
306
+ const manifest = path.join(directory, "package.json");
307
+ if (fs$1.existsSync(manifest)) {
308
+ const pkg = JSON.parse(fs$1.readFileSync(manifest, "utf8"));
309
+ if (!pkg.peerDependencies?.[name] || !pkg.peerDependenciesMeta?.[name]?.optional) return;
310
+ try {
311
+ createRequire(filename).resolve(id);
312
+ } catch (reason) {
313
+ if (reason.code === "MODULE_NOT_FOUND") return {
314
+ id: `https://cdn.jsdelivr.net/npm/${id}/+esm`,
315
+ external: true
316
+ };
317
+ }
318
+ return;
319
+ }
320
+ const parent = path.dirname(directory);
321
+ if (parent === directory) return;
322
+ directory = parent;
323
+ }
324
+ };
325
+ /**
326
+ * 本地包未安装的 optional peer 由浏览器按需从 CDN 加载。
327
+ * @returns 同时用于 Vite 模块转换与依赖预打包的解析插件。
328
+ */
329
+ var createOptionalDependenciesPlugin = () => {
330
+ const resolver = {
331
+ name: "docs-optional-dependencies",
332
+ resolveId: resolveOptionalDependency
333
+ };
334
+ return {
335
+ ...resolver,
336
+ enforce: "pre",
337
+ config: () => ({ optimizeDeps: { rolldownOptions: { plugins: [resolver] } } })
338
+ };
339
+ };
73
340
  //#endregion
74
341
  //#region packages/dever/src/plugins/index.ts
75
342
  var innerPathRegex = /^\/(@|__)/;
@@ -269,6 +536,15 @@ var configureWorkspaceServer = (options, resolvedWorkspace) => (server) => {
269
536
  let realResourceRoot = realWorkspace;
270
537
  try {
271
538
  const rawPathname = getRawPathname(req.url || "/");
539
+ if (/^\/__docs\/preview(?:-config-generate)?$/.test(rawPathname) && String(req.headers.accept || "").includes("text/html")) return next();
540
+ if (!options.preview && rawPathname.startsWith("/__docs/module/")) {
541
+ respondWorkspaceModule(req, res, resolved.projectRoot).catch((reason) => {
542
+ if (res.writableEnded) return;
543
+ res.statusCode = 500;
544
+ res.end(reason instanceof Error ? reason.message : "Module compile failed");
545
+ });
546
+ return;
547
+ }
272
548
  if (rawPathname.startsWith("/__docs/") && rawPathname !== "/__docs/events") {
273
549
  res.statusCode = 404;
274
550
  res.end("Not Found");
@@ -440,15 +716,17 @@ var createHistoryPlugin = (options, resolvedWorkspace) => ({
440
716
  server.middlewares.use(async (req, res, next) => {
441
717
  const requestUrl = req.url || "/";
442
718
  const url = requestUrl.replace(/\/+/g, "/").replace(/[?#].*$/s, "") || "";
719
+ const accept = String(req.headers?.accept || "text/html");
720
+ const isPreviewPage = /^\/__docs\/preview(?:-config-generate)?$/.test(url) && accept.includes("text/html");
443
721
  const candidate = path.resolve(root, `.${url}`);
444
722
  const isFile = fs$1.existsSync(candidate) && fs$1.statSync(candidate).isFile();
445
- if (options.preview && url.startsWith("/__docs/")) {
723
+ if (options.preview && url.startsWith("/__docs/") && !isPreviewPage) {
446
724
  res.statusCode = 404;
447
725
  res.end("Not Found");
448
726
  return;
449
727
  }
450
- if (res.writableEnded || requestUrl.includes("html-proxy&") || innerPathRegex.test(url) || isFile) return next();
451
- if (!String(req.headers?.accept || "text/html").includes("text/html")) return next();
728
+ if (res.writableEnded || requestUrl.includes("html-proxy&") || innerPathRegex.test(url) && !isPreviewPage || isFile) return next();
729
+ if (!accept.includes("text/html")) return next();
452
730
  const indexFile = resolvedWorkspace?.entry || resolveDocsWorkspace(options.preview ? root : process.cwd(), options.preview ? "." : options.workspace).entry;
453
731
  if (!fs$1.existsSync(indexFile)) {
454
732
  res.statusCode = 404;
@@ -513,6 +791,7 @@ var createStaticCopyPlugin = (options, resolvedWorkspace) => ({
513
791
  }
514
792
  });
515
793
  var plugins_default = (options = {}, resolvedWorkspace) => defineConfig({ plugins: [
794
+ createOptionalDependenciesPlugin(),
516
795
  createRuntimePlugin(options, resolvedWorkspace),
517
796
  createWorkspacePlugin(options, resolvedWorkspace),
518
797
  createHistoryPlugin(options, resolvedWorkspace),
@@ -677,7 +956,8 @@ var createPreviewRequestHandler = (options) => {
677
956
  return;
678
957
  }
679
958
  const pathname = decodeRequestPath(req.url || "/");
680
- if (pathname.startsWith("/__docs/")) {
959
+ const isPreviewPage = /^\/__docs\/preview(?:-config-generate)?$/.test(pathname) && String(req.headers.accept || "").includes("text/html");
960
+ if (pathname.startsWith("/__docs/") && !isPreviewPage) {
681
961
  res.statusCode = 404;
682
962
  res.end("Not Found");
683
963
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deot/docs-dever",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",