@silo-code/sdk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context-keys.d.ts +18 -3
- package/dist/context-keys.d.ts.map +1 -1
- package/dist/domain-types.d.ts +8 -32
- package/dist/domain-types.d.ts.map +1 -1
- package/dist/editor-service.d.ts +135 -3
- package/dist/editor-service.d.ts.map +1 -1
- package/dist/event.d.ts +28 -0
- package/dist/event.d.ts.map +1 -0
- package/dist/event.js +2 -0
- package/dist/event.js.map +1 -0
- package/dist/extension-storage.d.ts +7 -3
- package/dist/extension-storage.d.ts.map +1 -1
- package/dist/file-service.d.ts +48 -4
- package/dist/file-service.d.ts.map +1 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/dist/layout-service.d.ts +13 -13
- package/dist/layout-service.d.ts.map +1 -1
- package/dist/network-service.d.ts +70 -4
- package/dist/network-service.d.ts.map +1 -1
- package/dist/network-service.js +35 -1
- package/dist/network-service.js.map +1 -1
- package/dist/path.d.ts +62 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +150 -0
- package/dist/path.js.map +1 -0
- package/dist/process-service.d.ts +24 -4
- package/dist/process-service.d.ts.map +1 -1
- package/dist/processes-service.d.ts +1 -1
- package/dist/search-service.d.ts +11 -0
- package/dist/search-service.d.ts.map +1 -1
- package/dist/system-service.d.ts +2 -2
- package/dist/terminal-service.d.ts +66 -0
- package/dist/terminal-service.d.ts.map +1 -1
- package/dist/theme-service.d.ts +23 -7
- package/dist/theme-service.d.ts.map +1 -1
- package/dist/types.d.ts +102 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/workspace-service.d.ts +13 -0
- package/dist/workspace-service.d.ts.map +1 -1
- package/package.json +2 -4
- package/src/context-keys.ts +18 -3
- package/src/domain-types.ts +8 -29
- package/src/editor-service.ts +141 -5
- package/src/event.ts +28 -0
- package/src/extension-storage.ts +8 -3
- package/src/file-service.ts +49 -4
- package/src/index.ts +31 -6
- package/src/layout-service.ts +15 -13
- package/src/network-service.ts +85 -4
- package/src/path.test.ts +135 -0
- package/src/path.ts +188 -0
- package/src/process-service.ts +24 -4
- package/src/processes-service.ts +1 -1
- package/src/search-service.ts +11 -0
- package/src/system-service.ts +2 -2
- package/src/terminal-service.ts +71 -0
- package/src/theme-service.ts +23 -7
- package/src/types.ts +104 -25
- package/src/workspace-service.ts +13 -0
package/src/file-service.ts
CHANGED
|
@@ -24,6 +24,16 @@ export interface FileMeta {
|
|
|
24
24
|
modifiedMs: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The normalized kind of a filesystem change, as delivered in
|
|
29
|
+
* {@link FileChangeEvent.kind}. Backend-specific event kinds that don't map to
|
|
30
|
+
* one of the three meaningful values are surfaced as `"other"`.
|
|
31
|
+
*
|
|
32
|
+
* @category Core Types
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export type FileChangeKind = "create" | "modify" | "remove" | "other";
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* A filesystem change event delivered to a {@link FileService.watch} listener,
|
|
29
39
|
* for changes under that watch's path.
|
|
@@ -34,8 +44,12 @@ export interface FileMeta {
|
|
|
34
44
|
export interface FileChangeEvent {
|
|
35
45
|
/** The paths that changed in this event. */
|
|
36
46
|
paths: string[];
|
|
37
|
-
/**
|
|
38
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Normalized change kind. The host maps the backend's vocabulary to this
|
|
49
|
+
* closed union; backend-specific kinds that don't match arrive as `"other"`.
|
|
50
|
+
* Always compare against the union values — never against raw backend strings.
|
|
51
|
+
*/
|
|
52
|
+
kind: FileChangeKind;
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
/**
|
|
@@ -68,15 +82,46 @@ export interface FileService {
|
|
|
68
82
|
readBytes(path: string): Promise<ArrayBuffer>;
|
|
69
83
|
/** List a directory's immediate entries. */
|
|
70
84
|
readDir(path: string): Promise<FileMeta[]>;
|
|
71
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Resolve true if a file or directory exists at `path`. Prefer
|
|
87
|
+
* {@link FileService.stat} when you also need the entry's metadata — `stat`
|
|
88
|
+
* returning non-`null` subsumes this check in one call.
|
|
89
|
+
*/
|
|
72
90
|
pathExists(path: string): Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Metadata for a single path, following symlinks, or `null` if nothing
|
|
93
|
+
* exists there. Resolving `null` (rather than rejecting) for an absent path
|
|
94
|
+
* is deliberate — it makes `stat` a one-call replacement for
|
|
95
|
+
* {@link FileService.pathExists} that also returns size / mtime / type.
|
|
96
|
+
* Rejects only on a real I/O error (e.g. a permission failure).
|
|
97
|
+
*/
|
|
98
|
+
stat(path: string): Promise<FileMeta | null>;
|
|
73
99
|
/** Write UTF-8 text to a file, creating or overwriting it. */
|
|
74
100
|
writeText(path: string, content: string): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Write raw bytes to a file, creating or overwriting it (and creating any
|
|
103
|
+
* missing parent directories). The byte-oriented counterpart to
|
|
104
|
+
* {@link FileService.writeText} / {@link FileService.readBytes} — use it for
|
|
105
|
+
* binary assets (images, archives) where `writeText` would corrupt the data.
|
|
106
|
+
*/
|
|
107
|
+
writeBytes(path: string, data: ArrayBuffer | Uint8Array): Promise<void>;
|
|
75
108
|
/** Create a directory (and any missing parents). */
|
|
76
109
|
createDir(path: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Copy a file or directory from `src` to `dest`, recursively for
|
|
112
|
+
* directories, creating any missing parent directories. Requires read access
|
|
113
|
+
* to `src` and write access to `dest` (both are workspace-scoped). Overwrites
|
|
114
|
+
* existing files at the destination.
|
|
115
|
+
*/
|
|
116
|
+
copy(src: string, dest: string): Promise<void>;
|
|
77
117
|
/** Rename / move a file or directory. */
|
|
78
118
|
rename(oldPath: string, newPath: string): Promise<void>;
|
|
79
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* **Permanently** delete a file or directory (directories are removed
|
|
121
|
+
* recursively). This does **not** move the entry to the OS trash/recycle
|
|
122
|
+
* bin — the delete is irreversible, so confirm destructive removals with the
|
|
123
|
+
* user first. Rejects if the path does not exist.
|
|
124
|
+
*/
|
|
80
125
|
delete(path: string): Promise<void>;
|
|
81
126
|
/** Reveal a path in the OS file manager (Finder / Explorer). */
|
|
82
127
|
reveal(path: string): Promise<void>;
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The public
|
|
3
|
-
* extension author imports from.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The `@silo-code/sdk` public surface — the single curated entry point an
|
|
3
|
+
* extension author imports from. Re-exports **only** the blessed, permanently
|
|
4
|
+
* supported types and runtime helpers. Anything not re-exported here is
|
|
5
|
+
* host-internal and may change without notice.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* **What's here:** the types-first extension contract (see `types.ts`) plus a
|
|
8
|
+
* small set of blessed runtime helpers (`Tooltip`, `useFocusGroup`,
|
|
9
|
+
* `useServiceState`, `DND_MIME`, `PathDeniedError`, `NetworkError`). The SDK
|
|
10
|
+
* peer-depends on React 19; changes to the runtime helpers can be breaking
|
|
11
|
+
* even when the types are unchanged.
|
|
12
|
+
*
|
|
13
|
+
* This is also the entry point the API-reference generator (TypeDoc) reads, so
|
|
8
14
|
* the published reference is exactly this surface — no more, no less.
|
|
9
15
|
*
|
|
10
16
|
* @packageDocumentation
|
|
@@ -66,6 +72,9 @@ export type {
|
|
|
66
72
|
DiffContent,
|
|
67
73
|
DiffContentRequest,
|
|
68
74
|
DiffContentProvider,
|
|
75
|
+
ActiveEditorInfo,
|
|
76
|
+
EditorsState,
|
|
77
|
+
EditorSaveEvent,
|
|
69
78
|
} from "./editor-service";
|
|
70
79
|
export type {
|
|
71
80
|
LayoutService,
|
|
@@ -94,7 +103,12 @@ export type {
|
|
|
94
103
|
TerminalTabDecorationProvider,
|
|
95
104
|
OscEvent,
|
|
96
105
|
} from "./terminal-service";
|
|
97
|
-
export type {
|
|
106
|
+
export type {
|
|
107
|
+
FileService,
|
|
108
|
+
FileMeta,
|
|
109
|
+
FileChangeKind,
|
|
110
|
+
FileChangeEvent,
|
|
111
|
+
} from "./file-service";
|
|
98
112
|
// Cross-file content search exposed on the ExtensionContext as `ctx.search`.
|
|
99
113
|
export type {
|
|
100
114
|
SearchService,
|
|
@@ -157,10 +171,12 @@ export type {
|
|
|
157
171
|
} from "./ui-service";
|
|
158
172
|
|
|
159
173
|
// Server-side HTTP client — bypasses CORS, readable response headers.
|
|
174
|
+
export { NetworkError } from "./network-service";
|
|
160
175
|
export type {
|
|
161
176
|
NetworkService,
|
|
162
177
|
NetworkRequestOptions,
|
|
163
178
|
NetworkResponse,
|
|
179
|
+
NetworkBytesResponse,
|
|
164
180
|
} from "./network-service";
|
|
165
181
|
|
|
166
182
|
// Static host-platform metadata: OS, CPU arch, and Silo version.
|
|
@@ -174,6 +190,15 @@ export type { LogService, LogLevel } from "./output-service";
|
|
|
174
190
|
// Context keys referenced by `when` predicates on menu items / keybindings.
|
|
175
191
|
export type { ContextKeys } from "./context-keys";
|
|
176
192
|
|
|
193
|
+
// Typed event primitive: a subscribable Event<T> that returns a Disposable.
|
|
194
|
+
// The matching host-side emitter is internal to the extension-host package.
|
|
195
|
+
export type { Event } from "./event";
|
|
196
|
+
|
|
197
|
+
// Pure path utilities — cross-platform replacement for `node:path` (banned in
|
|
198
|
+
// extensions). All outputs use forward-slash separators. Both "/" and "\" are
|
|
199
|
+
// accepted as input. Exported as a namespaced `path` object.
|
|
200
|
+
export { path } from "./path";
|
|
201
|
+
|
|
177
202
|
// Tooltip — the same styled hover popup the host uses in the status bar.
|
|
178
203
|
// Extensions use this instead of native `title` attributes to match host chrome.
|
|
179
204
|
export { Tooltip } from "./Tooltip";
|
package/src/layout-service.ts
CHANGED
|
@@ -60,21 +60,23 @@ export interface LayoutService {
|
|
|
60
60
|
* web-viewer tab). No-op when the center dock has no active workspace.
|
|
61
61
|
*
|
|
62
62
|
* @param kindId - The {@link DockPanelKind.id} to instantiate.
|
|
63
|
-
* @param params - Arbitrary params forwarded to the panel
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* @param params - Arbitrary params forwarded to the panel component.
|
|
64
|
+
* Serialized into `ws.dockLayout` so state survives workspace
|
|
65
|
+
* close/reopen.
|
|
66
|
+
* @param options - `singleton: true` opens at most one instance at a time:
|
|
67
|
+
* if a panel with `kindId` already exists, it is focused instead of
|
|
68
|
+
* creating a new one. The panel's id equals `kindId` (not UUID-based) when
|
|
69
|
+
* singleton is set.
|
|
66
70
|
*/
|
|
67
|
-
openPanel(
|
|
71
|
+
openPanel(
|
|
72
|
+
kindId: string,
|
|
73
|
+
params?: Record<string, unknown>,
|
|
74
|
+
options?: { singleton?: boolean },
|
|
75
|
+
): void;
|
|
68
76
|
/**
|
|
69
|
-
* Open a **singleton** dock panel
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* at most one can exist). Use for utility panels like Output that don't
|
|
73
|
-
* benefit from multiple instances.
|
|
74
|
-
*
|
|
75
|
-
* @param kindId - The {@link DockPanelKind.id} to open or focus.
|
|
76
|
-
* @param params - Forwarded to the panel when creating; ignored when
|
|
77
|
-
* focusing an existing instance.
|
|
77
|
+
* Open a **singleton** dock panel.
|
|
78
|
+
* @deprecated Use `openPanel(kindId, params, { singleton: true })` instead.
|
|
79
|
+
* This method will be removed in a future release.
|
|
78
80
|
*/
|
|
79
81
|
openSingletonPanel(kindId: string, params?: Record<string, unknown>): void;
|
|
80
82
|
}
|
package/src/network-service.ts
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link NetworkService.fetch} and {@link NetworkService.fetchHeaders}
|
|
3
|
+
* when a request fails (network error, DNS failure, TLS error, timeout, etc.).
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* try {
|
|
7
|
+
* const res = await ctx.net.fetch("https://api.example.com/data");
|
|
8
|
+
* } catch (err) {
|
|
9
|
+
* if (err instanceof NetworkError) {
|
|
10
|
+
* console.error(`Request to ${err.url} failed: ${err.message}`);
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @category Core Types
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export class NetworkError extends Error {
|
|
19
|
+
/** The URL that was requested when the error occurred. */
|
|
20
|
+
readonly url: string;
|
|
21
|
+
|
|
22
|
+
constructor(url: string, message: string) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "NetworkError";
|
|
25
|
+
this.url = url;
|
|
26
|
+
// Restore the prototype chain so `instanceof` works across the down-leveled
|
|
27
|
+
// class output the SDK ships (and across the host↔extension boundary, where
|
|
28
|
+
// there's a single shared SDK instance).
|
|
29
|
+
Object.setPrototypeOf(this, NetworkError.prototype);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
/**
|
|
2
34
|
* Options for {@link NetworkService.fetch} and {@link NetworkService.fetchHeaders}.
|
|
3
35
|
*
|
|
@@ -12,8 +44,12 @@ export interface NetworkRequestOptions {
|
|
|
12
44
|
method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
13
45
|
/** Request headers to send. */
|
|
14
46
|
headers?: Record<string, string>;
|
|
15
|
-
/**
|
|
16
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Request body. A string is sent as-is; an `ArrayBuffer` / `Uint8Array` is
|
|
49
|
+
* sent as raw bytes (e.g. uploading a file). Only meaningful for methods that
|
|
50
|
+
* carry a body.
|
|
51
|
+
*/
|
|
52
|
+
body?: string | ArrayBuffer | Uint8Array;
|
|
17
53
|
/** Follow HTTP redirects. Defaults to `true`. */
|
|
18
54
|
followRedirects?: boolean;
|
|
19
55
|
/** Request timeout in milliseconds. Omit for the platform default (~30 s). */
|
|
@@ -40,6 +76,24 @@ export interface NetworkResponse {
|
|
|
40
76
|
finalUrl: string;
|
|
41
77
|
}
|
|
42
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Response from {@link NetworkService.fetchBytes} — identical to
|
|
81
|
+
* {@link NetworkResponse} but with the body delivered as raw bytes.
|
|
82
|
+
*
|
|
83
|
+
* @category Core Types
|
|
84
|
+
* @public
|
|
85
|
+
*/
|
|
86
|
+
export interface NetworkBytesResponse {
|
|
87
|
+
/** HTTP status code. */
|
|
88
|
+
status: number;
|
|
89
|
+
/** Response headers, lowercased (multi-value joined with `", "`). */
|
|
90
|
+
headers: Record<string, string>;
|
|
91
|
+
/** Response body as raw bytes. */
|
|
92
|
+
body: ArrayBuffer;
|
|
93
|
+
/** Final URL after redirects. */
|
|
94
|
+
finalUrl: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
43
97
|
/**
|
|
44
98
|
* Server-side HTTP client exposed as {@link ExtensionContext.net}. Requests
|
|
45
99
|
* run in the Rust backend via `reqwest`, so they bypass the browser's CORS
|
|
@@ -62,7 +116,7 @@ export interface NetworkService {
|
|
|
62
116
|
*
|
|
63
117
|
* @param url - The URL to fetch.
|
|
64
118
|
* @param options - Method, headers, body, redirect and timeout controls.
|
|
65
|
-
* @throws
|
|
119
|
+
* @throws {@link NetworkError} if the request fails (network error, DNS
|
|
66
120
|
* failure, TLS error, etc.).
|
|
67
121
|
*
|
|
68
122
|
* @example
|
|
@@ -72,6 +126,33 @@ export interface NetworkService {
|
|
|
72
126
|
*/
|
|
73
127
|
fetch(url: string, options?: NetworkRequestOptions): Promise<NetworkResponse>;
|
|
74
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Like {@link NetworkService.fetch}, but resolves the response body as raw
|
|
131
|
+
* bytes ({@link NetworkBytesResponse}) instead of decoding it as UTF-8 text —
|
|
132
|
+
* for downloading images, archives, or any binary payload.
|
|
133
|
+
*
|
|
134
|
+
* @param url - The URL to fetch.
|
|
135
|
+
* @param options - Method, headers, body, redirect and timeout controls. The
|
|
136
|
+
* body may itself be binary (`ArrayBuffer` / `Uint8Array`).
|
|
137
|
+
* @throws {@link NetworkError} if the request fails.
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* The body rides Tauri's binary IPC channel (no base64), but the whole
|
|
141
|
+
* response is still buffered in memory on both sides — suitable for typical
|
|
142
|
+
* asset downloads (up to a few tens of MB), not for streaming multi-hundred-MB
|
|
143
|
+
* files.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* const { body } = await ctx.net.fetchBytes("https://example.com/logo.png");
|
|
148
|
+
* await ctx.files.writeBytes("logo.png", body);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
fetchBytes(
|
|
152
|
+
url: string,
|
|
153
|
+
options?: NetworkRequestOptions,
|
|
154
|
+
): Promise<NetworkBytesResponse>;
|
|
155
|
+
|
|
75
156
|
/**
|
|
76
157
|
* Send a `HEAD` request and return only the response headers — no body is
|
|
77
158
|
* downloaded. More efficient than {@link NetworkService.fetch} when you only
|
|
@@ -83,7 +164,7 @@ export interface NetworkService {
|
|
|
83
164
|
* @param url - The URL to probe.
|
|
84
165
|
* @param options - Redirect and timeout controls (`method` and `body` are
|
|
85
166
|
* ignored — this is always a HEAD request).
|
|
86
|
-
* @throws
|
|
167
|
+
* @throws {@link NetworkError} if the request fails.
|
|
87
168
|
*
|
|
88
169
|
* @example
|
|
89
170
|
* ```ts
|
package/src/path.test.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { path } from "./path";
|
|
3
|
+
|
|
4
|
+
describe("path.normalize", () => {
|
|
5
|
+
it("collapses dot segments", () =>
|
|
6
|
+
expect(path.normalize("a/./b")).toBe("a/b"));
|
|
7
|
+
it("resolves dot-dot segments", () =>
|
|
8
|
+
expect(path.normalize("a/b/../c")).toBe("a/c"));
|
|
9
|
+
it("converts backslash to forward-slash", () =>
|
|
10
|
+
expect(path.normalize("a\\b")).toBe("a/b"));
|
|
11
|
+
it("collapses duplicate slashes", () =>
|
|
12
|
+
expect(path.normalize("a//b")).toBe("a/b"));
|
|
13
|
+
it("preserves POSIX absolute", () =>
|
|
14
|
+
expect(path.normalize("/a/b")).toBe("/a/b"));
|
|
15
|
+
it("handles Windows drive", () =>
|
|
16
|
+
expect(path.normalize("C:\\a\\b")).toBe("C:/a/b"));
|
|
17
|
+
it("handles Windows drive with forward-slash", () =>
|
|
18
|
+
expect(path.normalize("C:/a/b")).toBe("C:/a/b"));
|
|
19
|
+
it("stays at root when dot-dot exceeds root", () =>
|
|
20
|
+
expect(path.normalize("/a/../..")).toBe("/"));
|
|
21
|
+
it("returns dot for empty-ish relative path", () =>
|
|
22
|
+
expect(path.normalize("a/..")).toBe("."));
|
|
23
|
+
it("UNC path is preserved", () =>
|
|
24
|
+
expect(path.normalize("\\\\server\\share\\foo")).toBe(
|
|
25
|
+
"//server/share/foo",
|
|
26
|
+
));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("path.join", () => {
|
|
30
|
+
it("joins two segments", () => expect(path.join("a", "b")).toBe("a/b"));
|
|
31
|
+
it("joins many segments", () =>
|
|
32
|
+
expect(path.join("a", "b", "c")).toBe("a/b/c"));
|
|
33
|
+
it("resolves dot-dot across join", () =>
|
|
34
|
+
expect(path.join("a/b", "../c")).toBe("a/c"));
|
|
35
|
+
it("preserves leading slash from first segment", () =>
|
|
36
|
+
expect(path.join("/a", "b")).toBe("/a/b"));
|
|
37
|
+
it("converts backslash", () => expect(path.join("a\\b", "c")).toBe("a/b/c"));
|
|
38
|
+
it("returns dot for no arguments", () => expect(path.join()).toBe("."));
|
|
39
|
+
it("ignores empty string segments", () =>
|
|
40
|
+
expect(path.join("a", "", "b")).toBe("a/b"));
|
|
41
|
+
it("handles Windows drive", () =>
|
|
42
|
+
expect(path.join("C:\\a", "b")).toBe("C:/a/b"));
|
|
43
|
+
it('join("/", rel) produces "/rel" not "//rel" (UNC)', () =>
|
|
44
|
+
expect(path.join("/", "linked.md")).toBe("/linked.md"));
|
|
45
|
+
it("join of POSIX root with nested rel stays absolute", () =>
|
|
46
|
+
expect(path.join("/", "a/b/../c")).toBe("/a/c"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("path.dirname", () => {
|
|
50
|
+
it("returns parent dir", () => expect(path.dirname("/a/b/c")).toBe("/a/b"));
|
|
51
|
+
it("returns slash for top-level file", () =>
|
|
52
|
+
expect(path.dirname("/a")).toBe("/"));
|
|
53
|
+
it("returns dot for bare filename", () =>
|
|
54
|
+
expect(path.dirname("a")).toBe("."));
|
|
55
|
+
it("handles Windows drive", () =>
|
|
56
|
+
expect(path.dirname("C:\\a\\b")).toBe("C:/a"));
|
|
57
|
+
it("returns drive root for top-level", () =>
|
|
58
|
+
expect(path.dirname("C:\\a")).toBe("C:/"));
|
|
59
|
+
it("resolves trailing slash before computing", () =>
|
|
60
|
+
expect(path.dirname("/a/b/")).toBe("/a"));
|
|
61
|
+
it("handles UNC path", () =>
|
|
62
|
+
expect(path.dirname("//server/share/foo")).toBe("//server/share"));
|
|
63
|
+
it("returns dot for relative bare name", () =>
|
|
64
|
+
expect(path.dirname("file.ts")).toBe("."));
|
|
65
|
+
it("returns relative parent", () => expect(path.dirname("a/b")).toBe("a"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("path.basename", () => {
|
|
69
|
+
it("returns last segment", () =>
|
|
70
|
+
expect(path.basename("/a/b/file.ts")).toBe("file.ts"));
|
|
71
|
+
it("strips provided extension", () =>
|
|
72
|
+
expect(path.basename("file.ts", ".ts")).toBe("file"));
|
|
73
|
+
it("does not strip if extension doesn't match", () =>
|
|
74
|
+
expect(path.basename("file.ts", ".js")).toBe("file.ts"));
|
|
75
|
+
it("handles trailing slash", () => expect(path.basename("/a/b/")).toBe("b"));
|
|
76
|
+
it("handles Windows path", () =>
|
|
77
|
+
expect(path.basename("C:\\a\\b.txt")).toBe("b.txt"));
|
|
78
|
+
it("returns empty string for root", () =>
|
|
79
|
+
expect(path.basename("/")).toBe(""));
|
|
80
|
+
it("returns bare name", () =>
|
|
81
|
+
expect(path.basename("file.ts")).toBe("file.ts"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("path.extname", () => {
|
|
85
|
+
it("returns extension with dot", () =>
|
|
86
|
+
expect(path.extname("file.ts")).toBe(".ts"));
|
|
87
|
+
it("returns last extension only", () =>
|
|
88
|
+
expect(path.extname("file.test.ts")).toBe(".ts"));
|
|
89
|
+
it("returns empty for no extension", () =>
|
|
90
|
+
expect(path.extname("file")).toBe(""));
|
|
91
|
+
it("returns empty for dotfile (dot at position 0)", () =>
|
|
92
|
+
expect(path.extname(".gitignore")).toBe(""));
|
|
93
|
+
it("returns dot for trailing dot", () =>
|
|
94
|
+
expect(path.extname("file.")).toBe("."));
|
|
95
|
+
it("handles full path", () =>
|
|
96
|
+
expect(path.extname("/a/b/file.md")).toBe(".md"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("path.isAbsolute", () => {
|
|
100
|
+
it("true for POSIX absolute", () =>
|
|
101
|
+
expect(path.isAbsolute("/foo")).toBe(true));
|
|
102
|
+
it("true for Windows drive with backslash", () =>
|
|
103
|
+
expect(path.isAbsolute("C:\\foo")).toBe(true));
|
|
104
|
+
it("true for Windows drive with forward-slash", () =>
|
|
105
|
+
expect(path.isAbsolute("C:/foo")).toBe(true));
|
|
106
|
+
it("true for UNC backslash", () =>
|
|
107
|
+
expect(path.isAbsolute("\\\\server\\share")).toBe(true));
|
|
108
|
+
it("true for UNC forward-slash", () =>
|
|
109
|
+
expect(path.isAbsolute("//server/share")).toBe(true));
|
|
110
|
+
it("false for relative path", () =>
|
|
111
|
+
expect(path.isAbsolute("foo/bar")).toBe(false));
|
|
112
|
+
it("false for drive-relative (no slash)", () =>
|
|
113
|
+
expect(path.isAbsolute("C:foo")).toBe(false));
|
|
114
|
+
it("false for bare filename", () =>
|
|
115
|
+
expect(path.isAbsolute("file.ts")).toBe(false));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("path.relative", () => {
|
|
119
|
+
it("computes sibling relative path", () =>
|
|
120
|
+
expect(path.relative("/a/b", "/a/c")).toBe("../c"));
|
|
121
|
+
it("returns dot for same path", () =>
|
|
122
|
+
expect(path.relative("/a/b", "/a/b")).toBe("."));
|
|
123
|
+
it("returns descendant path", () =>
|
|
124
|
+
expect(path.relative("/a", "/a/b/c")).toBe("b/c"));
|
|
125
|
+
it("returns parent path", () =>
|
|
126
|
+
expect(path.relative("/a/b/c", "/a")).toBe("../.."));
|
|
127
|
+
it("handles root to subpath", () =>
|
|
128
|
+
expect(path.relative("/", "/a/b")).toBe("a/b"));
|
|
129
|
+
it("returns normalized `to` for different Windows drives", () =>
|
|
130
|
+
expect(path.relative("C:/a", "D:/b")).toBe("D:/b"));
|
|
131
|
+
it("same drive computes relative", () =>
|
|
132
|
+
expect(path.relative("C:/a/b", "C:/a/c")).toBe("../c"));
|
|
133
|
+
it("handles backslash inputs", () =>
|
|
134
|
+
expect(path.relative("C:\\a\\b", "C:\\a\\c")).toBe("../c"));
|
|
135
|
+
});
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Pure path utilities for extensions. A cross-platform replacement for
|
|
2
|
+
// `node:path`, which extensions are banned from importing (platform ban).
|
|
3
|
+
// All outputs use forward-slash separators — the form FileService accepts on
|
|
4
|
+
// every platform. Both "/" and "\" are accepted as inputs.
|
|
5
|
+
|
|
6
|
+
/** Normalize all separators to forward-slash. */
|
|
7
|
+
function normSep(p: string): string {
|
|
8
|
+
return p.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Extract a Windows drive-letter prefix ("C:") from a forward-slash path, or null. */
|
|
12
|
+
function parseDrive(p: string): string | null {
|
|
13
|
+
const m = /^([A-Za-z]):/.exec(p);
|
|
14
|
+
return m ? m[1].toUpperCase() + ":" : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize segments of a forward-slash path: collapse duplicate slashes,
|
|
19
|
+
* resolve "." and ".." segments. Preserves leading "/" (POSIX absolute),
|
|
20
|
+
* "C:/" (Windows drive-letter), and "//" (UNC paths).
|
|
21
|
+
*/
|
|
22
|
+
function normSegments(s: string): string {
|
|
23
|
+
const drive = parseDrive(s);
|
|
24
|
+
const afterDrive = drive ? s.slice(2) : s;
|
|
25
|
+
const isUnc = afterDrive.startsWith("//");
|
|
26
|
+
const isAbs = isUnc || afterDrive.startsWith("/");
|
|
27
|
+
|
|
28
|
+
const stack: string[] = [];
|
|
29
|
+
for (const seg of afterDrive.split("/")) {
|
|
30
|
+
if (seg === "" || seg === ".") continue;
|
|
31
|
+
if (seg === "..") {
|
|
32
|
+
if (stack.length > 0 && stack[stack.length - 1] !== "..") {
|
|
33
|
+
stack.pop();
|
|
34
|
+
} else if (!isAbs) {
|
|
35
|
+
stack.push("..");
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
stack.push(seg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let result = stack.join("/");
|
|
43
|
+
if (isUnc) result = "//" + result;
|
|
44
|
+
else if (isAbs) result = "/" + result;
|
|
45
|
+
if (drive) result = drive + result;
|
|
46
|
+
if (!result) return isAbs ? (drive ? drive + "/" : "/") : ".";
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Path utilities for extensions — a cross-platform replacement for
|
|
52
|
+
* `node:path`, which extensions are banned from importing. All output paths
|
|
53
|
+
* use forward-slash separators (the form {@link FileService} accepts on every
|
|
54
|
+
* platform). Both `/` and `\` are accepted as input separators.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { path } from "@silo-code/sdk";
|
|
59
|
+
*
|
|
60
|
+
* const dir = path.dirname(filePath); // "/home/user/docs"
|
|
61
|
+
* const full = path.join(dir, "images/fig.png"); // "/home/user/docs/images/fig.png"
|
|
62
|
+
* const rel = path.relative(dir, full); // "images/fig.png"
|
|
63
|
+
* const ext = path.extname(full); // ".png"
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @category Core Types
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
export const path: {
|
|
70
|
+
/**
|
|
71
|
+
* Join path segments and normalize the result. Empty segments are ignored;
|
|
72
|
+
* `\` separators in any segment are treated as `/`.
|
|
73
|
+
*/
|
|
74
|
+
join(...parts: string[]): string;
|
|
75
|
+
/**
|
|
76
|
+
* Return the directory portion of a path — everything up to (not including)
|
|
77
|
+
* the last `/`. Returns `"."` for a bare filename with no directory component.
|
|
78
|
+
*/
|
|
79
|
+
dirname(p: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Return the final component of a path. If `ext` is supplied and the
|
|
82
|
+
* basename ends with that string, it is stripped from the result.
|
|
83
|
+
*/
|
|
84
|
+
basename(p: string, ext?: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Return the extension of a path — the portion from the last `.` of the
|
|
87
|
+
* basename, including the dot. Returns `""` for paths with no extension and
|
|
88
|
+
* for dotfiles with no secondary extension (e.g. `".gitignore"` → `""`).
|
|
89
|
+
*/
|
|
90
|
+
extname(p: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Compute the relative path from `from` to `to`. Both should be absolute
|
|
93
|
+
* paths on the same drive. When they are on different Windows drive letters,
|
|
94
|
+
* `to` (normalized) is returned unchanged — no relative path exists between
|
|
95
|
+
* drives.
|
|
96
|
+
*/
|
|
97
|
+
relative(from: string, to: string): string;
|
|
98
|
+
/**
|
|
99
|
+
* Return `true` if `p` is an absolute path: starts with `/` (POSIX), has a
|
|
100
|
+
* drive letter followed by a slash (`C:/`, `C:\`), or is a UNC path
|
|
101
|
+
* (`\\server\share` / `//server/share`). Note: `C:foo` (drive-relative,
|
|
102
|
+
* no slash) is NOT absolute.
|
|
103
|
+
*/
|
|
104
|
+
isAbsolute(p: string): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Normalize a path: convert `\` to `/`, collapse duplicate slashes, and
|
|
107
|
+
* resolve `.` and `..` segments.
|
|
108
|
+
*/
|
|
109
|
+
normalize(p: string): string;
|
|
110
|
+
} = {
|
|
111
|
+
normalize(p) {
|
|
112
|
+
return normSegments(normSep(p));
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
join(...parts) {
|
|
116
|
+
const nonEmpty = parts.filter((p) => p.length > 0);
|
|
117
|
+
if (nonEmpty.length === 0) return ".";
|
|
118
|
+
// Concatenate without adding "/" when adjacent parts already supply the
|
|
119
|
+
// boundary, so join("/", "rel") → "/rel" rather than "//rel" (UNC).
|
|
120
|
+
const raw = nonEmpty.map(normSep).reduce((a, b) => {
|
|
121
|
+
if (a.endsWith("/") || b.startsWith("/")) return a + b;
|
|
122
|
+
return a + "/" + b;
|
|
123
|
+
});
|
|
124
|
+
return normSegments(raw);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
dirname(p) {
|
|
128
|
+
const n = normSegments(normSep(p));
|
|
129
|
+
const drive = parseDrive(n);
|
|
130
|
+
const rest = drive ? n.slice(2) : n;
|
|
131
|
+
const i = rest.lastIndexOf("/");
|
|
132
|
+
if (i < 0) return drive ? drive + "." : ".";
|
|
133
|
+
if (i === 0) return drive ? drive + "/" : "/";
|
|
134
|
+
return drive ? drive + rest.slice(0, i) : rest.slice(0, i);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
basename(p, ext) {
|
|
138
|
+
const n = normSep(p);
|
|
139
|
+
const segs = n.split("/");
|
|
140
|
+
let name = segs[segs.length - 1] ?? "";
|
|
141
|
+
// Trailing slash: "foo/" → last segment is "" → fall back to prior segment
|
|
142
|
+
if (name === "" && segs.length > 1) name = segs[segs.length - 2] ?? "";
|
|
143
|
+
if (ext !== undefined && name.endsWith(ext)) {
|
|
144
|
+
name = name.slice(0, name.length - ext.length);
|
|
145
|
+
}
|
|
146
|
+
return name;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
extname(p) {
|
|
150
|
+
const base = path.basename(normSep(p));
|
|
151
|
+
const i = base.lastIndexOf(".");
|
|
152
|
+
// i === 0 means a dotfile like ".gitignore" — no extension
|
|
153
|
+
if (i <= 0) return "";
|
|
154
|
+
return base.slice(i);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
isAbsolute(p) {
|
|
158
|
+
const n = normSep(p);
|
|
159
|
+
if (n.startsWith("//")) return true; // UNC (\\server\share or //server/share)
|
|
160
|
+
if (/^[A-Za-z]:\//.test(n)) return true; // Windows drive with slash (C:/ or C:\)
|
|
161
|
+
return n.startsWith("/"); // POSIX
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
relative(from, to) {
|
|
165
|
+
const nFrom = normSegments(normSep(from));
|
|
166
|
+
const nTo = normSegments(normSep(to));
|
|
167
|
+
const driveFrom = parseDrive(nFrom);
|
|
168
|
+
const driveTo = parseDrive(nTo);
|
|
169
|
+
// Can't express a relative path across different drives
|
|
170
|
+
if (driveFrom !== driveTo) return nTo;
|
|
171
|
+
const segsFrom = (driveFrom ? nFrom.slice(2) : nFrom)
|
|
172
|
+
.split("/")
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
const segsTo = (driveTo ? nTo.slice(2) : nTo).split("/").filter(Boolean);
|
|
175
|
+
let common = 0;
|
|
176
|
+
while (
|
|
177
|
+
common < segsFrom.length &&
|
|
178
|
+
common < segsTo.length &&
|
|
179
|
+
segsFrom[common] === segsTo[common]
|
|
180
|
+
) {
|
|
181
|
+
common++;
|
|
182
|
+
}
|
|
183
|
+
const ups = segsFrom.length - common;
|
|
184
|
+
const downs = segsTo.slice(common);
|
|
185
|
+
const parts = [...Array<string>(ups).fill(".."), ...downs];
|
|
186
|
+
return parts.join("/") || ".";
|
|
187
|
+
},
|
|
188
|
+
};
|