@akairos/pipeline 0.18.5 → 0.19.1

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.
Files changed (47) hide show
  1. package/ARTICLE-THEMES.md +2 -2
  2. package/README.md +55 -15
  3. package/THIRD-PARTY-NOTICES.md +1 -1
  4. package/package.json +4 -4
  5. package/src/adapter-loader.ts +24 -4
  6. package/src/adapter-types.ts +14 -0
  7. package/src/commands/typeset-assets-sync.test.ts +3 -0
  8. package/src/plugin-runner.ts +10 -4
  9. package/src/plugin-runtime.test.ts +19 -0
  10. package/src/plugin-runtime.ts +11 -2
  11. package/src/runtime-package.ts +9 -3
  12. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +47 -47
  13. package/src/wechat-preview/assets/browser-dist/assets/{index-BHJY0B4E.js → index-52O71prB.js} +1 -1
  14. package/src/wechat-preview/assets/browser-dist/assets/{index-CdxKyTX6.js → index-BOH11YN9.js} +1 -1
  15. package/src/wechat-preview/assets/browser-dist/assets/{index-DDQGWU_D.js → index-BOZWI3FE.js} +1 -1
  16. package/src/wechat-preview/assets/browser-dist/assets/{index-BlVpb0dF.js → index-BRUZEgPh.js} +1 -1
  17. package/src/wechat-preview/assets/browser-dist/assets/{index-BPmBcGI-.js → index-BTPh91V-.js} +1 -1
  18. package/src/wechat-preview/assets/browser-dist/assets/{index-BYV6TA8f.js → index-BXjcry0g.js} +1 -1
  19. package/src/wechat-preview/assets/browser-dist/assets/{index-ifI5svkD.js → index-BoIlqlZt.js} +1 -1
  20. package/src/wechat-preview/assets/browser-dist/assets/{index-D_EI5vFS.js → index-Btrc4kp0.js} +1 -1
  21. package/src/wechat-preview/assets/browser-dist/assets/{index-BFLGVgsp.js → index-ButcQVWD.js} +1 -1
  22. package/src/wechat-preview/assets/browser-dist/assets/{index-vRiJxvj2.js → index-C-LFn5io.js} +1 -1
  23. package/src/wechat-preview/assets/browser-dist/assets/{index-DpuVrwze.js → index-C1Bxnw4o.js} +1 -1
  24. package/src/wechat-preview/assets/browser-dist/assets/{index-BzHql5Rp.js → index-C2N6r4Fw.js} +1 -1
  25. package/src/wechat-preview/assets/browser-dist/assets/{index-BqKARoyW.js → index-Cbm1GiZg.js} +1 -1
  26. package/src/wechat-preview/assets/browser-dist/assets/{index-DENMoE5l.js → index-CwgSvQOC.js} +1 -1
  27. package/src/wechat-preview/assets/browser-dist/assets/{index-BTVIa90Y.js → index-D7f6KXoI.js} +1 -1
  28. package/src/wechat-preview/assets/browser-dist/assets/{index-IBlhKHQr.js → index-DkhSv5cA.js} +1 -1
  29. package/src/wechat-preview/assets/browser-dist/assets/{index-DA7Z4paG.js → index-DmoEW40N.js} +1 -1
  30. package/src/wechat-preview/assets/browser-dist/assets/{index-BDdU9pNh.js → index-Dyi5bUmz.js} +1 -1
  31. package/src/wechat-preview/assets/browser-dist/assets/{index-CPFEJ9fX.js → index-TOFwZskx.js} +1 -1
  32. package/src/wechat-preview/assets/browser-dist/assets/{index-BOnKEJxh.js → index-lWl3DOj9.js} +1 -1
  33. package/src/wechat-preview/assets/browser-dist/assets/{index-CyXDvcwY.js → index-sgpqKtmR.js} +1 -1
  34. package/src/wechat-preview/assets/browser-dist/assets/{style-Cif_gYUW.css → style-CVnSjpTH.css} +1 -1
  35. package/src/wechat-preview/assets/browser-dist/assets/{wechat-renderer-CcHuq7M-.js → wechat-renderer-GrfbOjo2.js} +13 -4
  36. package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -1
  37. package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +2 -2
  38. package/src/wechat-preview/assets/browser-dist/studio.js +3 -3
  39. package/src/wechat-preview/browser/editor-export.css +2 -2
  40. package/src/wechat-preview/browser/editor-export.ts +1 -1
  41. package/src/wechat-preview/browser/studio/myno-editor.ts +2 -2
  42. package/src/wechat-preview/browser/studio/studio.ts +1 -1
  43. package/src/wechat-preview/index.ts +8 -3
  44. package/src/wechat-preview/server/http.ts +56 -1
  45. package/src/wechat-preview/server/studio.test.ts +35 -1
  46. package/src/wechat-preview/wechat-export.integration.test.ts +3 -0
  47. package/src/wechat-preview.test.ts +1 -1
@@ -1,9 +1,9 @@
1
1
  /*
2
2
  * Thin re-export for tooling that still points at browser/editor-export.css.
3
- * Canonical source: package @akairos/milkdown-article-style (article.css).
3
+ * Canonical source: package @akairos/myno (article.css).
4
4
  *
5
5
  * The Vite export entry imports the package directly (?raw does not resolve
6
6
  * @import). Do not add rules here.
7
7
  */
8
8
 
9
- @import "@akairos/milkdown-article-style/article.css";
9
+ @import "@akairos/myno/article.css";
@@ -2,7 +2,7 @@ import { defaultValueCtx, Editor, rootCtx } from "@milkdown/kit/core";
2
2
  import { commonmark } from "@milkdown/kit/preset/commonmark";
3
3
  import { gfm } from "@milkdown/kit/preset/gfm";
4
4
  import { getHTML } from "@milkdown/kit/utils";
5
- import articleCss from "@akairos/milkdown-article-style/article.css?raw";
5
+ import articleCss from "@akairos/myno/article.css?raw";
6
6
  import { getCombinedMilkdownPlugins } from "@akairos/milkdown-article-plugins";
7
7
  import type { ArticleStructure } from "../wechat-renderer";
8
8
  import { renderWechatHtml } from "../wechat-renderer";
@@ -10,8 +10,8 @@ import {
10
10
 
11
11
  import "@milkdown/crepe/theme/common/style.css";
12
12
  import "@milkdown/crepe/theme/frame.css";
13
- import "@akairos/milkdown-article-style/tokens-default.css";
14
- import "@akairos/milkdown-article-style/article.css";
13
+ import "@akairos/myno/tokens-default.css";
14
+ import "@akairos/myno/article.css";
15
15
 
16
16
  export interface MynoEditorOptions {
17
17
  root: HTMLElement;
@@ -1,4 +1,4 @@
1
- import articleCss from "@akairos/milkdown-article-style/article.css?raw";
1
+ import articleCss from "@akairos/myno/article.css?raw";
2
2
  import { Editor, defaultValueCtx, rootCtx } from "@milkdown/kit/core";
3
3
  import { commonmark } from "@milkdown/kit/preset/commonmark";
4
4
  import { gfm } from "@milkdown/kit/preset/gfm";
@@ -100,6 +100,8 @@ export interface ExportMarkdownToWechatHtmlInput {
100
100
  timeoutMs?: number;
101
101
  /** Write intermediate artifacts for debugging. */
102
102
  debugDir?: string;
103
+ /** 外部插件提供的已构建入口;浏览器和文件输出继续由宿主管理。 */
104
+ browserScriptPath?: string;
103
105
  }
104
106
 
105
107
  export interface ExportMarkdownToWechatHtmlResult {
@@ -117,9 +119,9 @@ export interface ExportMarkdownToWechatHtmlResult {
117
119
 
118
120
  const requireFromHere = createRequire(import.meta.url);
119
121
 
120
- /** Absolute path to shared article.css (@akairos/milkdown-article-style). */
122
+ /** Absolute path to shared article.css (@akairos/myno). */
121
123
  export function resolveMilkdownArticleStylePath(): string {
122
- return requireFromHere.resolve("@akairos/milkdown-article-style/article.css");
124
+ return requireFromHere.resolve("@akairos/myno/article.css");
123
125
  }
124
126
 
125
127
  /** Source files that feed the browser Vite bundle. */
@@ -405,7 +407,9 @@ export async function exportMarkdownToWechatHtml(
405
407
 
406
408
  let bundle: BundleAssets;
407
409
  try {
408
- bundle = ensureBundle();
410
+ bundle = input.browserScriptPath
411
+ ? { scriptPath: input.browserScriptPath, rebuilt: false, stale: false }
412
+ : ensureBundle();
409
413
  bundleRebuilt = bundle.rebuilt;
410
414
  bundleStale = bundle.stale;
411
415
  } catch (error) {
@@ -432,6 +436,7 @@ export async function exportMarkdownToWechatHtml(
432
436
  assetBaseUrl: pathToFileURL(`${dirname(resolve(input.markdownPath))}/`).href,
433
437
  editorVars: theme.editorVars,
434
438
  exportTheme: theme.exportTheme,
439
+ syntaxPresets,
435
440
  customCss,
436
441
  structure,
437
442
  }),
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { dirname, join } from "path";
2
+ import { realpath, stat } from "fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
3
4
  import { z } from "zod";
4
5
  import {
5
6
  previewBaseUrl,
@@ -32,6 +33,8 @@ import { readArticleTheme } from "../../article-theme";
32
33
  import { PipelineConfigSchema, WechatExportThemeOverridesSchema, WechatStructureOverridesSchema } from "../../schema/config";
33
34
  import { loadConfig, saveConfig, getPipelineConfigPath, resolveConfigRelativePath } from "../../config";
34
35
  import { DIST_DIR, WECHAT_PREVIEW_DIR } from "../../runtime-paths";
36
+ import { resolveMarkdownRenderer } from "../../adapter-loader";
37
+ import type { MarkdownStudioAssets } from "../../adapter-types";
35
38
  import type { PreviewRegisterInput } from "./types";
36
39
 
37
40
  /** 保存入口先校验完整请求,避免写入 CLI 无法加载的配置。 */
@@ -105,6 +108,32 @@ export async function startPreviewServer(
105
108
  clearServerLock();
106
109
  }
107
110
 
111
+ // 工作台与导出使用同一个插件;没有工作台能力时给出明确提示。
112
+ const pluginConfig = loadConfig();
113
+ const externalMarkdown = Boolean(pluginConfig.plugins.markdownRenderer);
114
+ let studioAssets: Promise<MarkdownStudioAssets | undefined> | undefined;
115
+ /** 按需加载工作台,故障插件不影响预览登记和历史页面。 */
116
+ function getStudio() {
117
+ return studioAssets ??= resolveMarkdownRenderer(pluginConfig).then(async renderer => {
118
+ const studio = await renderer.studio?.();
119
+ return studio ? { ...studio, root: await realpath(studio.root) } : undefined;
120
+ });
121
+ }
122
+
123
+ /** 插件静态资源独立挂载,真实路径也必须留在插件资源目录内。 */
124
+ async function studioFile(name: string): Promise<string | null> {
125
+ try {
126
+ if (!externalMarkdown) return null;
127
+ const studioRoot = (await getStudio())?.root;
128
+ if (!studioRoot) return null;
129
+ const target = await realpath(resolve(studioRoot, name));
130
+ const rel = relative(studioRoot, target);
131
+ return rel && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`) && (await stat(target)).isFile() ? target : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
108
137
  let server: ReturnType<typeof Bun.serve>;
109
138
  server = Bun.serve({
110
139
  hostname: host,
@@ -317,6 +346,21 @@ export async function startPreviewServer(
317
346
  }
318
347
 
319
348
  if (pathname === "/studio" || pathname === "/studio/" || pathname === "/studio/index.html") {
349
+ if (externalMarkdown) {
350
+ let studio: MarkdownStudioAssets | undefined;
351
+ try { studio = await getStudio(); }
352
+ catch (error) { return json({ error: error instanceof Error ? error.message : String(error) }, 500); }
353
+ if (!studio) return html("<h1>当前 Markdown 渲染插件未提供 Studio 工作台。</h1>", 404);
354
+ const entry = await studioFile(studio.entry);
355
+ if (!entry) return html("<h1>插件 Studio 入口不存在或超出资源目录。</h1>", 500);
356
+ // 相对资源按插件入口解析,统一放入命名空间且保留页面导航链接。
357
+ return html(readFileSync(entry, "utf8").replace(/<(?:script|link)\b[^>]*>/g,
358
+ tag => tag.replace(/\b(src|href)=(["'])([^"']+)\2/g, (attribute, name, quote, value: string) => {
359
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(value)) return attribute;
360
+ const asset = new URL(value, `http://studio/${studio.entry}`);
361
+ return `${name}=${quote}/studio-assets${asset.pathname}${asset.search}${quote}`;
362
+ })));
363
+ }
320
364
  const studioHtmlPath = join(DIST_DIR, "src/wechat-preview/browser/studio/studio.html");
321
365
  const fallbackPath = join(DIST_DIR, "studio.html");
322
366
  const targetHtmlPath = existsSync(studioHtmlPath) ? studioHtmlPath : fallbackPath;
@@ -326,6 +370,17 @@ export async function startPreviewServer(
326
370
  return html("<h1>Studio not built. Run bun run build:wechat-preview first.</h1>", 500);
327
371
  }
328
372
 
373
+ if (pathname.startsWith("/studio-assets/")) {
374
+ let name: string;
375
+ try { name = decodeURIComponent(pathname.slice("/studio-assets/".length)); }
376
+ catch { return json({ error: "invalid asset path" }, 400); }
377
+ const target = await studioFile(name);
378
+ if (!target) return json({ error: "Studio asset not found" }, 404);
379
+ return new Response(new Uint8Array(readFileSync(target)), {
380
+ headers: { "content-type": guessContentType(target), "cache-control": "no-cache" },
381
+ });
382
+ }
383
+
329
384
  if (pathname === "/studio.js" || pathname === "/editor-export.js") {
330
385
  const filePath = join(DIST_DIR, pathname.slice(1));
331
386
  if (existsSync(filePath)) {
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
 
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
5
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "fs";
6
6
  import { startPreviewServer, type StartPreviewServerResult } from "./http";
7
7
  import { loadConfig, saveConfig } from "../../config";
8
8
 
@@ -192,4 +192,38 @@ describe("wechat-preview studio server APIs", () => {
192
192
  expect(html).toContain("studio.js");
193
193
  expect(html).toContain('id="btn-export-config"');
194
194
  });
195
+
196
+ test("external Studio serves plugin resources without exposing files outside its root", async () => {
197
+ const root = join(testDir, "plugin-assets");
198
+ mkdirSync(root);
199
+ writeFileSync(join(root, "studio.html"), '<script src="/studio.js"></script><link href="/style.css"><a href="/">History</a>');
200
+ writeFileSync(join(root, "studio.js"), "/* plugin studio */");
201
+ writeFileSync(join(testDir, "private.txt"), "private");
202
+ symlinkSync(join(testDir, "private.txt"), join(root, "outside.txt"));
203
+ const pluginPath = join(testDir, "studio-plugin.mjs");
204
+ writeFileSync(pluginPath, `export default {name:'studio-test',render(){},studio(){return {root:${JSON.stringify(root)},entry:'studio.html'}}}`);
205
+ const config = loadConfig();
206
+ const original = config.plugins.markdownRenderer;
207
+ config.plugins.markdownRenderer = pluginPath;
208
+ saveConfig(config);
209
+ let external: StartPreviewServerResult | undefined;
210
+ const originalServerDir = process.env.ZZHUB_WECHAT_PREVIEW_DIR;
211
+ process.env.ZZHUB_WECHAT_PREVIEW_DIR = join(testDir, "external-server");
212
+ try {
213
+ external = await startPreviewServer({ host: "127.0.0.1", port: 0, reuseExisting: false });
214
+ const page = await (await fetch(`${external.url}/studio`)).text();
215
+ expect(page).toContain('src="/studio-assets/studio.js"');
216
+ expect(page).toContain('href="/studio-assets/style.css"');
217
+ expect(page).toContain('<a href="/">History</a>');
218
+ expect(await (await fetch(`${external.url}/studio-assets/studio.js`)).text()).toBe("/* plugin studio */");
219
+ expect((await fetch(`${external.url}/studio-assets/outside.txt`)).status).toBe(404);
220
+ expect((await fetch(`${external.url}/studio-assets/..%2fprivate.txt`)).status).toBe(404);
221
+ expect((await fetch(`${external.url}/api/studio/config`)).status).toBe(200);
222
+ } finally {
223
+ external?.stop?.();
224
+ process.env.ZZHUB_WECHAT_PREVIEW_DIR = originalServerDir;
225
+ config.plugins.markdownRenderer = original;
226
+ saveConfig(config);
227
+ }
228
+ });
195
229
  });
@@ -175,6 +175,9 @@ window.__zzhubRenderReady = (async () => {
175
175
  expect(html).toContain("![代码示例](./missing-example.png)");
176
176
  expect(html).not.toMatch(/<img[^>]+src="\.\.?\//);
177
177
  expect(html).toContain("<table");
178
+ expect(html).toMatch(/<table[^>]*overflow-x: auto/);
179
+ expect(html).toMatch(/<th\b[^>]*white-space: nowrap/);
180
+ expect(html).toMatch(/<td\b[^>]*word-break: break-word/);
178
181
  expect(html).toContain("<img ");
179
182
  expect(html).toContain("图片说明:克制、清晰、适合移动端阅读");
180
183
  expect(html).toContain("typescript</p><pre");
@@ -30,7 +30,7 @@ describe("wechat preview theme mapping", () => {
30
30
  // - it styles both bare <code> (live editor) and the export-only
31
31
  // [data-wechat-node] hooks emitted by wechat-renderer.
32
32
  // Exact colors/padding values belong to the external
33
- // @akairos/milkdown-article-style package and must NOT be pinned here, or a
33
+ // @akairos/myno package and must NOT be pinned here, or a
34
34
  // package theme bump breaks this repo for no functional reason.
35
35
  const { resolveMilkdownArticleStylePath } = await import("./wechat-preview/index");
36
36
  const css = await Bun.file(resolveMilkdownArticleStylePath()).text();