diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index ebeb1fae370..2cbccefe4c1 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -1220,6 +1220,16 @@ declare module '@podman-desktop/api' { */ static file(path: string): Uri; + /** + * Create a new uri which path is the result of joining + * the path of the base uri with the provided path segments. + * + * @param base An uri. Must have a path. + * @param pathSegments One more more path fragments + * @returns A new uri which path is joined with the given fragments + */ + static joinPath(base: Uri, ...pathSegments: string[]): Uri; + /** * Use the `file` and `parse` factory functions to create new `Uri` objects. */ @@ -1257,6 +1267,43 @@ declare module '@podman-desktop/api' { */ readonly fragment: string; + /** + * Derive a new Uri from this Uri. + * + * ```ts + * const foo = Uri.parse('http://foo'); + * const httpsFoo = foo.with({ scheme: 'https' }); + * // httpsFoo is now 'https://foo' + * ``` + * + * @param change An object that describes a change to this Uri. To unset components use `undefined` or + * the empty string. + * @returns A new Uri that reflects the given change. Will return `this` Uri if the change + * is not changing anything. + */ + with(change: { + /** + * The new scheme, defaults to this Uri's scheme. + */ + scheme?: string; + /** + * The new authority, defaults to this Uri's authority. + */ + authority?: string; + /** + * The new path, defaults to this Uri's path. + */ + path?: string; + /** + * The new query, defaults to this Uri's query. + */ + query?: string; + /** + * The new fragment, defaults to this Uri's fragment. + */ + fragment?: string; + }): Uri; + toString(): string; } diff --git a/packages/main/src/plugin/types/uri.spec.ts b/packages/main/src/plugin/types/uri.spec.ts index 611d6e19401..61c31592285 100644 --- a/packages/main/src/plugin/types/uri.spec.ts +++ b/packages/main/src/plugin/types/uri.spec.ts @@ -49,3 +49,49 @@ test('toString', () => { const uri = Uri.parse('https://podman-desktop.io'); expect(uri.toString()).toBe('https://podman-desktop.io/'); }); + +test('joinPath without path', () => { + const uri = Uri.parse('https://podman-desktop.io'); + + // delete path for the error + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (uri as any)._path; + + expect(() => Uri.joinPath(uri, 'foo')).toThrowError('cannot call joinPath on Uri without a path'); +}); + +test.each([ + ['file:///foo/', '../../bar', 'file:///bar'], + ['file:///foo', '../../bar', 'file:///bar'], + ['file:///foo/bar', './baz', 'file:///foo/bar/baz'], + ['http://foo', 'bar', 'http://foo/bar'], + ['https://foo', 'bar', 'https://foo/bar'], +])('joinPath %s %s', (left, right, expected) => { + const leftUri = Uri.parse(left); + const joinPathUri = Uri.joinPath(leftUri, right); + expect(joinPathUri.toString()).toBe(expected); +}); + +test('Uri.with without any change should return same object', () => { + const uri = Uri.parse('https://podman-desktop.io'); + + const updatedUri = uri.with(); + + expect(updatedUri).toBe(uri); +}); + +test('Uri.with and undefined path', () => { + const uri = Uri.parse('https://podman-desktop.io'); + + const updatedUri = uri.with({ scheme: 'http' }); + + expect(updatedUri.scheme).toBe('http'); +}); + +test('Uri.with and same change', () => { + const uri = Uri.parse('https://podman-desktop.io'); + + const updatedUri = uri.with({ scheme: 'https', authority: 'podman-desktop.io', path: '/', query: '', fragment: '' }); + + expect(updatedUri).toBe(uri); +}); diff --git a/packages/main/src/plugin/types/uri.ts b/packages/main/src/plugin/types/uri.ts index 6c5c0b13fa8..e95f2974ab6 100644 --- a/packages/main/src/plugin/types/uri.ts +++ b/packages/main/src/plugin/types/uri.ts @@ -16,6 +16,9 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ +import path, { join } from 'path'; +import { isWindows } from '/@/util.js'; + /** * Represents a resource that can be manipulated. The resource is identified by a Uri. */ @@ -42,6 +45,64 @@ export class Uri { static file(path: string): Uri { return new Uri('file', '', path, '', ''); } + + /** + * Join a URI path with path fragments and normalizes the resulting path. + * + * @param uri The input URI. + * @param pathFragment The path fragment to add to the URI path. + * @returns The resulting URI. + */ + static joinPath(uri: Uri, ...pathFragment: string[]): Uri { + if (!uri.path) { + throw new Error('cannot call joinPath on Uri without a path'); + } + let newPath: string = join(uri.path, ...pathFragment); + if (isWindows()) { + // normalize windows path + newPath = newPath.split(path.sep).join(path.posix.sep); + } + return uri.with({ path: newPath }); + } + + with(change?: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri { + if (!change) { + return this; + } + + let { scheme, authority, path, query, fragment } = change; + if (scheme === undefined) { + scheme = this._scheme; + } + + if (authority === undefined) { + authority = this._authority; + } + + if (path === undefined) { + path = this._path; + } + if (query === undefined) { + query = this._query; + } + + if (fragment === undefined) { + fragment = this._fragment; + } + + if ( + scheme === this.scheme && + authority === this.authority && + path === this.path && + query === this.query && + fragment === this.fragment + ) { + return this; + } + + return new Uri(scheme, authority, path, query, fragment); + } + get fsPath(): string { return this._path; }