Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ jobs:

- name: Typecheck
run: pnpm typecheck

- name: Knip
run: pnpm exec knip
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ caido-dev watch [path] [--config <path-to-config>]
- **Description**: Start the development server and watch for changes.
- **Options**:
- `-c, --config <path>`: Path to the `caido.config.ts` file.

## README Assets

Plugin packages always include the root `README.md`. Local README images are converted to compressed WebP data URIs during packaging, with each image limited to about 125 KiB and the final README limited to about 2 MiB. External `http` and `https` URLs are removed from README links and images, while `data:` URIs and fragment links are preserved.
9 changes: 9 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["src/**/*.spec.ts", "playgrounds/**/*.spec.ts"],
"project": ["src/**/*.ts", "playgrounds/**/*.ts", "*.config.ts"],
"ignore": [
"playgrounds/**/caido.config.ts",
"playgrounds/**/packages/**"
]
}
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,25 @@
"glob": "11.0.1",
"jiti": "2.4.2",
"jszip": "3.10.1",
"ws": "8.18.0",
"zod": "3.24.1",
"parse5": "^8.0.1",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"sharp": "^0.34.5",
"tsup": "8.3.5",
"unified": "^11.0.5",
"unist-util-visit": "^5.1.0",
Comment on lines 43 to +50
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark is added as a runtime dependency but isn’t imported/used anywhere in the codebase (only remark-parse and remark-stringify are). Removing the unused dependency will reduce install size and lockfile churn.

Copilot uses AI. Check for mistakes.
"vite": "6.0.7",
"tsup": "8.3.5"
"ws": "8.18.0",
"zod": "3.24.1"
},
"devDependencies": {
"@caido/eslint-config": "0.0.6",
"@types/express": "5.0.0",
"@types/mdast": "^4.0.4",
"@types/node": "22.10.2",
"@types/ws": "8.5.13",
"eslint": "9.17.0",
"tsup": "8.3.5",
"knip": "^6.7.0",
"tsx": "4.19.2",
"typescript": "5.7.2",
"vitest": "3.0.5"
Expand Down
8 changes: 8 additions & 0 deletions playgrounds/build-backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Backend Plugin Playground

This is a test playground for building backend plugins.

![Test Asset](assets/test.png)
![External Image](https://example.com/image.png)

The plugin includes a backend plugin with assets.
25 changes: 25 additions & 0 deletions playgrounds/build-backend/__tests__/build-backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,29 @@ describe("build-backend", () => {

expect(assetContent).toBeUndefined();
});

it("should have README.md file", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toContain("Backend Plugin Playground");
});

it("should inline README image links as WebP data URIs", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toContain("data:image/webp;base64,");
expect(readmeContent).not.toContain("assets/test.png");
});

it("should remove external image URLs (http, https)", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
// Verify that external URLs are removed (set to empty string)
// The README has: ![External Image](https://example.com/image.png)
// After transformation, it should be: ![](...)
expect(readmeContent).toMatch(/!\[External Image\]\(\)/);
});
});
Binary file added playgrounds/build-backend/assets/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions playgrounds/build-frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Frontend Build Playground

This directory serves as a test fixture for validating frontend plugin build behavior in `@caido-community/dev`.

## Purpose

It demonstrates a minimal frontend Caido plugin configuration and is used in integration tests to verify:

- Vite-based frontend builds
- Asset bundling
- README and referenced asset inclusion in final plugin packages

## Test Asset Reference

The following image reference is used to test automatic asset detection from README content:

![Test Asset](assets/test.png)

## Link Test Cases

Local link that should be preserved:

[Local Doc](assets/test.txt)

External link that should be removed:

[External Link](https://example.com/docs)

Fragment-only link that should be preserved:

[Jump to section](#purpose)

## Reference-Style Definition Test Cases

Reference-style image and link using definitions:

![Ref Image][ref-image]
[Ref Link][ref-link]

[ref-image]: assets/test.png
[ref-link]: https://example.com/docs

## HTML Test Cases

Raw HTML with local and external URLs:

<img src="assets/test.png" alt="HTML Local Image" />
<img src="https://example.com/image.png" alt="HTML External Image" />
<a href="assets/test.txt">HTML Local Link</a>
<a href="https://example.com/docs">HTML External Link</a>

## External URL Test Cases

The following images test external URL handling (should be removed):

![External HTTP](http://example.com/image.png)
![External HTTPS](https://example.com/image.png)
![Data URI](data:image/png;base64,abc123)
83 changes: 83 additions & 0 deletions playgrounds/build-frontend/__tests__/build-frontend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,89 @@ import { describe, expect, it } from "vitest";
import { getZipFileContent } from "../../../playgrounds/utils";

describe("build-frontend", () => {
it("should have README.md file", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toContain("Frontend Build Playground");
});

it("should inline README image links as WebP data URIs", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toContain("data:image/webp;base64,");
expect(readmeContent).not.toContain("assets/test.png");
});

it("should remove external image URLs", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
// External URLs should be removed (set to empty string)
expect(readmeContent).not.toContain("http://example.com/image.png");
expect(readmeContent).not.toContain("https://example.com/image.png");
// data: URIs should be kept as-is (not removed)
expect(readmeContent).toContain("data:image/png;base64,abc123");
// Image syntax should still exist but with empty URLs (alt text preserved)
expect(readmeContent).toContain("![External HTTP]()");
expect(readmeContent).toContain("![External HTTPS]()");
expect(readmeContent).toContain(
"![Data URI](data:image/png;base64,abc123)",
);
});

it("should preserve local markdown link URLs", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toContain("[Local Doc](assets/test.txt)");
});

it("should remove external markdown link URLs", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
// External link URL should be removed but link text preserved
expect(readmeContent).not.toContain("https://example.com/docs");
expect(readmeContent).toContain("[External Link]()");
});

it("should preserve fragment-only links", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
// Same-document anchors should be preserved as-is
expect(readmeContent).toContain("[Jump to section](#purpose)");
});

it("should inline reference-style image definition URLs", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toMatch(/\[ref-image\]:\s*data:image\/webp;base64,/);
// [ref-link]: https://example.com/docs is external and should be removed
expect(readmeContent).not.toContain("https://example.com/docs");
});

it("should inline local HTML image src attributes and preserve href attributes", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
expect(readmeContent).toMatch(/src="data:image\/webp;base64,/);
expect(readmeContent).toContain('href="assets/test.txt"');
});

it("should remove external URLs in HTML attributes", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");
const readmeContent = await getZipFileContent(zipPath, "README.md");
expect(readmeContent).toBeDefined();
// External URLs in HTML should be emptied
expect(readmeContent).not.toContain("https://example.com/image.png");
expect(readmeContent).not.toContain("https://example.com/docs");
expect(readmeContent).toContain('src=""');
expect(readmeContent).toContain('href=""');
});
it("should have manifest.json file", async () => {
const zipPath = path.resolve(__dirname, "../dist/plugin_package.zip");

Expand Down
Binary file added playgrounds/build-frontend/assets/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 16 additions & 4 deletions playgrounds/setup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { execSync } from "child_process";
import { execFileSync, execSync } from "child_process";
import path from "path";

import { afterAll, beforeAll, expect } from "vitest";

function hasPathSegment(filePath: string, segment: string) {
return filePath.split(/[\\/]+/).includes(segment);
}

beforeAll(({ file }) => {
// Get the test file path from the current test file
const testPath = file.filepath;

if (!hasPathSegment(testPath, "playgrounds")) {
return;
}

// Find the playground directory (parent of __tests__ directory)
const playgroundDir = path.dirname(path.dirname(testPath));

Expand All @@ -18,9 +26,13 @@ beforeAll(({ file }) => {

// Run pnpm build in the playground directory
console.log(`Building playground in ${playgroundDir}...`);
execSync("node ../../dist/cli.js build", {
cwd: playgroundDir,
});
execFileSync(
process.execPath,
[path.join("..", "..", "dist", "cli.js"), "build"],
{
cwd: playgroundDir,
},
);
});

afterAll(() => {
Expand Down
Loading
Loading