@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.
- package/ARTICLE-THEMES.md +2 -2
- package/README.md +55 -15
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/package.json +4 -4
- package/src/adapter-loader.ts +24 -4
- package/src/adapter-types.ts +14 -0
- package/src/commands/typeset-assets-sync.test.ts +3 -0
- package/src/plugin-runner.ts +10 -4
- package/src/plugin-runtime.test.ts +19 -0
- package/src/plugin-runtime.ts +11 -2
- package/src/runtime-package.ts +9 -3
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +47 -47
- package/src/wechat-preview/assets/browser-dist/assets/{index-BHJY0B4E.js → index-52O71prB.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-CdxKyTX6.js → index-BOH11YN9.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-DDQGWU_D.js → index-BOZWI3FE.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BlVpb0dF.js → index-BRUZEgPh.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BPmBcGI-.js → index-BTPh91V-.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BYV6TA8f.js → index-BXjcry0g.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-ifI5svkD.js → index-BoIlqlZt.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-D_EI5vFS.js → index-Btrc4kp0.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BFLGVgsp.js → index-ButcQVWD.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-vRiJxvj2.js → index-C-LFn5io.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-DpuVrwze.js → index-C1Bxnw4o.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BzHql5Rp.js → index-C2N6r4Fw.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BqKARoyW.js → index-Cbm1GiZg.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-DENMoE5l.js → index-CwgSvQOC.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BTVIa90Y.js → index-D7f6KXoI.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-IBlhKHQr.js → index-DkhSv5cA.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-DA7Z4paG.js → index-DmoEW40N.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BDdU9pNh.js → index-Dyi5bUmz.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-CPFEJ9fX.js → index-TOFwZskx.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-BOnKEJxh.js → index-lWl3DOj9.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{index-CyXDvcwY.js → index-sgpqKtmR.js} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{style-Cif_gYUW.css → style-CVnSjpTH.css} +1 -1
- package/src/wechat-preview/assets/browser-dist/assets/{wechat-renderer-CcHuq7M-.js → wechat-renderer-GrfbOjo2.js} +13 -4
- package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -1
- package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +2 -2
- package/src/wechat-preview/assets/browser-dist/studio.js +3 -3
- package/src/wechat-preview/browser/editor-export.css +2 -2
- package/src/wechat-preview/browser/editor-export.ts +1 -1
- package/src/wechat-preview/browser/studio/myno-editor.ts +2 -2
- package/src/wechat-preview/browser/studio/studio.ts +1 -1
- package/src/wechat-preview/index.ts +8 -3
- package/src/wechat-preview/server/http.ts +56 -1
- package/src/wechat-preview/server/studio.test.ts +35 -1
- package/src/wechat-preview/wechat-export.integration.test.ts +3 -0
- 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/
|
|
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/
|
|
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/
|
|
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/
|
|
14
|
-
import "@akairos/
|
|
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/
|
|
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/
|
|
122
|
+
/** Absolute path to shared article.css (@akairos/myno). */
|
|
121
123
|
export function resolveMilkdownArticleStylePath(): string {
|
|
122
|
-
return requireFromHere.resolve("@akairos/
|
|
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 =
|
|
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 {
|
|
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("");
|
|
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/
|
|
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();
|