@deot/docs-dever 1.1.0 → 1.1.2

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
@@ -99,6 +99,230 @@ var resolveDocsWorkspace = (cwd = process.cwd(), workspace) => {
99
99
  node_path.join(projectRoot, "index.html")
100
100
  ].join(" "));
101
101
  };
102
+ var BUNDLED_SPECIFIERS = /* @__PURE__ */ new Set(["vue/jsx-runtime", "vue/jsx-dev-runtime"]);
103
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
104
+ "coverage",
105
+ "dist",
106
+ "node_modules"
107
+ ]);
108
+ var cache = /* @__PURE__ */ new Map();
109
+ var pending = /* @__PURE__ */ new Map();
110
+ var buildCount = 0;
111
+ var readPackageName = (manifestPath) => {
112
+ try {
113
+ const manifest = JSON.parse(node_fs.readFileSync(manifestPath, "utf8"));
114
+ return typeof manifest.name === "string" && manifest.name ? manifest.name : "";
115
+ } catch {
116
+ return "";
117
+ }
118
+ };
119
+ var resolveSourceEntry = (directory) => {
120
+ for (const relative of ["src/index.ts", "index.ts"]) {
121
+ const candidate = node_path.resolve(directory, relative);
122
+ if (!node_fs.existsSync(candidate) || !node_fs.statSync(candidate).isFile()) continue;
123
+ const realEntry = node_fs.realpathSync(candidate);
124
+ if (!isInside(directory, realEntry)) continue;
125
+ return realEntry;
126
+ }
127
+ return "";
128
+ };
129
+ /**
130
+ * 收集项目 `packages/*` 里能编成浏览器模块的包。只接受留在项目内的真实路径。
131
+ * @param projectRoot 文档项目根目录。
132
+ * @returns 包名到源码入口的映射。同名包保留先扫描到的一项。
133
+ */
134
+ var resolveWorkspaceModuleEntries = (projectRoot) => {
135
+ const realRoot = node_fs.existsSync(projectRoot) ? node_fs.realpathSync(projectRoot) : node_path.resolve(projectRoot);
136
+ const packagesDir = node_path.resolve(realRoot, "packages");
137
+ if (!node_fs.existsSync(packagesDir) || !node_fs.statSync(packagesDir).isDirectory()) return [];
138
+ const realPackages = node_fs.realpathSync(packagesDir);
139
+ if (!isInside(realRoot, realPackages)) return [];
140
+ const seen = /* @__PURE__ */ new Set();
141
+ const entries = [];
142
+ for (const item of node_fs.readdirSync(packagesDir, { withFileTypes: true })) {
143
+ if (!item.isDirectory() && !item.isSymbolicLink()) continue;
144
+ const directory = node_path.resolve(packagesDir, item.name);
145
+ if (!node_fs.existsSync(directory) || !node_fs.statSync(directory).isDirectory()) continue;
146
+ const realDirectory = node_fs.realpathSync(directory);
147
+ if (!isInside(realPackages, realDirectory)) continue;
148
+ const name = readPackageName(node_path.join(realDirectory, "package.json"));
149
+ const entry = name ? resolveSourceEntry(realDirectory) : "";
150
+ if (!name || !entry || seen.has(name)) continue;
151
+ seen.add(name);
152
+ entries.push({
153
+ name,
154
+ entry,
155
+ directory: realDirectory
156
+ });
157
+ }
158
+ return entries;
159
+ };
160
+ var directorySignature = (directory) => {
161
+ const files = [];
162
+ const walk = (current) => {
163
+ const realCurrent = node_fs.realpathSync(current);
164
+ if (!isInside(directory, realCurrent)) return;
165
+ for (const item of node_fs.readdirSync(realCurrent, { withFileTypes: true })) {
166
+ if (item.name === ".git" || IGNORED_DIRECTORIES.has(item.name)) continue;
167
+ const filename = node_path.join(realCurrent, item.name);
168
+ if (item.isSymbolicLink()) {
169
+ if (!node_fs.existsSync(filename)) continue;
170
+ const realName = node_fs.realpathSync(filename);
171
+ if (!isInside(directory, realName)) continue;
172
+ const stat = node_fs.statSync(realName);
173
+ if (stat.isDirectory()) walk(realName);
174
+ else if (stat.isFile()) files.push(`${realName}:${stat.mtimeMs}`);
175
+ continue;
176
+ }
177
+ if (item.isDirectory()) {
178
+ walk(filename);
179
+ continue;
180
+ }
181
+ if (item.isFile()) files.push(`${filename}:${node_fs.statSync(filename).mtimeMs}`);
182
+ }
183
+ };
184
+ walk(directory);
185
+ files.sort();
186
+ return files.join("\n");
187
+ };
188
+ var isWorkspaceModuleExternal = (id) => {
189
+ if (!id || id.startsWith("\0") || id.startsWith(".") || node_path.isAbsolute(id)) return false;
190
+ if (BUNDLED_SPECIFIERS.has(id)) return false;
191
+ return true;
192
+ };
193
+ var readAssetSource = (source) => typeof source === "string" ? source : Buffer.from(source).toString("utf8");
194
+ var toBundleResult = (result) => {
195
+ const output = Array.isArray(result) ? result[0] : result;
196
+ if (!output || !("output" in output) || !Array.isArray(output.output)) throw new Error("Module build did not produce a bundle");
197
+ return output;
198
+ };
199
+ var injectStyles = (name, code, css) => {
200
+ if (!css) return code;
201
+ return [
202
+ "if (typeof document !== \"undefined\") {",
203
+ " const style = document.createElement(\"style\");",
204
+ `\tstyle.setAttribute("data-docs-module", ${JSON.stringify(name)});`,
205
+ `\tstyle.textContent = ${JSON.stringify(css)};`,
206
+ " document.head.appendChild(style);",
207
+ "}",
208
+ code
209
+ ].join("\n");
210
+ };
211
+ var buildWorkspaceModule = async (projectRoot, entry) => {
212
+ buildCount += 1;
213
+ const bundle = toBundleResult(await (0, vite.build)({
214
+ configFile: false,
215
+ envFile: false,
216
+ root: projectRoot,
217
+ publicDir: false,
218
+ logLevel: "silent",
219
+ mode: "development",
220
+ appType: "custom",
221
+ define: { "process.env.NODE_ENV": JSON.stringify("development") },
222
+ plugins: [(0, _vitejs_plugin_vue.default)(), (0, _vitejs_plugin_vue_jsx.default)()],
223
+ build: {
224
+ write: false,
225
+ minify: false,
226
+ cssMinify: false,
227
+ cssCodeSplit: false,
228
+ sourcemap: false,
229
+ emptyOutDir: false,
230
+ copyPublicDir: false,
231
+ modulePreload: false,
232
+ reportCompressedSize: false,
233
+ assetsInlineLimit: 1e8,
234
+ chunkSizeWarningLimit: 1e5,
235
+ rolldownOptions: {
236
+ input: entry.entry,
237
+ preserveEntrySignatures: "strict",
238
+ treeshake: false,
239
+ external: isWorkspaceModuleExternal,
240
+ output: {
241
+ format: "es",
242
+ codeSplitting: false,
243
+ entryFileNames: "module.js"
244
+ }
245
+ }
246
+ }
247
+ }));
248
+ const chunks = bundle.output.filter((item) => item.type === "chunk");
249
+ if (chunks.length !== 1) throw new Error(`Expected a single module chunk, got ${chunks.length}`);
250
+ const css = bundle.output.filter((item) => item.type === "asset" && item.fileName.endsWith(".css")).map((item) => readAssetSource(item.source)).join("\n");
251
+ return injectStyles(entry.name, chunks[0].code, css);
252
+ };
253
+ /**
254
+ * 把工作区包编成一份浏览器 ESM。`vue` 和其它工作区包保持裸导入。
255
+ * @param projectRoot 用于解析 node_modules 的项目根。
256
+ * @param entry 已通过边界检查的包入口。
257
+ * @returns 可直接交给浏览器执行的模块源码。
258
+ */
259
+ var compileWorkspaceModule = (projectRoot, entry) => {
260
+ const signature = directorySignature(entry.directory);
261
+ const cached = cache.get(entry.name);
262
+ if (cached?.signature === signature) return Promise.resolve(cached.code);
263
+ const inflight = pending.get(entry.name);
264
+ if (inflight) return inflight;
265
+ const task = buildWorkspaceModule(projectRoot, entry).then((code) => {
266
+ cache.set(entry.name, {
267
+ signature: directorySignature(entry.directory),
268
+ code
269
+ });
270
+ return code;
271
+ }).finally(() => {
272
+ if (pending.get(entry.name) === task) pending.delete(entry.name);
273
+ });
274
+ pending.set(entry.name, task);
275
+ return task;
276
+ };
277
+ var moduleFailure = (message) => `throw new Error(${JSON.stringify(message)});`;
278
+ /**
279
+ * 响应 `/__docs/module/<包名>`。未知包返回 404;编译失败返回会抛错的 ESM。
280
+ * @param req 开发服务请求。
281
+ * @param res 开发服务响应。
282
+ * @param projectRoot 文档项目根目录。
283
+ * @returns 是否已处理该请求。路径不匹配时为 false。
284
+ */
285
+ var respondWorkspaceModule = async (req, res, projectRoot) => {
286
+ const pathname = (req.url || "/").split(/[?#]/u, 1)[0] || "/";
287
+ if (!pathname.startsWith("/__docs/module/")) return false;
288
+ if (req.method !== "GET" && req.method !== "HEAD") {
289
+ res.statusCode = 405;
290
+ res.setHeader("Allow", "GET, HEAD");
291
+ res.end("Method Not Allowed");
292
+ return true;
293
+ }
294
+ let name = "";
295
+ try {
296
+ name = decodeURIComponent(pathname.slice(15));
297
+ } catch {
298
+ res.statusCode = 400;
299
+ res.end("Bad Request");
300
+ return true;
301
+ }
302
+ if (!name || name.split("/").includes("..") || name.includes("\\") || name.includes("\0")) {
303
+ res.statusCode = 404;
304
+ res.end("Not Found");
305
+ return true;
306
+ }
307
+ const entry = resolveWorkspaceModuleEntries(projectRoot).find((item) => item.name === name);
308
+ if (!entry) {
309
+ res.statusCode = 404;
310
+ res.end("Not Found");
311
+ return true;
312
+ }
313
+ let code;
314
+ try {
315
+ code = await compileWorkspaceModule(projectRoot, entry);
316
+ } catch (reason) {
317
+ code = moduleFailure(reason instanceof Error ? reason.message : "Module compile failed");
318
+ }
319
+ res.statusCode = 200;
320
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
321
+ res.setHeader("Cache-Control", "no-store");
322
+ if (req.method === "HEAD") res.end();
323
+ else res.end(code);
324
+ return true;
325
+ };
102
326
  //#endregion
103
327
  //#region packages/dever/src/plugins/index.ts
104
328
  var innerPathRegex = /^\/(@|__)/;
@@ -227,6 +451,12 @@ var decodeWorkspacePath = (requestUrl, urlPrefix) => {
227
451
  return decoded;
228
452
  };
229
453
  var isPackageReadme = (relative) => /^[^/]+\/README\.md$/u.test(relative);
454
+ /**
455
+ * 传统 workspace 把语言放在第一段目录,例如 `zh-CN/guide.md`。
456
+ * `docs/`、`src/` 这类共享目录不能当成语言,否则 SSE identity 对不上 Client。
457
+ * @param value
458
+ */
459
+ var isLocaleDirectory = (value) => /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/u.test(value);
230
460
  var getContentType = (extension) => {
231
461
  switch (extension) {
232
462
  case ".md": return "text/markdown; charset=utf-8";
@@ -292,6 +522,14 @@ var configureWorkspaceServer = (options, resolvedWorkspace) => (server) => {
292
522
  let realResourceRoot = realWorkspace;
293
523
  try {
294
524
  const rawPathname = getRawPathname(req.url || "/");
525
+ if (!options.preview && rawPathname.startsWith("/__docs/module/")) {
526
+ respondWorkspaceModule(req, res, resolved.projectRoot).catch((reason) => {
527
+ if (res.writableEnded) return;
528
+ res.statusCode = 500;
529
+ res.end(reason instanceof Error ? reason.message : "Module compile failed");
530
+ });
531
+ return;
532
+ }
295
533
  if (rawPathname.startsWith("/__docs/") && rawPathname !== "/__docs/events") {
296
534
  res.statusCode = 404;
297
535
  res.end("Not Found");
@@ -386,6 +624,8 @@ var configureEvents = (server, workspace, entry, packages, rootReadme) => {
386
624
  res.setHeader("Content-Type", "text/event-stream");
387
625
  res.setHeader("Cache-Control", "no-cache");
388
626
  res.setHeader("Connection", "keep-alive");
627
+ res.setHeader("X-Accel-Buffering", "no");
628
+ res.flushHeaders?.();
389
629
  res.write(": connected\n\n");
390
630
  clients.add(res);
391
631
  req.on("close", () => clients.delete(res));
@@ -430,11 +670,18 @@ var configureEvents = (server, workspace, entry, packages, rootReadme) => {
430
670
  }
431
671
  const [lang, ...segments] = relative.split("/");
432
672
  if (!lang || !segments.length) return;
433
- const payload = JSON.stringify({
673
+ const resourceType = getResourceType(relative);
674
+ const payload = JSON.stringify(isLocaleDirectory(lang) ? {
434
675
  type,
435
676
  lang,
436
677
  source: `./${segments.join("/")}`,
437
- resourceType: getResourceType(relative),
678
+ resourceType,
679
+ timestamp: Date.now()
680
+ } : {
681
+ type,
682
+ lang: "",
683
+ source: relative,
684
+ resourceType,
438
685
  timestamp: Date.now()
439
686
  });
440
687
  clients.forEach((client) => client.write(`data: ${payload}\n\n`));
package/dist/index.js CHANGED
@@ -70,6 +70,230 @@ var resolveDocsWorkspace = (cwd = process.cwd(), workspace) => {
70
70
  path.join(projectRoot, "index.html")
71
71
  ].join(" "));
72
72
  };
73
+ var BUNDLED_SPECIFIERS = /* @__PURE__ */ new Set(["vue/jsx-runtime", "vue/jsx-dev-runtime"]);
74
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
75
+ "coverage",
76
+ "dist",
77
+ "node_modules"
78
+ ]);
79
+ var cache = /* @__PURE__ */ new Map();
80
+ var pending = /* @__PURE__ */ new Map();
81
+ var buildCount = 0;
82
+ var readPackageName = (manifestPath) => {
83
+ try {
84
+ const manifest = JSON.parse(fs$1.readFileSync(manifestPath, "utf8"));
85
+ return typeof manifest.name === "string" && manifest.name ? manifest.name : "";
86
+ } catch {
87
+ return "";
88
+ }
89
+ };
90
+ var resolveSourceEntry = (directory) => {
91
+ for (const relative of ["src/index.ts", "index.ts"]) {
92
+ const candidate = path.resolve(directory, relative);
93
+ if (!fs$1.existsSync(candidate) || !fs$1.statSync(candidate).isFile()) continue;
94
+ const realEntry = fs$1.realpathSync(candidate);
95
+ if (!isInside(directory, realEntry)) continue;
96
+ return realEntry;
97
+ }
98
+ return "";
99
+ };
100
+ /**
101
+ * 收集项目 `packages/*` 里能编成浏览器模块的包。只接受留在项目内的真实路径。
102
+ * @param projectRoot 文档项目根目录。
103
+ * @returns 包名到源码入口的映射。同名包保留先扫描到的一项。
104
+ */
105
+ var resolveWorkspaceModuleEntries = (projectRoot) => {
106
+ const realRoot = fs$1.existsSync(projectRoot) ? fs$1.realpathSync(projectRoot) : path.resolve(projectRoot);
107
+ const packagesDir = path.resolve(realRoot, "packages");
108
+ if (!fs$1.existsSync(packagesDir) || !fs$1.statSync(packagesDir).isDirectory()) return [];
109
+ const realPackages = fs$1.realpathSync(packagesDir);
110
+ if (!isInside(realRoot, realPackages)) return [];
111
+ const seen = /* @__PURE__ */ new Set();
112
+ const entries = [];
113
+ for (const item of fs$1.readdirSync(packagesDir, { withFileTypes: true })) {
114
+ if (!item.isDirectory() && !item.isSymbolicLink()) continue;
115
+ const directory = path.resolve(packagesDir, item.name);
116
+ if (!fs$1.existsSync(directory) || !fs$1.statSync(directory).isDirectory()) continue;
117
+ const realDirectory = fs$1.realpathSync(directory);
118
+ if (!isInside(realPackages, realDirectory)) continue;
119
+ const name = readPackageName(path.join(realDirectory, "package.json"));
120
+ const entry = name ? resolveSourceEntry(realDirectory) : "";
121
+ if (!name || !entry || seen.has(name)) continue;
122
+ seen.add(name);
123
+ entries.push({
124
+ name,
125
+ entry,
126
+ directory: realDirectory
127
+ });
128
+ }
129
+ return entries;
130
+ };
131
+ var directorySignature = (directory) => {
132
+ const files = [];
133
+ const walk = (current) => {
134
+ const realCurrent = fs$1.realpathSync(current);
135
+ if (!isInside(directory, realCurrent)) return;
136
+ for (const item of fs$1.readdirSync(realCurrent, { withFileTypes: true })) {
137
+ if (item.name === ".git" || IGNORED_DIRECTORIES.has(item.name)) continue;
138
+ const filename = path.join(realCurrent, item.name);
139
+ if (item.isSymbolicLink()) {
140
+ if (!fs$1.existsSync(filename)) continue;
141
+ const realName = fs$1.realpathSync(filename);
142
+ if (!isInside(directory, realName)) continue;
143
+ const stat = fs$1.statSync(realName);
144
+ if (stat.isDirectory()) walk(realName);
145
+ else if (stat.isFile()) files.push(`${realName}:${stat.mtimeMs}`);
146
+ continue;
147
+ }
148
+ if (item.isDirectory()) {
149
+ walk(filename);
150
+ continue;
151
+ }
152
+ if (item.isFile()) files.push(`${filename}:${fs$1.statSync(filename).mtimeMs}`);
153
+ }
154
+ };
155
+ walk(directory);
156
+ files.sort();
157
+ return files.join("\n");
158
+ };
159
+ var isWorkspaceModuleExternal = (id) => {
160
+ if (!id || id.startsWith("\0") || id.startsWith(".") || path.isAbsolute(id)) return false;
161
+ if (BUNDLED_SPECIFIERS.has(id)) return false;
162
+ return true;
163
+ };
164
+ var readAssetSource = (source) => typeof source === "string" ? source : Buffer.from(source).toString("utf8");
165
+ var toBundleResult = (result) => {
166
+ const output = Array.isArray(result) ? result[0] : result;
167
+ if (!output || !("output" in output) || !Array.isArray(output.output)) throw new Error("Module build did not produce a bundle");
168
+ return output;
169
+ };
170
+ var injectStyles = (name, code, css) => {
171
+ if (!css) return code;
172
+ return [
173
+ "if (typeof document !== \"undefined\") {",
174
+ " const style = document.createElement(\"style\");",
175
+ `\tstyle.setAttribute("data-docs-module", ${JSON.stringify(name)});`,
176
+ `\tstyle.textContent = ${JSON.stringify(css)};`,
177
+ " document.head.appendChild(style);",
178
+ "}",
179
+ code
180
+ ].join("\n");
181
+ };
182
+ var buildWorkspaceModule = async (projectRoot, entry) => {
183
+ buildCount += 1;
184
+ const bundle = toBundleResult(await build({
185
+ configFile: false,
186
+ envFile: false,
187
+ root: projectRoot,
188
+ publicDir: false,
189
+ logLevel: "silent",
190
+ mode: "development",
191
+ appType: "custom",
192
+ define: { "process.env.NODE_ENV": JSON.stringify("development") },
193
+ plugins: [vue(), vueJsx()],
194
+ build: {
195
+ write: false,
196
+ minify: false,
197
+ cssMinify: false,
198
+ cssCodeSplit: false,
199
+ sourcemap: false,
200
+ emptyOutDir: false,
201
+ copyPublicDir: false,
202
+ modulePreload: false,
203
+ reportCompressedSize: false,
204
+ assetsInlineLimit: 1e8,
205
+ chunkSizeWarningLimit: 1e5,
206
+ rolldownOptions: {
207
+ input: entry.entry,
208
+ preserveEntrySignatures: "strict",
209
+ treeshake: false,
210
+ external: isWorkspaceModuleExternal,
211
+ output: {
212
+ format: "es",
213
+ codeSplitting: false,
214
+ entryFileNames: "module.js"
215
+ }
216
+ }
217
+ }
218
+ }));
219
+ const chunks = bundle.output.filter((item) => item.type === "chunk");
220
+ if (chunks.length !== 1) throw new Error(`Expected a single module chunk, got ${chunks.length}`);
221
+ const css = bundle.output.filter((item) => item.type === "asset" && item.fileName.endsWith(".css")).map((item) => readAssetSource(item.source)).join("\n");
222
+ return injectStyles(entry.name, chunks[0].code, css);
223
+ };
224
+ /**
225
+ * 把工作区包编成一份浏览器 ESM。`vue` 和其它工作区包保持裸导入。
226
+ * @param projectRoot 用于解析 node_modules 的项目根。
227
+ * @param entry 已通过边界检查的包入口。
228
+ * @returns 可直接交给浏览器执行的模块源码。
229
+ */
230
+ var compileWorkspaceModule = (projectRoot, entry) => {
231
+ const signature = directorySignature(entry.directory);
232
+ const cached = cache.get(entry.name);
233
+ if (cached?.signature === signature) return Promise.resolve(cached.code);
234
+ const inflight = pending.get(entry.name);
235
+ if (inflight) return inflight;
236
+ const task = buildWorkspaceModule(projectRoot, entry).then((code) => {
237
+ cache.set(entry.name, {
238
+ signature: directorySignature(entry.directory),
239
+ code
240
+ });
241
+ return code;
242
+ }).finally(() => {
243
+ if (pending.get(entry.name) === task) pending.delete(entry.name);
244
+ });
245
+ pending.set(entry.name, task);
246
+ return task;
247
+ };
248
+ var moduleFailure = (message) => `throw new Error(${JSON.stringify(message)});`;
249
+ /**
250
+ * 响应 `/__docs/module/<包名>`。未知包返回 404;编译失败返回会抛错的 ESM。
251
+ * @param req 开发服务请求。
252
+ * @param res 开发服务响应。
253
+ * @param projectRoot 文档项目根目录。
254
+ * @returns 是否已处理该请求。路径不匹配时为 false。
255
+ */
256
+ var respondWorkspaceModule = async (req, res, projectRoot) => {
257
+ const pathname = (req.url || "/").split(/[?#]/u, 1)[0] || "/";
258
+ if (!pathname.startsWith("/__docs/module/")) return false;
259
+ if (req.method !== "GET" && req.method !== "HEAD") {
260
+ res.statusCode = 405;
261
+ res.setHeader("Allow", "GET, HEAD");
262
+ res.end("Method Not Allowed");
263
+ return true;
264
+ }
265
+ let name = "";
266
+ try {
267
+ name = decodeURIComponent(pathname.slice(15));
268
+ } catch {
269
+ res.statusCode = 400;
270
+ res.end("Bad Request");
271
+ return true;
272
+ }
273
+ if (!name || name.split("/").includes("..") || name.includes("\\") || name.includes("\0")) {
274
+ res.statusCode = 404;
275
+ res.end("Not Found");
276
+ return true;
277
+ }
278
+ const entry = resolveWorkspaceModuleEntries(projectRoot).find((item) => item.name === name);
279
+ if (!entry) {
280
+ res.statusCode = 404;
281
+ res.end("Not Found");
282
+ return true;
283
+ }
284
+ let code;
285
+ try {
286
+ code = await compileWorkspaceModule(projectRoot, entry);
287
+ } catch (reason) {
288
+ code = moduleFailure(reason instanceof Error ? reason.message : "Module compile failed");
289
+ }
290
+ res.statusCode = 200;
291
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
292
+ res.setHeader("Cache-Control", "no-store");
293
+ if (req.method === "HEAD") res.end();
294
+ else res.end(code);
295
+ return true;
296
+ };
73
297
  //#endregion
74
298
  //#region packages/dever/src/plugins/index.ts
75
299
  var innerPathRegex = /^\/(@|__)/;
@@ -198,6 +422,12 @@ var decodeWorkspacePath = (requestUrl, urlPrefix) => {
198
422
  return decoded;
199
423
  };
200
424
  var isPackageReadme = (relative) => /^[^/]+\/README\.md$/u.test(relative);
425
+ /**
426
+ * 传统 workspace 把语言放在第一段目录,例如 `zh-CN/guide.md`。
427
+ * `docs/`、`src/` 这类共享目录不能当成语言,否则 SSE identity 对不上 Client。
428
+ * @param value
429
+ */
430
+ var isLocaleDirectory = (value) => /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/u.test(value);
201
431
  var getContentType = (extension) => {
202
432
  switch (extension) {
203
433
  case ".md": return "text/markdown; charset=utf-8";
@@ -263,6 +493,14 @@ var configureWorkspaceServer = (options, resolvedWorkspace) => (server) => {
263
493
  let realResourceRoot = realWorkspace;
264
494
  try {
265
495
  const rawPathname = getRawPathname(req.url || "/");
496
+ if (!options.preview && rawPathname.startsWith("/__docs/module/")) {
497
+ respondWorkspaceModule(req, res, resolved.projectRoot).catch((reason) => {
498
+ if (res.writableEnded) return;
499
+ res.statusCode = 500;
500
+ res.end(reason instanceof Error ? reason.message : "Module compile failed");
501
+ });
502
+ return;
503
+ }
266
504
  if (rawPathname.startsWith("/__docs/") && rawPathname !== "/__docs/events") {
267
505
  res.statusCode = 404;
268
506
  res.end("Not Found");
@@ -357,6 +595,8 @@ var configureEvents = (server, workspace, entry, packages, rootReadme) => {
357
595
  res.setHeader("Content-Type", "text/event-stream");
358
596
  res.setHeader("Cache-Control", "no-cache");
359
597
  res.setHeader("Connection", "keep-alive");
598
+ res.setHeader("X-Accel-Buffering", "no");
599
+ res.flushHeaders?.();
360
600
  res.write(": connected\n\n");
361
601
  clients.add(res);
362
602
  req.on("close", () => clients.delete(res));
@@ -401,11 +641,18 @@ var configureEvents = (server, workspace, entry, packages, rootReadme) => {
401
641
  }
402
642
  const [lang, ...segments] = relative.split("/");
403
643
  if (!lang || !segments.length) return;
404
- const payload = JSON.stringify({
644
+ const resourceType = getResourceType(relative);
645
+ const payload = JSON.stringify(isLocaleDirectory(lang) ? {
405
646
  type,
406
647
  lang,
407
648
  source: `./${segments.join("/")}`,
408
- resourceType: getResourceType(relative),
649
+ resourceType,
650
+ timestamp: Date.now()
651
+ } : {
652
+ type,
653
+ lang: "",
654
+ source: relative,
655
+ resourceType,
409
656
  timestamp: Date.now()
410
657
  });
411
658
  clients.forEach((client) => client.write(`data: ${payload}\n\n`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deot/docs-dever",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",