Files
paseo/packages/protocol/src/plugin-source-reference.ts
T
Mohamed Boudra 047f40e623 Install npm plugins and review source updates (#4975)
* feat(plugins): unify source installation and simplify plugin settings

* feat(plugins): install npm sources and review CLI updates

Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm.

* fix(plugins): preserve CLI diagnostics and exercise reviewed updates

Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes.

* test(plugins): remove unused pending helper

* test(plugins): pack registry fixtures with the Windows command runner

* test(plugins): assert failed client contribution setup

* docs(plugins): consolidate guides and document npm publishing

* docs(plugins): introduce publishing choices before npm instructions

* test(plugins): own browser journey cleanup in a fixture

* feat(plugins): prepare scaffolded packages for publishing
2026-09-17 10:22:20 +02:00

70 lines
2.8 KiB
TypeScript

import type { PluginInstallation } from "./messages.js";
export interface PluginSourceReference {
source: string;
pluginPath: string | undefined;
}
export function parsePluginSourceReference(reference: string): PluginSourceReference {
const prefix = /^(npm:|github:|git:(?!\/\/))/.exec(reference)?.[0];
const source = prefix ? reference.slice(prefix.length) : reference;
const parsed = splitPluginPath(source, { scp: prefix !== "npm:" });
return { ...parsed, source: `${prefix ?? ""}${parsed.source}` };
}
function splitPluginPath(reference: string, options: { scp: boolean }): PluginSourceReference {
const separator = reference.lastIndexOf(":");
if (separator === -1) return { source: reference, pluginPath: undefined };
const pluginPath = reference.slice(separator + 1);
if (!isPortableRelativePluginPath(pluginPath)) {
return { source: reference, pluginPath: undefined };
}
const scheme = reference.indexOf("://");
if (scheme !== -1) {
const pathStart = reference.indexOf("/", scheme + 3);
if (pathStart === -1 || separator < pathStart) {
return { source: reference, pluginPath: undefined };
}
} else if (options.scp) {
const scpSeparator = reference.match(/^[^/@\s]+@[^:\s]+:/)?.[0].length;
if (scpSeparator !== undefined && separator === scpSeparator - 1) {
return { source: reference, pluginPath: undefined };
}
}
return { source: reference.slice(0, separator), pluginPath };
}
export function formatPluginSourceReference(
source: string,
pluginPath: string | undefined,
): string {
if (pluginPath === undefined) return source;
if (!isPortableRelativePluginPath(pluginPath)) {
throw new Error("Plugin path must be a portable path relative to the repository");
}
return `${source}:${pluginPath}`;
}
function isPortableRelativePluginPath(pluginPath: string): boolean {
if (pluginPath === ".") return true;
if (!pluginPath || pluginPath.startsWith("/") || pluginPath.startsWith("\\")) return false;
if (/^[A-Za-z]:/.test(pluginPath) || pluginPath.includes(":")) return false;
return pluginPath.split(/[\\/]/).every((part) => part !== "" && part !== "." && part !== "..");
}
export function formatPluginIdentity(identity: PluginInstallation["identity"]): string {
if (identity.kind === "directory") return identity.path;
const source = identity.kind === "npm" ? `npm:${identity.packageName}` : `git:${identity.remote}`;
return identity.pluginPath === "." ? source : `${source}:${identity.pluginPath}`;
}
export function formatPluginInstallation(installation: PluginInstallation): string {
const identity = formatPluginIdentity(installation.identity);
const revision =
installation.identity.kind === "git"
? installation.currentRevision?.slice(0, 12)
: installation.currentRevision;
return revision ? `${identity} · ${revision}` : identity;
}