mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 22:42:31 +00:00
feat: split diffs language pack
This commit is contained in:
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
@@ -491,6 +491,7 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/diffs/**"
|
||||
- "extensions/diffs-language-pack/**"
|
||||
"extensions: elevenlabs":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -249,6 +249,7 @@ extensions/qa-lab/web/dist/
|
||||
# Generated bundled plugin runtime dependency manifests
|
||||
extensions/**/.openclaw-runtime-deps.json
|
||||
extensions/**/.openclaw-runtime-deps-stamp.json
|
||||
extensions/diffs-language-pack/assets/viewer-runtime.js
|
||||
|
||||
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
|
||||
/.opengrep-out/
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"dist-runtime/",
|
||||
"docs/_layouts/",
|
||||
"extensions/diffs/assets/viewer-runtime.js",
|
||||
"extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
"node_modules/",
|
||||
"patches/",
|
||||
"pnpm-lock.yaml",
|
||||
|
||||
@@ -151,6 +151,7 @@ commands.
|
||||
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
|
||||
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
|
||||
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
|
||||
| [diffs-language-pack](/plugins/reference/diffs-language-pack) | Adds syntax highlighting for languages outside the default diffs viewer set. | `@openclaw/diffs-language-pack`<br />npm; ClawHub | plugin |
|
||||
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: transcriptSourceProviders |
|
||||
| [feishu](/plugins/reference/feishu) | Adds the Feishu channel surface for sending and receiving OpenClaw messages. | `@openclaw/feishu`<br />npm; ClawHub | channels: feishu; contracts: tools; skills |
|
||||
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />npm; ClawHub | contracts: tools |
|
||||
|
||||
@@ -44,6 +44,7 @@ pnpm plugins:inventory:gen
|
||||
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
|
||||
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
|
||||
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
|
||||
| [diffs-language-pack](/plugins/reference/diffs-language-pack) | Adds syntax highlighting for languages outside the default diffs viewer set. | `@openclaw/diffs-language-pack`<br />npm; ClawHub | plugin |
|
||||
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord; contracts: transcriptSourceProviders |
|
||||
| [document-extract](/plugins/reference/document-extract) | Extract text and fallback page images from local document attachments. | `@openclaw/document-extract-plugin`<br />included in OpenClaw | contracts: documentExtractors |
|
||||
| [duckduckgo](/plugins/reference/duckduckgo) | Adds web search provider support. | `@openclaw/duckduckgo-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
|
||||
|
||||
19
docs/plugins/reference/diffs-language-pack.md
Normal file
19
docs/plugins/reference/diffs-language-pack.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
summary: "Adds syntax highlighting for languages outside the default diffs viewer set."
|
||||
read_when:
|
||||
- You are installing, configuring, or auditing the diffs-language-pack plugin
|
||||
title: "Diffs Language Pack plugin"
|
||||
---
|
||||
|
||||
# Diffs Language Pack plugin
|
||||
|
||||
Adds syntax highlighting for languages outside the default diffs viewer set.
|
||||
|
||||
## Distribution
|
||||
|
||||
- Package: `@openclaw/diffs-language-pack`
|
||||
- Install route: npm; ClawHub
|
||||
|
||||
## Surface
|
||||
|
||||
plugin
|
||||
@@ -136,8 +136,10 @@ All fields are optional unless noted.
|
||||
Display filename for before and after mode.
|
||||
</ParamField>
|
||||
<ParamField path="lang" type="string">
|
||||
Language override hint for before and after mode. Unknown values fall back to plain text.
|
||||
Language override hint for before and after mode. Unknown values and languages outside the default viewer set fall back to plain text unless the
|
||||
Diff Viewer Language Pack plugin is installed.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="title" type="string">
|
||||
Viewer title override.
|
||||
</ParamField>
|
||||
@@ -200,6 +202,22 @@ All fields are optional unless noted.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Syntax highlighting
|
||||
|
||||
OpenClaw includes syntax highlighting for common source, config, and documentation languages:
|
||||
|
||||
`javascript`, `typescript`, `tsx`, `jsx`, `json`, `markdown`, `yaml`, `css`, `html`, `sh`, `python`, `go`, `rust`, `java`, `c`, `cpp`, `csharp`, `php`, `sql`, and `docker`.
|
||||
|
||||
Common aliases such as `js`, `ts`, `bash`, `md`, `yml`, `c++`, and `dockerfile` are normalized to those default languages.
|
||||
|
||||
Install the Diff Viewer Language Pack plugin to highlight other languages:
|
||||
|
||||
```bash
|
||||
openclaw plugins install diffs-language-pack
|
||||
```
|
||||
|
||||
With the language pack available, OpenClaw automatically uses it for languages outside the default list. Without it, those files stay readable as plain text.
|
||||
|
||||
## Output details contract
|
||||
|
||||
The tool returns structured metadata under `details`.
|
||||
@@ -385,6 +403,7 @@ Viewer assets:
|
||||
|
||||
- `/plugins/diffs/assets/viewer.js`
|
||||
- `/plugins/diffs/assets/viewer-runtime.js`
|
||||
- `/plugins/diffs-language-pack/assets/viewer.js` when the diff uses a language from the Diff Viewer Language Pack
|
||||
|
||||
The viewer document resolves those assets relative to the viewer URL, so an optional `baseUrl` path prefix is preserved for both asset requests too.
|
||||
|
||||
|
||||
6
extensions/diffs-language-pack/api.ts
Normal file
6
extensions/diffs-language-pack/api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
definePluginEntry,
|
||||
type OpenClawPluginApi,
|
||||
type OpenClawPluginHttpRouteHandler,
|
||||
type PluginLogger,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
9
extensions/diffs-language-pack/index.ts
Normal file
9
extensions/diffs-language-pack/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { definePluginEntry } from "./api.js";
|
||||
import { registerDiffsLanguagePackPlugin } from "./src/plugin.js";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "diffs-language-pack",
|
||||
name: "Diff Viewer Language Pack",
|
||||
description: "Adds syntax highlighting for languages outside the default diffs viewer set.",
|
||||
register: registerDiffsLanguagePackPlugin,
|
||||
});
|
||||
12
extensions/diffs-language-pack/npm-shrinkwrap.json
generated
Normal file
12
extensions/diffs-language-pack/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.5.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.5.26"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
extensions/diffs-language-pack/openclaw.plugin.json
Normal file
8
extensions/diffs-language-pack/openclaw.plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "diffs-language-pack",
|
||||
"activation": {
|
||||
"onStartup": true
|
||||
},
|
||||
"name": "Diff Viewer Language Pack",
|
||||
"description": "Adds syntax highlighting for languages outside the default diffs viewer set."
|
||||
}
|
||||
47
extensions/diffs-language-pack/package.json
Normal file
47
extensions/diffs-language-pack/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"version": "2026.5.26",
|
||||
"description": "OpenClaw diffs viewer syntax highlighting language pack",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openclaw/openclaw"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:viewer": "bun scripts/build-viewer.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"esbuild": "0.28.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/diffs-language-pack",
|
||||
"localPath": "extensions/diffs-language-pack",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.30"
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.26"
|
||||
},
|
||||
"assetScripts": {
|
||||
"build": "pnpm build:viewer"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.26",
|
||||
"staticAssets": [
|
||||
{
|
||||
"source": "./assets/viewer-runtime.js",
|
||||
"output": "assets/viewer-runtime.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
}
|
||||
}
|
||||
}
|
||||
38
extensions/diffs-language-pack/scripts/build-viewer.mjs
Normal file
38
extensions/diffs-language-pack/scripts/build-viewer.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { transform } from "esbuild";
|
||||
|
||||
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outputPath = path.join(pluginRoot, "assets", "viewer-runtime.js");
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(pluginRoot, "..", "diffs", "src", "viewer-client.ts")],
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
minify: true,
|
||||
outdir: path.dirname(outputPath),
|
||||
naming: path.basename(outputPath),
|
||||
write: true,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
for (const log of result.logs) {
|
||||
console.error(log.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runtime = await fs.readFile(outputPath, "utf8");
|
||||
const minified = await transform(runtime, {
|
||||
format: "esm",
|
||||
legalComments: "none",
|
||||
loader: "js",
|
||||
minify: true,
|
||||
target: "es2020",
|
||||
});
|
||||
await fs.writeFile(outputPath, minified.code.replace(/[ \t]+$/gm, ""));
|
||||
64
extensions/diffs-language-pack/src/plugin.ts
Normal file
64
extensions/diffs-language-pack/src/plugin.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";
|
||||
|
||||
export function registerDiffsLanguagePackPlugin(api: OpenClawPluginApi): void {
|
||||
api.registerHttpRoute({
|
||||
path: "/plugins/diffs-language-pack",
|
||||
auth: "plugin",
|
||||
match: "prefix",
|
||||
handler: createDiffsLanguagePackHttpHandler(),
|
||||
});
|
||||
}
|
||||
|
||||
function createDiffsLanguagePackHttpHandler() {
|
||||
return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
|
||||
const parsed = parseRequestUrl(req.url);
|
||||
if (!parsed?.pathname.startsWith(VIEWER_ASSET_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
respondText(res, 405, "Method not allowed");
|
||||
return true;
|
||||
}
|
||||
|
||||
const asset = await getServedViewerAsset(parsed.pathname);
|
||||
if (!asset) {
|
||||
respondText(res, 404, "Asset not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
setSharedHeaders(res, asset.contentType);
|
||||
if (req.method === "HEAD") {
|
||||
res.end();
|
||||
} else {
|
||||
res.end(asset.body);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function parseRequestUrl(rawUrl?: string): URL | null {
|
||||
if (!rawUrl) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new URL(rawUrl, "http://127.0.0.1");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function respondText(res: ServerResponse, statusCode: number, body: string): void {
|
||||
res.statusCode = statusCode;
|
||||
setSharedHeaders(res, "text/plain; charset=utf-8");
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function setSharedHeaders(res: ServerResponse, contentType: string): void {
|
||||
res.setHeader("cache-control", "no-store, max-age=0");
|
||||
res.setHeader("content-type", contentType);
|
||||
res.setHeader("x-content-type-options", "nosniff");
|
||||
res.setHeader("referrer-policy", "no-referrer");
|
||||
}
|
||||
90
extensions/diffs-language-pack/src/viewer-assets.ts
Normal file
90
extensions/diffs-language-pack/src/viewer-assets.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const VIEWER_ASSET_PREFIX = "/plugins/diffs-language-pack/assets/";
|
||||
export const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
|
||||
export const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
||||
const VIEWER_RUNTIME_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
|
||||
const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = [
|
||||
"./assets/viewer-runtime.js",
|
||||
"../assets/viewer-runtime.js",
|
||||
] as const;
|
||||
|
||||
type ServedViewerAsset = {
|
||||
body: string | Buffer;
|
||||
contentType: string;
|
||||
};
|
||||
|
||||
type RuntimeAssetCache = {
|
||||
mtimeMs: number;
|
||||
runtimeBody: Buffer;
|
||||
loaderBody: string;
|
||||
};
|
||||
|
||||
let runtimeAssetCache: RuntimeAssetCache | null = null;
|
||||
|
||||
function isMissingFileError(error: unknown): error is NodeJS.ErrnoException {
|
||||
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
||||
}
|
||||
|
||||
export async function resolveViewerRuntimeFileUrl(): Promise<URL> {
|
||||
let missingFileError: NodeJS.ErrnoException | null = null;
|
||||
|
||||
for (const relativePath of VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS) {
|
||||
const candidateUrl = new URL(relativePath, import.meta.url);
|
||||
try {
|
||||
await fs.stat(fileURLToPath(candidateUrl));
|
||||
return candidateUrl;
|
||||
} catch (error) {
|
||||
if (isMissingFileError(error)) {
|
||||
missingFileError = error;
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFileError) {
|
||||
throw missingFileError;
|
||||
}
|
||||
|
||||
throw new Error("viewer runtime asset candidates were not checked");
|
||||
}
|
||||
|
||||
export async function getServedViewerAsset(pathname: string): Promise<ServedViewerAsset | null> {
|
||||
if (pathname !== VIEWER_LOADER_PATH && pathname !== VIEWER_RUNTIME_PATH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assets = await loadViewerAssets();
|
||||
if (pathname === VIEWER_LOADER_PATH) {
|
||||
return {
|
||||
body: assets.loaderBody,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
body: assets.runtimeBody,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
};
|
||||
}
|
||||
|
||||
async function loadViewerAssets(): Promise<RuntimeAssetCache> {
|
||||
const runtimeUrl = await resolveViewerRuntimeFileUrl();
|
||||
const runtimePath = fileURLToPath(runtimeUrl);
|
||||
const runtimeStat = await fs.stat(runtimePath);
|
||||
if (runtimeAssetCache && runtimeAssetCache.mtimeMs === runtimeStat.mtimeMs) {
|
||||
return runtimeAssetCache;
|
||||
}
|
||||
|
||||
const runtimeBody = await fs.readFile(runtimePath);
|
||||
const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
|
||||
runtimeAssetCache = {
|
||||
mtimeMs: runtimeStat.mtimeMs,
|
||||
runtimeBody,
|
||||
loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`,
|
||||
};
|
||||
return runtimeAssetCache;
|
||||
}
|
||||
@@ -61,6 +61,7 @@ Useful options:
|
||||
- `expandUnchanged`: expand unchanged sections (per-call option only, not a plugin default key)
|
||||
- `path`: display name for before and after input
|
||||
- `lang`: language hint for before/after input; unknown values fall back to plain text
|
||||
- Default syntax highlighting covers common languages. Install `diffs-language-pack` for the extended language catalog.
|
||||
- `title`: explicit viewer title
|
||||
- `ttlSeconds`: artifact lifetime for viewer and standalone file outputs
|
||||
- `baseUrl`: override the gateway base URL used in the returned viewer link (origin or origin+base path only; no query/hash)
|
||||
|
||||
File diff suppressed because one or more lines are too long
5
extensions/diffs/npm-shrinkwrap.json
generated
5
extensions/diffs/npm-shrinkwrap.json
generated
@@ -10,7 +10,12 @@
|
||||
"dependencies": {
|
||||
"@pierre/diffs": "1.2.2",
|
||||
"@pierre/theme": "1.0.3",
|
||||
"@shikijs/core": "3.23.0",
|
||||
"@shikijs/engine-javascript": "3.23.0",
|
||||
"@shikijs/engine-oniguruma": "3.23.0",
|
||||
"@shikijs/langs": "3.23.0",
|
||||
"playwright-core": "1.60.0",
|
||||
"shiki": "3.23.0",
|
||||
"typebox": "1.1.38",
|
||||
"zod": "4.4.3"
|
||||
}
|
||||
|
||||
@@ -8,17 +8,23 @@
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:viewer": "bun build src/viewer-client.ts --target browser --format esm --minify --outfile assets/viewer-runtime.js && node -e \"let fs=require('fs'),p='assets/viewer-runtime.js',s=fs.readFileSync(p,'utf8').replace(/[ \\\\t]+$/gm,'');fs.writeFileSync(p,s)\""
|
||||
"build:viewer": "bun scripts/build-viewer.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pierre/diffs": "1.2.2",
|
||||
"@pierre/theme": "1.0.3",
|
||||
"@shikijs/core": "3.23.0",
|
||||
"@shikijs/engine-javascript": "3.23.0",
|
||||
"@shikijs/engine-oniguruma": "3.23.0",
|
||||
"@shikijs/langs": "3.23.0",
|
||||
"playwright-core": "1.60.0",
|
||||
"shiki": "3.23.0",
|
||||
"typebox": "1.1.38",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"esbuild": "0.28.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
@@ -33,6 +39,9 @@
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.26"
|
||||
},
|
||||
"assetScripts": {
|
||||
"build": "pnpm build:viewer"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.26",
|
||||
"staticAssets": [
|
||||
|
||||
49
extensions/diffs/scripts/build-viewer.mjs
Normal file
49
extensions/diffs/scripts/build-viewer.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { transform } from "esbuild";
|
||||
|
||||
const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outputPath = path.join(pluginRoot, "assets", "viewer-runtime.js");
|
||||
const curatedShikiPath = path.join(pluginRoot, "src", "shiki-curated.ts");
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [path.join(pluginRoot, "src", "viewer-client.ts")],
|
||||
target: "browser",
|
||||
format: "esm",
|
||||
minify: true,
|
||||
outdir: path.dirname(outputPath),
|
||||
naming: path.basename(outputPath),
|
||||
write: true,
|
||||
plugins: [
|
||||
{
|
||||
name: "openclaw-diffs-curated-shiki",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^shiki$/ }, () => ({
|
||||
path: curatedShikiPath,
|
||||
}));
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
for (const log of result.logs) {
|
||||
console.error(log.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runtime = await fs.readFile(outputPath, "utf8");
|
||||
const minified = await transform(runtime, {
|
||||
format: "esm",
|
||||
legalComments: "none",
|
||||
loader: "js",
|
||||
minify: true,
|
||||
target: "es2020",
|
||||
});
|
||||
await fs.writeFile(outputPath, minified.code.replace(/[ \t]+$/gm, ""));
|
||||
@@ -6,7 +6,12 @@ import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtim
|
||||
import { chromium } from "playwright-core";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import type { DiffRenderOptions, DiffTheme } from "./types.js";
|
||||
import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js";
|
||||
import {
|
||||
LANGUAGE_PACK_VIEWER_ASSET_PREFIX,
|
||||
VIEWER_ASSET_PREFIX,
|
||||
getServedLanguagePackViewerAsset,
|
||||
getServedViewerAsset,
|
||||
} from "./viewer-assets.js";
|
||||
|
||||
const DEFAULT_BROWSER_IDLE_MS = 30_000;
|
||||
const SHARED_BROWSER_KEY = "__default__";
|
||||
@@ -97,12 +102,18 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter {
|
||||
await route.abort();
|
||||
return;
|
||||
}
|
||||
if (!parsed.pathname.startsWith(VIEWER_ASSET_PREFIX)) {
|
||||
const isBaseViewerAsset = parsed.pathname.startsWith(VIEWER_ASSET_PREFIX);
|
||||
const isLanguagePackViewerAsset = parsed.pathname.startsWith(
|
||||
LANGUAGE_PACK_VIEWER_ASSET_PREFIX,
|
||||
);
|
||||
if (!isBaseViewerAsset && !isLanguagePackViewerAsset) {
|
||||
await route.abort();
|
||||
return;
|
||||
}
|
||||
const pathname = parsed.pathname;
|
||||
const asset = await getServedViewerAsset(pathname);
|
||||
const asset = isLanguagePackViewerAsset
|
||||
? await getServedLanguagePackViewerAsset(pathname)
|
||||
: await getServedViewerAsset(pathname);
|
||||
if (!asset) {
|
||||
await route.abort();
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { validateJsonSchemaValue, type JsonSchemaObject } from "openclaw/plugin-sdk/config-schema";
|
||||
@@ -12,10 +13,13 @@ import {
|
||||
resolveDiffsPluginSecurity,
|
||||
resolveDiffsPluginViewerBaseUrl,
|
||||
} from "./config.js";
|
||||
import { resolveDiffsLanguagePackAvailability } from "./plugin.js";
|
||||
import { buildViewerUrl, normalizeViewerBaseUrl } from "./url.js";
|
||||
import {
|
||||
getServedLanguagePackViewerAsset,
|
||||
getServedViewerAsset,
|
||||
resolveViewerRuntimeFileUrl,
|
||||
LANGUAGE_PACK_VIEWER_LOADER_PATH,
|
||||
VIEWER_LOADER_PATH,
|
||||
VIEWER_RUNTIME_PATH,
|
||||
} from "./viewer-assets.js";
|
||||
@@ -517,11 +521,52 @@ describe("viewer assets", () => {
|
||||
expect(String(runtime?.body)).toContain('style.gap="6px"');
|
||||
});
|
||||
|
||||
it("serves the optional language-pack loader only when its generated runtime is present", async () => {
|
||||
const loader = await getServedLanguagePackViewerAsset(LANGUAGE_PACK_VIEWER_LOADER_PATH);
|
||||
|
||||
if (!loader) {
|
||||
expect(loader).toBeNull();
|
||||
return;
|
||||
}
|
||||
expect(loader.contentType).toBe("text/javascript; charset=utf-8");
|
||||
expect(String(loader.body)).toContain(`./viewer-runtime.js?v=`);
|
||||
});
|
||||
|
||||
it("returns null for unknown asset paths", async () => {
|
||||
await expect(getServedViewerAsset("/plugins/diffs/assets/not-real.js")).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDiffsLanguagePackAvailability", () => {
|
||||
it("requires both the sibling language-pack manifest and generated runtime asset", () => {
|
||||
const root = fs.mkdtempSync(join(os.tmpdir(), "openclaw-diffs-language-pack-"));
|
||||
try {
|
||||
const diffsRoot = join(root, "diffs");
|
||||
const languagePackRoot = join(root, "diffs-language-pack");
|
||||
fs.mkdirSync(diffsRoot, { recursive: true });
|
||||
fs.mkdirSync(languagePackRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
join(languagePackRoot, "openclaw.plugin.json"),
|
||||
'{"id":"diffs-language-pack"}\n',
|
||||
);
|
||||
const api = {
|
||||
rootDir: diffsRoot,
|
||||
config: { plugins: {} },
|
||||
runtime: { config: { current: () => ({ plugins: {} }) } },
|
||||
} as Parameters<typeof resolveDiffsLanguagePackAvailability>[0];
|
||||
|
||||
expect(resolveDiffsLanguagePackAvailability(api)).toBe(false);
|
||||
|
||||
fs.mkdirSync(join(languagePackRoot, "assets"), { recursive: true });
|
||||
fs.writeFileSync(join(languagePackRoot, "assets", "viewer-runtime.js"), "export {};\n");
|
||||
|
||||
expect(resolveDiffsLanguagePackAvailability(api)).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseViewerPayloadJson", () => {
|
||||
function buildValidPayload(): Record<string, unknown> {
|
||||
return {
|
||||
|
||||
@@ -7,12 +7,29 @@ import {
|
||||
|
||||
describe("filterSupportedLanguageHints", () => {
|
||||
it("keeps supported languages", async () => {
|
||||
await expect(filterSupportedLanguageHints(["typescript", "text"])).resolves.toEqual([
|
||||
await expect(filterSupportedLanguageHints(["typescript", "cpp", "text"])).resolves.toEqual([
|
||||
"typescript",
|
||||
"cpp",
|
||||
"text",
|
||||
]);
|
||||
});
|
||||
|
||||
it("normalizes common aliases to base viewer languages", async () => {
|
||||
await expect(
|
||||
filterSupportedLanguageHints(["ts", "c++", "bash", "dockerfile"]),
|
||||
).resolves.toEqual(["typescript", "cpp", "sh", "docker"]);
|
||||
});
|
||||
|
||||
it("drops uncommon languages without the language pack", async () => {
|
||||
await expect(filterSupportedLanguageHints(["abap"])).resolves.toEqual(["text"]);
|
||||
});
|
||||
|
||||
it("keeps uncommon languages when the language pack is available", async () => {
|
||||
await expect(
|
||||
filterSupportedLanguageHints(["abap"], { languagePackAvailable: true }),
|
||||
).resolves.toEqual(["abap"]);
|
||||
});
|
||||
|
||||
it("drops invalid languages and falls back to text", async () => {
|
||||
await expect(filterSupportedLanguageHints(["not-a-real-language"])).resolves.toEqual(["text"]);
|
||||
});
|
||||
@@ -88,6 +105,37 @@ describe("normalizeDiffViewerPayloadLanguages", () => {
|
||||
expect(result.newFile?.lang).toBe("typescript");
|
||||
});
|
||||
|
||||
it("keeps uncommon hydrated languages when the language pack is available", async () => {
|
||||
const result = await normalizeDiffViewerPayloadLanguages(
|
||||
{
|
||||
prerenderedHTML: "<div>diff</div>",
|
||||
options: {
|
||||
theme: {
|
||||
light: "pierre-light",
|
||||
dark: "pierre-dark",
|
||||
},
|
||||
diffStyle: "unified",
|
||||
diffIndicators: "bars",
|
||||
disableLineNumbers: false,
|
||||
expandUnchanged: false,
|
||||
themeType: "dark",
|
||||
backgroundEnabled: true,
|
||||
overflow: "wrap",
|
||||
unsafeCSS: "",
|
||||
},
|
||||
langs: ["abap" as never],
|
||||
fileDiff: {
|
||||
name: "demo.abap",
|
||||
lang: "abap" as never,
|
||||
} as unknown as FileDiffMetadata,
|
||||
},
|
||||
{ languagePackAvailable: true },
|
||||
);
|
||||
|
||||
expect(result.langs).toEqual(["abap"]);
|
||||
expect(result.fileDiff?.lang).toBe("abap");
|
||||
});
|
||||
|
||||
it("rewrites blank explicit language overrides to plain text", async () => {
|
||||
const result = await normalizeDiffViewerPayloadLanguages({
|
||||
prerenderedHTML: "<div>diff</div>",
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import { resolveLanguage } from "@pierre/diffs";
|
||||
import type { FileContents, FileDiffMetadata, SupportedLanguages } from "@pierre/diffs";
|
||||
import { bundledLanguagesBase, bundledLanguagesInfo } from "./shiki-curated-languages.js";
|
||||
import type { DiffViewerPayload } from "./types.js";
|
||||
|
||||
const PASSTHROUGH_LANGUAGE_HINTS = new Set<SupportedLanguages>(["ansi", "text"]);
|
||||
export const BASE_DIFF_VIEWER_LANGUAGE_HINTS = [
|
||||
...Object.keys(bundledLanguagesBase),
|
||||
"text",
|
||||
"ansi",
|
||||
] as const satisfies readonly SupportedLanguages[];
|
||||
export type DiffViewerBaseLanguage = (typeof BASE_DIFF_VIEWER_LANGUAGE_HINTS)[number];
|
||||
|
||||
const BASE_LANGUAGE_HINTS = new Set<SupportedLanguages>(BASE_DIFF_VIEWER_LANGUAGE_HINTS);
|
||||
const BASE_LANGUAGE_ALIASES = new Map<string, SupportedLanguages>(
|
||||
bundledLanguagesInfo.flatMap(
|
||||
(language) =>
|
||||
language.aliases?.map((alias) => [alias, language.id as SupportedLanguages]) ?? [],
|
||||
),
|
||||
);
|
||||
type DiffPayloadFile = FileContents | FileDiffMetadata;
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
@@ -15,14 +29,22 @@ function normalizeOptionalString(value: unknown): string | undefined {
|
||||
|
||||
export async function normalizeSupportedLanguageHint(
|
||||
value?: string,
|
||||
options: { languagePackAvailable?: boolean } = {},
|
||||
): Promise<SupportedLanguages | undefined> {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (PASSTHROUGH_LANGUAGE_HINTS.has(normalized as SupportedLanguages)) {
|
||||
const baseAlias = BASE_LANGUAGE_ALIASES.get(normalized);
|
||||
if (baseAlias) {
|
||||
return baseAlias;
|
||||
}
|
||||
if (BASE_LANGUAGE_HINTS.has(normalized as SupportedLanguages)) {
|
||||
return normalized as SupportedLanguages;
|
||||
}
|
||||
if (!options.languagePackAvailable) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
await resolveLanguage(normalized as Exclude<SupportedLanguages, "text" | "ansi">);
|
||||
return normalized as SupportedLanguages;
|
||||
@@ -33,17 +55,18 @@ export async function normalizeSupportedLanguageHint(
|
||||
|
||||
export async function filterSupportedLanguageHints(
|
||||
values: Iterable<string>,
|
||||
options: { languagePackAvailable?: boolean } = {},
|
||||
): Promise<SupportedLanguages[]> {
|
||||
return normalizeSupportedLanguageHints(values, { fallbackToText: true });
|
||||
return normalizeSupportedLanguageHints(values, { fallbackToText: true, ...options });
|
||||
}
|
||||
|
||||
async function normalizeSupportedLanguageHints(
|
||||
values: Iterable<string>,
|
||||
options: { fallbackToText: boolean },
|
||||
options: { fallbackToText: boolean; languagePackAvailable?: boolean },
|
||||
): Promise<SupportedLanguages[]> {
|
||||
const supported = new Set<SupportedLanguages>();
|
||||
for (const value of values) {
|
||||
const normalized = await normalizeSupportedLanguageHint(value);
|
||||
const normalized = await normalizeSupportedLanguageHint(value, options);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
@@ -75,6 +98,7 @@ export function collectDiffPayloadLanguageHints(payload: {
|
||||
|
||||
async function normalizeDiffPayloadFileLanguage(
|
||||
file: DiffPayloadFile | undefined,
|
||||
options: { languagePackAvailable?: boolean },
|
||||
): Promise<DiffPayloadFile | undefined> {
|
||||
if (!file) {
|
||||
return undefined;
|
||||
@@ -82,7 +106,7 @@ async function normalizeDiffPayloadFileLanguage(
|
||||
if (typeof file.lang !== "string") {
|
||||
return file;
|
||||
}
|
||||
const normalized = await normalizeSupportedLanguageHint(file.lang);
|
||||
const normalized = await normalizeSupportedLanguageHint(file.lang, options);
|
||||
if (file.lang === normalized) {
|
||||
return file;
|
||||
}
|
||||
@@ -100,12 +124,15 @@ async function normalizeDiffPayloadFileLanguage(
|
||||
|
||||
export async function normalizeDiffViewerPayloadLanguages(
|
||||
payload: DiffViewerPayload,
|
||||
options: { languagePackAvailable?: boolean } = {},
|
||||
): Promise<DiffViewerPayload> {
|
||||
const [fileDiff, oldFile, newFile, payloadLangs] = await Promise.all([
|
||||
normalizeDiffPayloadFileLanguage(payload.fileDiff) as Promise<FileDiffMetadata | undefined>,
|
||||
normalizeDiffPayloadFileLanguage(payload.oldFile) as Promise<FileContents | undefined>,
|
||||
normalizeDiffPayloadFileLanguage(payload.newFile) as Promise<FileContents | undefined>,
|
||||
normalizeSupportedLanguageHints(payload.langs, { fallbackToText: false }),
|
||||
normalizeDiffPayloadFileLanguage(payload.fileDiff, options) as Promise<
|
||||
FileDiffMetadata | undefined
|
||||
>,
|
||||
normalizeDiffPayloadFileLanguage(payload.oldFile, options) as Promise<FileContents | undefined>,
|
||||
normalizeDiffPayloadFileLanguage(payload.newFile, options) as Promise<FileContents | undefined>,
|
||||
normalizeSupportedLanguageHints(payload.langs, { fallbackToText: false, ...options }),
|
||||
]);
|
||||
const langs = new Set<SupportedLanguages>(payloadLangs);
|
||||
for (const lang of collectDiffPayloadLanguageHints({ fileDiff, oldFile, newFile })) {
|
||||
@@ -122,3 +149,7 @@ export async function normalizeDiffViewerPayloadLanguages(
|
||||
langs: [...langs],
|
||||
};
|
||||
}
|
||||
|
||||
export function isBaseDiffViewerLanguage(lang: string): boolean {
|
||||
return BASE_LANGUAGE_HINTS.has(lang as SupportedLanguages);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import {
|
||||
@@ -15,6 +16,8 @@ import { DIFFS_AGENT_GUIDANCE } from "./prompt-guidance.js";
|
||||
import { DiffArtifactStore } from "./store.js";
|
||||
import { createDiffsTool } from "./tool.js";
|
||||
|
||||
const DIFFS_LANGUAGE_PACK_PLUGIN_ID = "diffs-language-pack";
|
||||
|
||||
export function registerDiffsPlugin(api: OpenClawPluginApi): void {
|
||||
const store = new DiffArtifactStore({
|
||||
rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
|
||||
@@ -47,6 +50,7 @@ export function registerDiffsPlugin(api: OpenClawPluginApi): void {
|
||||
store,
|
||||
defaults: resolveDiffsPluginDefaults(pluginConfig),
|
||||
viewerBaseUrl: resolveDiffsPluginViewerBaseUrl(pluginConfig),
|
||||
languagePackAvailable: resolveDiffsLanguagePackAvailability(api),
|
||||
context: ctx,
|
||||
});
|
||||
},
|
||||
@@ -71,3 +75,32 @@ export function registerDiffsPlugin(api: OpenClawPluginApi): void {
|
||||
prependSystemContext: DIFFS_AGENT_GUIDANCE,
|
||||
}));
|
||||
}
|
||||
|
||||
export function resolveDiffsLanguagePackAvailability(api: OpenClawPluginApi): boolean {
|
||||
const currentConfig = (api.runtime.config?.current?.() ?? api.config) as OpenClawConfig;
|
||||
const plugins = currentConfig.plugins;
|
||||
if (plugins?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
if (plugins?.deny?.includes(DIFFS_LANGUAGE_PACK_PLUGIN_ID)) {
|
||||
return false;
|
||||
}
|
||||
if (plugins?.allow && !plugins.allow.includes(DIFFS_LANGUAGE_PACK_PLUGIN_ID)) {
|
||||
return false;
|
||||
}
|
||||
if (plugins?.entries?.[DIFFS_LANGUAGE_PACK_PLUGIN_ID]?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
return hasSiblingLanguagePackRuntime(api.rootDir);
|
||||
}
|
||||
|
||||
function hasSiblingLanguagePackRuntime(rootDir: string | undefined): boolean {
|
||||
if (!rootDir) {
|
||||
return false;
|
||||
}
|
||||
const languagePackRoot = path.join(path.dirname(rootDir), DIFFS_LANGUAGE_PACK_PLUGIN_ID);
|
||||
return (
|
||||
fs.existsSync(path.join(languagePackRoot, "openclaw.plugin.json")) &&
|
||||
fs.existsSync(path.join(languagePackRoot, "assets", "viewer-runtime.js"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ describe("renderDiffDocument", () => {
|
||||
|
||||
expect(rendered.title).toBe("src/example.ts");
|
||||
expect(rendered.fileCount).toBe(1);
|
||||
expect(rendered.viewerRuntime).toBe("base");
|
||||
expect(rendered.html).toContain("data-openclaw-diff-root");
|
||||
expect(rendered.html).toContain("src/example.ts");
|
||||
expect(rendered.html).toContain("../../assets/viewer.js");
|
||||
@@ -97,6 +98,60 @@ describe("renderDiffDocument", () => {
|
||||
expect(payloads[0]?.newFile?.lang).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps uncommon language diffs readable without the language pack", async () => {
|
||||
const rendered = await renderDiffDocument(
|
||||
{
|
||||
kind: "before_after",
|
||||
before: "REPORT z_demo.\n",
|
||||
after: "REPORT z_demo2.\n",
|
||||
lang: "abap",
|
||||
},
|
||||
{
|
||||
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
|
||||
expandUnchanged: false,
|
||||
},
|
||||
"viewer",
|
||||
);
|
||||
|
||||
const html = rendered.html ?? "";
|
||||
const payload = parseViewerPayloadJson(
|
||||
html.match(/data-openclaw-diff-payload>(.*?)<\/script>/)?.[1] ?? "",
|
||||
);
|
||||
|
||||
expect(rendered.viewerRuntime).toBe("base");
|
||||
expect(html).toContain("../../assets/viewer.js");
|
||||
expect(html).not.toContain("diffs-language-pack");
|
||||
expect(payload.langs).toEqual(["text"]);
|
||||
});
|
||||
|
||||
it("uses the language-pack viewer runtime for uncommon languages when available", async () => {
|
||||
const rendered = await renderDiffDocument(
|
||||
{
|
||||
kind: "before_after",
|
||||
before: "REPORT z_demo.\n",
|
||||
after: "REPORT z_demo2.\n",
|
||||
lang: "abap",
|
||||
},
|
||||
{
|
||||
presentation: DEFAULT_DIFFS_TOOL_DEFAULTS,
|
||||
image: resolveDiffImageRenderOptions({ defaults: DEFAULT_DIFFS_TOOL_DEFAULTS }),
|
||||
expandUnchanged: false,
|
||||
languagePackAvailable: true,
|
||||
},
|
||||
"viewer",
|
||||
);
|
||||
|
||||
const html = rendered.html ?? "";
|
||||
const payload = parseViewerPayloadJson(
|
||||
html.match(/data-openclaw-diff-payload>(.*?)<\/script>/)?.[1] ?? "",
|
||||
);
|
||||
|
||||
expect(rendered.viewerRuntime).toBe("language-pack");
|
||||
expect(html).toContain("../../../diffs-language-pack/assets/viewer.js");
|
||||
expect(payload.langs).toEqual(["abap"]);
|
||||
});
|
||||
|
||||
it("renders multi-file patch input", async () => {
|
||||
const patch = [
|
||||
"diff --git a/a.ts b/a.ts",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { parsePatchFiles } from "@pierre/diffs";
|
||||
import { preloadFileDiff, preloadMultiFileDiff } from "@pierre/diffs/ssr";
|
||||
import {
|
||||
collectDiffPayloadLanguageHints,
|
||||
isBaseDiffViewerLanguage,
|
||||
normalizeDiffViewerPayloadLanguages,
|
||||
normalizeSupportedLanguageHint,
|
||||
} from "./language-hints.js";
|
||||
@@ -20,6 +21,7 @@ const DEFAULT_FILE_NAME = "diff.txt";
|
||||
const MAX_PATCH_FILE_COUNT = 128;
|
||||
const MAX_PATCH_TOTAL_LINES = 120_000;
|
||||
const VIEWER_LOADER_DOCUMENT_PATH = "../../assets/viewer.js";
|
||||
const LANGUAGE_PACK_VIEWER_LOADER_DOCUMENT_PATH = "../../../diffs-language-pack/assets/viewer.js";
|
||||
|
||||
function escapeCssString(value: string): string {
|
||||
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
@@ -198,7 +200,12 @@ function buildHtmlDocument(params: {
|
||||
theme: DiffRenderOptions["presentation"]["theme"];
|
||||
imageMaxWidth: number;
|
||||
runtimeMode: "viewer" | "image";
|
||||
viewerRuntime: "base" | "language-pack";
|
||||
}): string {
|
||||
const viewerLoaderPath =
|
||||
params.viewerRuntime === "language-pack"
|
||||
? LANGUAGE_PACK_VIEWER_LOADER_DOCUMENT_PATH
|
||||
: VIEWER_LOADER_DOCUMENT_PATH;
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -292,7 +299,7 @@ function buildHtmlDocument(params: {
|
||||
${params.bodyHtml}
|
||||
</div>
|
||||
</main>
|
||||
<script type="module" src="${VIEWER_LOADER_DOCUMENT_PATH}"></script>
|
||||
<script type="module" src="${viewerLoaderPath}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -300,8 +307,13 @@ function buildHtmlDocument(params: {
|
||||
type RenderedSection = {
|
||||
viewer?: string;
|
||||
image?: string;
|
||||
usesLanguagePack?: boolean;
|
||||
};
|
||||
|
||||
function payloadUsesLanguagePack(payload: DiffViewerPayload | undefined): boolean {
|
||||
return payload?.langs.some((lang) => !isBaseDiffViewerLanguage(lang)) ?? false;
|
||||
}
|
||||
|
||||
function buildRenderedSection(params: {
|
||||
viewerPayload?: DiffViewerPayload;
|
||||
imagePayload?: DiffViewerPayload;
|
||||
@@ -309,6 +321,8 @@ function buildRenderedSection(params: {
|
||||
return {
|
||||
...(params.viewerPayload ? { viewer: renderDiffCard(params.viewerPayload) } : {}),
|
||||
...(params.imagePayload ? { image: renderDiffCard(params.imagePayload) } : {}),
|
||||
usesLanguagePack:
|
||||
payloadUsesLanguagePack(params.viewerPayload) || payloadUsesLanguagePack(params.imagePayload),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -328,10 +342,16 @@ async function renderBeforeAfterDiff(
|
||||
input: Extract<DiffInput, { kind: "before_after" }>,
|
||||
options: DiffRenderOptions,
|
||||
target: DiffRenderTarget,
|
||||
): Promise<{ viewerBodyHtml?: string; imageBodyHtml?: string; fileCount: number }> {
|
||||
): Promise<{
|
||||
viewerBodyHtml?: string;
|
||||
imageBodyHtml?: string;
|
||||
fileCount: number;
|
||||
usesLanguagePack: boolean;
|
||||
}> {
|
||||
ensurePierreThemesRegistered();
|
||||
|
||||
const lang = await normalizeSupportedLanguageHint(input.lang);
|
||||
const languagePackAvailable = options.languagePackAvailable === true;
|
||||
const lang = await normalizeSupportedLanguageHint(input.lang, { languagePackAvailable });
|
||||
const fileName = resolveBeforeAfterFileName({ input, lang });
|
||||
const oldFile: FileContents = {
|
||||
name: fileName,
|
||||
@@ -362,28 +382,34 @@ async function renderBeforeAfterDiff(
|
||||
]);
|
||||
const [viewerPayload, imagePayload] = await Promise.all([
|
||||
viewerResult && viewerOptions
|
||||
? normalizeDiffViewerPayloadLanguages({
|
||||
prerenderedHTML: viewerResult.prerenderedHTML,
|
||||
oldFile: viewerResult.oldFile,
|
||||
newFile: viewerResult.newFile,
|
||||
options: viewerOptions,
|
||||
langs: collectDiffPayloadLanguageHints({
|
||||
? normalizeDiffViewerPayloadLanguages(
|
||||
{
|
||||
prerenderedHTML: viewerResult.prerenderedHTML,
|
||||
oldFile: viewerResult.oldFile,
|
||||
newFile: viewerResult.newFile,
|
||||
}),
|
||||
})
|
||||
options: viewerOptions,
|
||||
langs: collectDiffPayloadLanguageHints({
|
||||
oldFile: viewerResult.oldFile,
|
||||
newFile: viewerResult.newFile,
|
||||
}),
|
||||
},
|
||||
{ languagePackAvailable },
|
||||
)
|
||||
: Promise.resolve(undefined),
|
||||
imageResult && imageOptions
|
||||
? normalizeDiffViewerPayloadLanguages({
|
||||
prerenderedHTML: imageResult.prerenderedHTML,
|
||||
oldFile: imageResult.oldFile,
|
||||
newFile: imageResult.newFile,
|
||||
options: imageOptions,
|
||||
langs: collectDiffPayloadLanguageHints({
|
||||
? normalizeDiffViewerPayloadLanguages(
|
||||
{
|
||||
prerenderedHTML: imageResult.prerenderedHTML,
|
||||
oldFile: imageResult.oldFile,
|
||||
newFile: imageResult.newFile,
|
||||
}),
|
||||
})
|
||||
options: imageOptions,
|
||||
langs: collectDiffPayloadLanguageHints({
|
||||
oldFile: imageResult.oldFile,
|
||||
newFile: imageResult.newFile,
|
||||
}),
|
||||
},
|
||||
{ languagePackAvailable },
|
||||
)
|
||||
: Promise.resolve(undefined),
|
||||
]);
|
||||
const section = buildRenderedSection({
|
||||
@@ -394,6 +420,7 @@ async function renderBeforeAfterDiff(
|
||||
return {
|
||||
...buildRenderedBodies([section]),
|
||||
fileCount: 1,
|
||||
usesLanguagePack: section.usesLanguagePack === true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -401,10 +428,20 @@ async function renderPatchDiff(
|
||||
input: Extract<DiffInput, { kind: "patch" }>,
|
||||
options: DiffRenderOptions,
|
||||
target: DiffRenderTarget,
|
||||
): Promise<{ viewerBodyHtml?: string; imageBodyHtml?: string; fileCount: number }> {
|
||||
): Promise<{
|
||||
viewerBodyHtml?: string;
|
||||
imageBodyHtml?: string;
|
||||
fileCount: number;
|
||||
usesLanguagePack: boolean;
|
||||
}> {
|
||||
ensurePierreThemesRegistered();
|
||||
|
||||
const files = parsePatchFiles(input.patch).flatMap((entry) => entry.files ?? []);
|
||||
const languagePackAvailable = options.languagePackAvailable === true;
|
||||
const files = await Promise.all(
|
||||
parsePatchFiles(input.patch)
|
||||
.flatMap((entry) => entry.files ?? [])
|
||||
.map((fileDiff) => normalizePatchFileLanguage(fileDiff, { languagePackAvailable })),
|
||||
);
|
||||
if (files.length === 0) {
|
||||
throw new Error("Patch input did not contain any file diffs.");
|
||||
}
|
||||
@@ -440,20 +477,26 @@ async function renderPatchDiff(
|
||||
|
||||
const [viewerPayload, imagePayload] = await Promise.all([
|
||||
viewerResult && viewerOptions
|
||||
? normalizeDiffViewerPayloadLanguages({
|
||||
prerenderedHTML: viewerResult.prerenderedHTML,
|
||||
fileDiff: viewerResult.fileDiff,
|
||||
options: viewerOptions,
|
||||
langs: collectDiffPayloadLanguageHints({ fileDiff: viewerResult.fileDiff }),
|
||||
})
|
||||
? normalizeDiffViewerPayloadLanguages(
|
||||
{
|
||||
prerenderedHTML: viewerResult.prerenderedHTML,
|
||||
fileDiff: viewerResult.fileDiff,
|
||||
options: viewerOptions,
|
||||
langs: collectDiffPayloadLanguageHints({ fileDiff: viewerResult.fileDiff }),
|
||||
},
|
||||
{ languagePackAvailable },
|
||||
)
|
||||
: Promise.resolve(undefined),
|
||||
imageResult && imageOptions
|
||||
? normalizeDiffViewerPayloadLanguages({
|
||||
prerenderedHTML: imageResult.prerenderedHTML,
|
||||
fileDiff: imageResult.fileDiff,
|
||||
options: imageOptions,
|
||||
langs: collectDiffPayloadLanguageHints({ fileDiff: imageResult.fileDiff }),
|
||||
})
|
||||
? normalizeDiffViewerPayloadLanguages(
|
||||
{
|
||||
prerenderedHTML: imageResult.prerenderedHTML,
|
||||
fileDiff: imageResult.fileDiff,
|
||||
options: imageOptions,
|
||||
langs: collectDiffPayloadLanguageHints({ fileDiff: imageResult.fileDiff }),
|
||||
},
|
||||
{ languagePackAvailable },
|
||||
)
|
||||
: Promise.resolve(undefined),
|
||||
]);
|
||||
|
||||
@@ -467,6 +510,21 @@ async function renderPatchDiff(
|
||||
return {
|
||||
...buildRenderedBodies(sections),
|
||||
fileCount: files.length,
|
||||
usesLanguagePack: sections.some((section) => section.usesLanguagePack === true),
|
||||
};
|
||||
}
|
||||
|
||||
async function normalizePatchFileLanguage(
|
||||
fileDiff: FileDiffMetadata,
|
||||
options: { languagePackAvailable: boolean },
|
||||
): Promise<FileDiffMetadata> {
|
||||
const lang = await normalizeSupportedLanguageHint(fileDiff.lang, options);
|
||||
if (lang === fileDiff.lang) {
|
||||
return fileDiff;
|
||||
}
|
||||
return {
|
||||
...fileDiff,
|
||||
...(lang ? { lang } : { lang: "text" }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -480,6 +538,7 @@ export async function renderDiffDocument(
|
||||
input.kind === "before_after"
|
||||
? await renderBeforeAfterDiff(input, options, target)
|
||||
: await renderPatchDiff(input, options, target);
|
||||
const viewerRuntime = rendered.usesLanguagePack ? "language-pack" : "base";
|
||||
|
||||
return {
|
||||
...(rendered.viewerBodyHtml
|
||||
@@ -490,6 +549,7 @@ export async function renderDiffDocument(
|
||||
theme: options.presentation.theme,
|
||||
imageMaxWidth: options.image.maxWidth,
|
||||
runtimeMode: "viewer",
|
||||
viewerRuntime,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
@@ -501,12 +561,14 @@ export async function renderDiffDocument(
|
||||
theme: options.presentation.theme,
|
||||
imageMaxWidth: options.image.maxWidth,
|
||||
runtimeMode: "image",
|
||||
viewerRuntime,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
title,
|
||||
fileCount: rendered.fileCount,
|
||||
inputKind: input.kind,
|
||||
viewerRuntime,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
56
extensions/diffs/src/shiki-curated-languages.ts
Normal file
56
extensions/diffs/src/shiki-curated-languages.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
const javascript = () => import("@shikijs/langs/javascript");
|
||||
const typescript = () => import("@shikijs/langs/typescript");
|
||||
const tsx = () => import("@shikijs/langs/tsx");
|
||||
const jsx = () => import("@shikijs/langs/jsx");
|
||||
const json = () => import("@shikijs/langs/json");
|
||||
const markdown = () => import("@shikijs/langs/markdown");
|
||||
const yaml = () => import("@shikijs/langs/yaml");
|
||||
const css = () => import("@shikijs/langs/css");
|
||||
const html = () => import("@shikijs/langs/html");
|
||||
const sh = () => import("@shikijs/langs/sh");
|
||||
const python = () => import("@shikijs/langs/python");
|
||||
const go = () => import("@shikijs/langs/go");
|
||||
const rust = () => import("@shikijs/langs/rust");
|
||||
const java = () => import("@shikijs/langs/java");
|
||||
const c = () => import("@shikijs/langs/c");
|
||||
const cpp = () => import("@shikijs/langs/cpp");
|
||||
const csharp = () => import("@shikijs/langs/csharp");
|
||||
const php = () => import("@shikijs/langs/php");
|
||||
const sql = () => import("@shikijs/langs/sql");
|
||||
const docker = () => import("@shikijs/langs/docker");
|
||||
|
||||
export const bundledLanguagesInfo = [
|
||||
{ id: "javascript", name: "JavaScript", aliases: ["js", "mjs", "cjs"], import: javascript },
|
||||
{ id: "typescript", name: "TypeScript", aliases: ["ts", "mts", "cts"], import: typescript },
|
||||
{ id: "tsx", name: "TSX", import: tsx },
|
||||
{ id: "jsx", name: "JSX", import: jsx },
|
||||
{ id: "json", name: "JSON", aliases: ["jsonc", "json5", "jsonl"], import: json },
|
||||
{ id: "markdown", name: "Markdown", aliases: ["md"], import: markdown },
|
||||
{ id: "yaml", name: "YAML", aliases: ["yml"], import: yaml },
|
||||
{ id: "css", name: "CSS", import: css },
|
||||
{ id: "html", name: "HTML", import: html },
|
||||
{ id: "sh", name: "Shell", aliases: ["bash", "shell", "shellscript", "zsh"], import: sh },
|
||||
{ id: "python", name: "Python", aliases: ["py"], import: python },
|
||||
{ id: "go", name: "Go", import: go },
|
||||
{ id: "rust", name: "Rust", aliases: ["rs"], import: rust },
|
||||
{ id: "java", name: "Java", import: java },
|
||||
{ id: "c", name: "C", import: c },
|
||||
{ id: "cpp", name: "C++", aliases: ["c++"], import: cpp },
|
||||
{ id: "csharp", name: "C#", aliases: ["cs"], import: csharp },
|
||||
{ id: "php", name: "PHP", import: php },
|
||||
{ id: "sql", name: "SQL", import: sql },
|
||||
{ id: "docker", name: "Docker", aliases: ["dockerfile"], import: docker },
|
||||
] as const;
|
||||
|
||||
export const bundledLanguagesBase = Object.fromEntries(
|
||||
bundledLanguagesInfo.map((language) => [language.id, language.import]),
|
||||
);
|
||||
export const bundledLanguagesAlias = Object.fromEntries(
|
||||
bundledLanguagesInfo.flatMap(
|
||||
(language) => language.aliases?.map((alias) => [alias, language.import]) ?? [],
|
||||
),
|
||||
);
|
||||
export const bundledLanguages = {
|
||||
...bundledLanguagesBase,
|
||||
...bundledLanguagesAlias,
|
||||
};
|
||||
53
extensions/diffs/src/shiki-curated.ts
Normal file
53
extensions/diffs/src/shiki-curated.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
createBundledHighlighter,
|
||||
createCssVariablesTheme,
|
||||
createSingletonShorthands,
|
||||
getTokenStyleObject,
|
||||
guessEmbeddedLanguages,
|
||||
normalizeTheme,
|
||||
stringifyTokenStyle,
|
||||
} from "@shikijs/core";
|
||||
import {
|
||||
createJavaScriptRegexEngine,
|
||||
defaultJavaScriptRegexConstructor,
|
||||
} from "@shikijs/engine-javascript";
|
||||
import { createOnigurumaEngine, loadWasm } from "@shikijs/engine-oniguruma";
|
||||
import { bundledLanguages } from "./shiki-curated-languages.js";
|
||||
export * from "@shikijs/core";
|
||||
export {
|
||||
bundledLanguages,
|
||||
bundledLanguagesAlias,
|
||||
bundledLanguagesBase,
|
||||
bundledLanguagesInfo,
|
||||
} from "./shiki-curated-languages.js";
|
||||
export { bundledThemes, bundledThemesInfo } from "shiki/themes";
|
||||
import { bundledThemes } from "shiki/themes";
|
||||
|
||||
export type BundledLanguage = keyof typeof bundledLanguages;
|
||||
export type BundledTheme = keyof typeof bundledThemes;
|
||||
|
||||
export const createHighlighter = createBundledHighlighter({
|
||||
langs: bundledLanguages,
|
||||
themes: bundledThemes,
|
||||
engine: () => createOnigurumaEngine(import("shiki/wasm")),
|
||||
});
|
||||
|
||||
const shorthands = createSingletonShorthands(createHighlighter, { guessEmbeddedLanguages });
|
||||
|
||||
export const codeToHtml = shorthands.codeToHtml;
|
||||
export const codeToHast = shorthands.codeToHast;
|
||||
export const codeToTokens = shorthands.codeToTokens;
|
||||
export const codeToTokensBase = shorthands.codeToTokensBase;
|
||||
export const codeToTokensWithThemes = shorthands.codeToTokensWithThemes;
|
||||
export const getSingletonHighlighter = shorthands.getSingletonHighlighter;
|
||||
export const getLastGrammarState = shorthands.getLastGrammarState;
|
||||
export {
|
||||
createCssVariablesTheme,
|
||||
createJavaScriptRegexEngine,
|
||||
createOnigurumaEngine,
|
||||
defaultJavaScriptRegexConstructor,
|
||||
getTokenStyleObject,
|
||||
loadWasm,
|
||||
normalizeTheme,
|
||||
stringifyTokenStyle,
|
||||
};
|
||||
@@ -159,6 +159,7 @@ export function createDiffsTool(params: {
|
||||
store: DiffArtifactStore;
|
||||
defaults: DiffToolDefaults;
|
||||
viewerBaseUrl?: string;
|
||||
languagePackAvailable?: boolean;
|
||||
screenshotter?: DiffScreenshotter;
|
||||
context?: OpenClawPluginToolContext;
|
||||
}): AnyAgentTool {
|
||||
@@ -198,6 +199,7 @@ export function createDiffsTool(params: {
|
||||
},
|
||||
image,
|
||||
expandUnchanged,
|
||||
languagePackAvailable: params.languagePackAvailable,
|
||||
},
|
||||
renderTarget,
|
||||
);
|
||||
|
||||
@@ -67,6 +67,7 @@ export type DiffRenderOptions = {
|
||||
maxPixels: number;
|
||||
};
|
||||
expandUnchanged: boolean;
|
||||
languagePackAvailable?: boolean;
|
||||
};
|
||||
|
||||
export type DiffViewerOptions = {
|
||||
@@ -99,6 +100,7 @@ export type RenderedDiffDocument = {
|
||||
title: string;
|
||||
fileCount: number;
|
||||
inputKind: DiffInput["kind"];
|
||||
viewerRuntime: "base" | "language-pack";
|
||||
};
|
||||
|
||||
export type DiffArtifactContext = {
|
||||
|
||||
@@ -5,11 +5,18 @@ import { fileURLToPath } from "node:url";
|
||||
export const VIEWER_ASSET_PREFIX = "/plugins/diffs/assets/";
|
||||
export const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
|
||||
export const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
||||
export const LANGUAGE_PACK_VIEWER_ASSET_PREFIX = "/plugins/diffs-language-pack/assets/";
|
||||
export const LANGUAGE_PACK_VIEWER_LOADER_PATH = `${LANGUAGE_PACK_VIEWER_ASSET_PREFIX}viewer.js`;
|
||||
export const LANGUAGE_PACK_VIEWER_RUNTIME_PATH = `${LANGUAGE_PACK_VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
||||
const VIEWER_RUNTIME_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
|
||||
const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = [
|
||||
"./assets/viewer-runtime.js",
|
||||
"../assets/viewer-runtime.js",
|
||||
] as const;
|
||||
const LANGUAGE_PACK_RUNTIME_CANDIDATE_RELATIVE_PATHS = [
|
||||
"../../diffs-language-pack/assets/viewer-runtime.js",
|
||||
"../diffs-language-pack/assets/viewer-runtime.js",
|
||||
] as const;
|
||||
|
||||
type ServedViewerAsset = {
|
||||
body: string | Buffer;
|
||||
@@ -23,6 +30,7 @@ type RuntimeAssetCache = {
|
||||
};
|
||||
|
||||
let runtimeAssetCache: RuntimeAssetCache | null = null;
|
||||
let languagePackRuntimeAssetCache: RuntimeAssetCache | null = null;
|
||||
|
||||
type ViewerRuntimeFileUrlParams = {
|
||||
baseUrl?: string | URL;
|
||||
@@ -84,20 +92,98 @@ export async function getServedViewerAsset(pathname: string): Promise<ServedView
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServedLanguagePackViewerAsset(
|
||||
pathname: string,
|
||||
): Promise<ServedViewerAsset | null> {
|
||||
if (
|
||||
pathname !== LANGUAGE_PACK_VIEWER_LOADER_PATH &&
|
||||
pathname !== LANGUAGE_PACK_VIEWER_RUNTIME_PATH
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let assets: RuntimeAssetCache;
|
||||
try {
|
||||
const runtimeUrl = await resolveRuntimeFileUrl(LANGUAGE_PACK_RUNTIME_CANDIDATE_RELATIVE_PATHS);
|
||||
assets = await loadRuntimeAssets({
|
||||
runtimeUrl,
|
||||
cache: languagePackRuntimeAssetCache,
|
||||
updateCache: (cache) => {
|
||||
languagePackRuntimeAssetCache = cache;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (isMissingFileError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (pathname === LANGUAGE_PACK_VIEWER_LOADER_PATH) {
|
||||
return {
|
||||
body: assets.loaderBody,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
body: assets.runtimeBody,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
};
|
||||
}
|
||||
|
||||
async function loadViewerAssets(): Promise<RuntimeAssetCache> {
|
||||
const runtimeUrl = await resolveViewerRuntimeFileUrl();
|
||||
const runtimePath = fileURLToPath(runtimeUrl);
|
||||
return loadRuntimeAssets({
|
||||
runtimeUrl,
|
||||
cache: runtimeAssetCache,
|
||||
updateCache: (cache) => {
|
||||
runtimeAssetCache = cache;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRuntimeAssets(params: {
|
||||
cache: RuntimeAssetCache | null;
|
||||
runtimeUrl: URL;
|
||||
updateCache(cache: RuntimeAssetCache): void;
|
||||
}): Promise<RuntimeAssetCache> {
|
||||
const runtimePath = fileURLToPath(params.runtimeUrl);
|
||||
const runtimeStat = await fs.stat(runtimePath);
|
||||
if (runtimeAssetCache && runtimeAssetCache.mtimeMs === runtimeStat.mtimeMs) {
|
||||
return runtimeAssetCache;
|
||||
if (params.cache && params.cache.mtimeMs === runtimeStat.mtimeMs) {
|
||||
return params.cache;
|
||||
}
|
||||
|
||||
const runtimeBody = await fs.readFile(runtimePath);
|
||||
const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
|
||||
runtimeAssetCache = {
|
||||
const cache = {
|
||||
mtimeMs: runtimeStat.mtimeMs,
|
||||
runtimeBody,
|
||||
loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`,
|
||||
};
|
||||
return runtimeAssetCache;
|
||||
params.updateCache(cache);
|
||||
return cache;
|
||||
}
|
||||
|
||||
async function resolveRuntimeFileUrl(relativePaths: readonly string[]): Promise<URL> {
|
||||
let missingFileError: NodeJS.ErrnoException | null = null;
|
||||
|
||||
for (const relativePath of relativePaths) {
|
||||
const candidateUrl = new URL(relativePath, import.meta.url);
|
||||
try {
|
||||
await fs.stat(fileURLToPath(candidateUrl));
|
||||
return candidateUrl;
|
||||
} catch (error) {
|
||||
if (isMissingFileError(error)) {
|
||||
missingFileError = error;
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFileError) {
|
||||
throw missingFileError;
|
||||
}
|
||||
|
||||
throw new Error("viewer runtime asset candidates were not checked");
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"!dist/extensions/diagnostics-otel/**",
|
||||
"!dist/extensions/diagnostics-prometheus/**",
|
||||
"!dist/extensions/diffs/**",
|
||||
"!dist/extensions/diffs-language-pack/**",
|
||||
"!dist/extensions/discord/**",
|
||||
"!dist/extensions/feishu/**",
|
||||
"!dist/extensions/google-meet/**",
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -597,9 +597,24 @@ importers:
|
||||
'@pierre/theme':
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3
|
||||
'@shikijs/core':
|
||||
specifier: 3.23.0
|
||||
version: 3.23.0
|
||||
'@shikijs/engine-javascript':
|
||||
specifier: 3.23.0
|
||||
version: 3.23.0
|
||||
'@shikijs/engine-oniguruma':
|
||||
specifier: 3.23.0
|
||||
version: 3.23.0
|
||||
'@shikijs/langs':
|
||||
specifier: 3.23.0
|
||||
version: 3.23.0
|
||||
playwright-core:
|
||||
specifier: 1.60.0
|
||||
version: 1.60.0
|
||||
shiki:
|
||||
specifier: 3.23.0
|
||||
version: 3.23.0
|
||||
typebox:
|
||||
specifier: 1.1.38
|
||||
version: 1.1.38
|
||||
@@ -610,6 +625,18 @@ importers:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/plugin-sdk
|
||||
esbuild:
|
||||
specifier: 0.28.0
|
||||
version: 0.28.0
|
||||
|
||||
extensions/diffs-language-pack:
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/plugin-sdk
|
||||
esbuild:
|
||||
specifier: 0.28.0
|
||||
version: 0.28.0
|
||||
|
||||
extensions/discord:
|
||||
dependencies:
|
||||
|
||||
@@ -103,6 +103,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/diffs-language-pack",
|
||||
"description": "OpenClaw diffs viewer syntax highlighting language pack",
|
||||
"source": "official",
|
||||
"kind": "plugin",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "diffs-language-pack",
|
||||
"label": "Diff Viewer Language Pack"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@openclaw/diffs-language-pack",
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.4.30"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/google-meet",
|
||||
"description": "OpenClaw Google Meet participant plugin",
|
||||
|
||||
@@ -42,6 +42,11 @@ describe("official external plugin catalog", () => {
|
||||
expect(resolveOfficialExternalPluginInstall(expectCatalogEntry("line"))?.npmSpec).toBe(
|
||||
"@openclaw/line",
|
||||
);
|
||||
expect(resolveOfficialExternalPluginInstall(expectCatalogEntry("diffs-language-pack"))).toEqual({
|
||||
npmSpec: "@openclaw/diffs-language-pack",
|
||||
defaultChoice: "npm",
|
||||
minHostVersion: ">=2026.4.30",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows invalid-config recovery for externalized stock plugins", () => {
|
||||
|
||||
@@ -123,6 +123,7 @@ describe("oxlint config", () => {
|
||||
expect(ignorePatterns).toContain("**/.cache/**");
|
||||
expect(ignorePatterns).toContain("**/.openclaw-runtime-deps-copy-*/**");
|
||||
expect(ignorePatterns).toContain("extensions/diffs/assets/viewer-runtime.js");
|
||||
expect(ignorePatterns).toContain("extensions/diffs-language-pack/assets/viewer-runtime.js");
|
||||
});
|
||||
|
||||
it("enables strict empty object type lint with named single-extends interfaces allowed", () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("runtime postbuild static assets", () => {
|
||||
"dist/extensions/acpx/error-format.mjs",
|
||||
"dist/extensions/acpx/mcp-command-line.mjs",
|
||||
"dist/extensions/acpx/mcp-proxy.mjs",
|
||||
"dist/extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
"dist/extensions/diffs/assets/viewer-runtime.js",
|
||||
]);
|
||||
});
|
||||
@@ -59,8 +60,12 @@ describe("runtime postbuild static assets", () => {
|
||||
"dist/extensions/acpx/error-format.mjs",
|
||||
"dist/extensions/acpx/mcp-command-line.mjs",
|
||||
"dist/extensions/acpx/mcp-proxy.mjs",
|
||||
"dist/extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
"dist/extensions/diffs/assets/viewer-runtime.js",
|
||||
]);
|
||||
expect(payload.sources).toContain(
|
||||
"extensions/diffs-language-pack/assets/viewer-runtime.js",
|
||||
);
|
||||
expect(payload.sources).toContain("extensions/diffs/assets/viewer-runtime.js");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user