Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@effect/language-service": "catalog:",
"@effect/vitest": "catalog:",
"@t3tools/contracts": "workspace:*",
"@t3tools/plugin-sdk": "workspace:*",
"@t3tools/shared": "workspace:*",
"@t3tools/web": "workspace:*",
"@types/bun": "catalog:",
Expand Down
148 changes: 148 additions & 0 deletions apps/server/src/plugins/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import fs from "node:fs/promises";
import path from "node:path";

import type { DiscoveredPluginManifest, DiscoveredPluginRoot } from "./types";

const PLUGINS_ENV_VAR = "T3CODE_PLUGIN_DIRS";
const DEFAULT_LOCAL_PLUGINS_DIR = "plugins";
const PLUGIN_MANIFEST_FILE = "t3-plugin.json";

interface RawPluginManifest {
readonly id?: unknown;
readonly name?: unknown;
readonly version?: unknown;
readonly hostApiVersion?: unknown;
readonly enabled?: unknown;
readonly serverEntry?: unknown;
readonly webEntry?: unknown;
}

function trimNonEmpty(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}

async function pathExists(candidatePath: string): Promise<boolean> {
try {
await fs.access(candidatePath);
return true;
} catch {
return false;
}
}

async function isDirectory(candidatePath: string): Promise<boolean> {
try {
return (await fs.stat(candidatePath)).isDirectory();
} catch {
return false;
}
}

function normalizePluginRoots(cwd: string): string[] {
const configuredRoots = (process.env[PLUGINS_ENV_VAR] ?? "")
.split(path.delimiter)
.map((value) => value.trim())
.filter((value) => value.length > 0)
.map((value) => path.resolve(value));
const localRoot = path.resolve(cwd, DEFAULT_LOCAL_PLUGINS_DIR);
return Array.from(new Set([localRoot, ...configuredRoots]));
Comment on lines +45 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium plugins/discovery.ts:45

Line 50 resolves relative paths from the environment variable against process.cwd() rather than the cwd parameter. When a caller passes a cwd different from process.cwd(), relative paths in the environment variable resolve inconsistently with localRoot which correctly uses path.resolve(cwd, DEFAULT_LOCAL_PLUGINS_DIR). Consider changing path.resolve(value) to path.resolve(cwd, value) so all paths resolve relative to the same base.

-    .map((value) => path.resolve(value));
+    .map((value) => path.resolve(cwd, value));
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/plugins/discovery.ts around lines 45-52:

Line 50 resolves relative paths from the environment variable against `process.cwd()` rather than the `cwd` parameter. When a caller passes a `cwd` different from `process.cwd()`, relative paths in the environment variable resolve inconsistently with `localRoot` which correctly uses `path.resolve(cwd, DEFAULT_LOCAL_PLUGINS_DIR)`. Consider changing `path.resolve(value)` to `path.resolve(cwd, value)` so all paths resolve relative to the same base.

Evidence trail:
apps/server/src/plugins/discovery.ts lines 44-52 (REVIEWED_COMMIT): `normalizePluginRoots(cwd: string)` function shows line 50 `.map((value) => path.resolve(value))` resolving env var paths against process.cwd(), while line 51 `path.resolve(cwd, DEFAULT_LOCAL_PLUGINS_DIR)` resolves localRoot against the cwd parameter. apps/server/src/plugins/manager.ts line 337: `discoverPluginRoots(input.cwd)` shows the function is called with a cwd parameter that may differ from process.cwd().

}

async function discoverRootCandidates(rootPath: string): Promise<DiscoveredPluginRoot[]> {
if (!(await isDirectory(rootPath))) {
return [];
}

const directManifestPath = path.join(rootPath, PLUGIN_MANIFEST_FILE);
if (await pathExists(directManifestPath)) {
return [{ rootDir: rootPath, manifestPath: directManifestPath }];
}

const entries = await fs.readdir(rootPath, { withFileTypes: true }).catch(() => []);
const childCandidates = entries
.filter((entry) => entry.isDirectory())
.map((entry) => ({
rootDir: path.join(rootPath, entry.name),
manifestPath: path.join(rootPath, entry.name, PLUGIN_MANIFEST_FILE),
}));

const existingCandidates = await Promise.all(
childCandidates.map(async (candidate) =>
(await pathExists(candidate.manifestPath)) ? candidate : null,
),
);

return existingCandidates.filter(
(candidate): candidate is DiscoveredPluginRoot => candidate !== null,
);
}

export async function discoverPluginRoots(cwd: string): Promise<DiscoveredPluginRoot[]> {
const rootCandidates = await Promise.all(
normalizePluginRoots(cwd).map((rootPath) => discoverRootCandidates(rootPath)),
);

const flatCandidates = rootCandidates.flat();
const existingCandidates = await Promise.all(
flatCandidates.map(async (candidate) =>
(await pathExists(candidate.manifestPath)) ? candidate : null,
),
);

return existingCandidates.filter(
(candidate): candidate is DiscoveredPluginRoot => candidate !== null,
);
}

export async function loadPluginManifest(
root: DiscoveredPluginRoot,
): Promise<DiscoveredPluginManifest> {
const rawManifest = await fs
.readFile(root.manifestPath, "utf8")
.then((contents) => JSON.parse(contents) as RawPluginManifest)
.catch(() => ({}) as RawPluginManifest);

const fallbackId = path.basename(root.rootDir);
const id = trimNonEmpty(rawManifest.id) ?? fallbackId;
const name = trimNonEmpty(rawManifest.name) ?? id;
const version = trimNonEmpty(rawManifest.version) ?? "0.0.0";
const hostApiVersion = trimNonEmpty(rawManifest.hostApiVersion) ?? "unknown";
const enabled = rawManifest.enabled !== false;
const serverEntry = trimNonEmpty(rawManifest.serverEntry) ?? "dist/server.js";
const webEntry = trimNonEmpty(rawManifest.webEntry) ?? "dist/web.js";
const serverEntryPath = (await pathExists(path.resolve(root.rootDir, serverEntry)))
? path.resolve(root.rootDir, serverEntry)
: null;
const webEntryPath = (await pathExists(path.resolve(root.rootDir, webEntry)))
? path.resolve(root.rootDir, webEntry)
: null;

let error: string | null = null;
if (!trimNonEmpty(rawManifest.id)) {
error = "Plugin manifest is missing a valid 'id'.";
} else if (!trimNonEmpty(rawManifest.name)) {
error = "Plugin manifest is missing a valid 'name'.";
} else if (!trimNonEmpty(rawManifest.version)) {
error = "Plugin manifest is missing a valid 'version'.";
} else if (!trimNonEmpty(rawManifest.hostApiVersion)) {
error = "Plugin manifest is missing a valid 'hostApiVersion'.";
}

return {
id,
name,
version,
hostApiVersion,
enabled,
compatible: hostApiVersion === "1" && error === null,
rootDir: root.rootDir,
manifestPath: root.manifestPath,
serverEntryPath,
webEntryPath,
error,
};
}
Loading