@devframes/service-open 0.9.7 → 0.9.9
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/index.d.mts +4 -4
- package/dist/index.mjs +38 -12
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -5,21 +5,21 @@ declare const OPEN_SERVICE_PACKAGE = "@devframes/service-open";
|
|
|
5
5
|
declare const OPEN_SERVICE_SCOPE = "devframes:service:open";
|
|
6
6
|
interface OpenServiceOptions {
|
|
7
7
|
/**
|
|
8
|
-
* Preferred editor command
|
|
8
|
+
* Preferred editor command, one of the `KNOWN_EDITORS` `launch-editor`
|
|
9
9
|
* recognizes. Auto-detected (via `LAUNCH_EDITOR` and common defaults)
|
|
10
10
|
* when omitted. On merge, the later installer's choice wins.
|
|
11
11
|
*/
|
|
12
12
|
editor?: KnownEditor;
|
|
13
13
|
/**
|
|
14
14
|
* Additional directories files may be opened from, on top of the
|
|
15
|
-
* context's `workspaceRoot
|
|
15
|
+
* context's `workspaceRoot`, e.g. a plugin's managed storage dir that
|
|
16
16
|
* lives outside the workspace. Merged as a union across installers.
|
|
17
17
|
*/
|
|
18
18
|
roots?: string[];
|
|
19
19
|
}
|
|
20
20
|
interface OpenInEditorInput {
|
|
21
21
|
/**
|
|
22
|
-
* File to open
|
|
22
|
+
* File to open, absolute or relative to the service's `workspaceRoot`
|
|
23
23
|
* (so a client with only a workspace-relative path, e.g. a message's file
|
|
24
24
|
* position, can call this directly without a server-side bridge).
|
|
25
25
|
*/
|
|
@@ -52,7 +52,7 @@ declare module 'devframe' {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* The open wire service
|
|
55
|
+
* The open wire service: `open-in-editor` / `open-in-finder` RPC shared by
|
|
56
56
|
* every plugin on the host, replacing per-plugin registrations of the
|
|
57
57
|
* (deprecated) `devframe/recipes/common-rpc-functions` recipes. Paths may be
|
|
58
58
|
* absolute or relative to the `workspaceRoot`; the service refuses paths
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
1
2
|
import { defineRpcFunction } from "devframe";
|
|
2
3
|
import { KNOWN_EDITORS } from "devframe/recipes/common-rpc-functions";
|
|
3
4
|
import { s } from "devframe/utils/simple-schema";
|
|
4
|
-
import { isAbsolute, relative, resolve } from "pathe";
|
|
5
|
+
import { dirname, isAbsolute, normalize, relative, resolve } from "pathe";
|
|
5
6
|
import { defineDiagnostics } from "devframe/utils/nostics";
|
|
6
7
|
//#region package.json
|
|
7
|
-
var version = "0.9.
|
|
8
|
+
var version = "0.9.9";
|
|
8
9
|
//#endregion
|
|
9
10
|
//#region src/diagnostics.ts
|
|
11
|
+
/**
|
|
12
|
+
* Uses the service's own `DS_OPEN_` prefix per the built-in convention,
|
|
13
|
+
* keeping it collision-free with devframe core (`DF00xx`), the hub
|
|
14
|
+
* (`DF8xxx`), and the plugins (`DP_<SLUG>_`).
|
|
15
|
+
*/
|
|
10
16
|
const diagnostics = defineDiagnostics({
|
|
11
17
|
docsBase: "https://devfra.me/errors",
|
|
12
18
|
codes: { DS_OPEN_0002: {
|
|
@@ -16,10 +22,21 @@ const diagnostics = defineDiagnostics({
|
|
|
16
22
|
});
|
|
17
23
|
//#endregion
|
|
18
24
|
//#region src/index.ts
|
|
25
|
+
/** Canonical path of the nearest existing ancestor of `absolute`. */
|
|
26
|
+
async function nearestExistingCanonical(absolute) {
|
|
27
|
+
let current = absolute;
|
|
28
|
+
for (;;) try {
|
|
29
|
+
return normalize(await realpath(current));
|
|
30
|
+
} catch {
|
|
31
|
+
const parent = dirname(current);
|
|
32
|
+
if (parent === current) return current;
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
19
36
|
const OPEN_SERVICE_PACKAGE = "@devframes/service-open";
|
|
20
37
|
const OPEN_SERVICE_SCOPE = "devframes:service:open";
|
|
21
38
|
/**
|
|
22
|
-
* The open wire service
|
|
39
|
+
* The open wire service: `open-in-editor` / `open-in-finder` RPC shared by
|
|
23
40
|
* every plugin on the host, replacing per-plugin registrations of the
|
|
24
41
|
* (deprecated) `devframe/recipes/common-rpc-functions` recipes. Paths may be
|
|
25
42
|
* absolute or relative to the `workspaceRoot`; the service refuses paths
|
|
@@ -34,29 +51,38 @@ function createOpenService(options) {
|
|
|
34
51
|
version,
|
|
35
52
|
scope: OPEN_SERVICE_SCOPE,
|
|
36
53
|
options,
|
|
54
|
+
/**
|
|
55
|
+
* Option sets from multiple installers merge via devframe's default
|
|
56
|
+
* deep-merge: `roots` union, `editor` last-wins.
|
|
57
|
+
*/
|
|
37
58
|
setup(ctx, { options }) {
|
|
38
|
-
const allowedRoots = [ctx.workspaceRoot, ...options?.roots ?? []].map((
|
|
59
|
+
const allowedRoots = [ctx.workspaceRoot, ...options?.roots ?? []].map((r) => resolve(r));
|
|
60
|
+
const canonicalRoots = Promise.all(allowedRoots.map((r) => nearestExistingCanonical(r)));
|
|
61
|
+
const within = (roots, p) => roots.some((root) => {
|
|
62
|
+
const rel = relative(root, p);
|
|
63
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
64
|
+
});
|
|
39
65
|
/**
|
|
40
66
|
* Resolve `path` (relative paths against `workspaceRoot`) and assert it
|
|
41
|
-
* lands inside one of the allowed roots, or throw.
|
|
67
|
+
* lands inside one of the allowed roots, or throw. The lexical pass
|
|
68
|
+
* rejects plain `..`/absolute escapes; the canonical pass rejects a
|
|
69
|
+
* symlink that would redirect the open outside every root, while still
|
|
70
|
+
* allowing not-yet-existing files under a root.
|
|
42
71
|
*/
|
|
43
|
-
function assertAllowedPath(path) {
|
|
72
|
+
async function assertAllowedPath(path) {
|
|
44
73
|
const resolved = isAbsolute(path) ? resolve(path) : resolve(ctx.workspaceRoot, path);
|
|
45
|
-
if (!allowedRoots
|
|
46
|
-
const rel = relative(root, resolved);
|
|
47
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
48
|
-
})) throw diagnostics.DS_OPEN_0002({ path });
|
|
74
|
+
if (!within(allowedRoots, resolved) || !within(await canonicalRoots, await nearestExistingCanonical(resolved))) throw diagnostics.DS_OPEN_0002({ path });
|
|
49
75
|
return resolved;
|
|
50
76
|
}
|
|
51
77
|
const api = {
|
|
52
78
|
async openInEditor(input) {
|
|
53
|
-
const path = assertAllowedPath(input.path);
|
|
79
|
+
const path = await assertAllowedPath(input.path);
|
|
54
80
|
const target = input.line != null ? `${path}:${input.line}${input.column != null ? `:${input.column}` : ""}` : path;
|
|
55
81
|
const { launchEditor } = await import("devframe/utils/launch-editor");
|
|
56
82
|
launchEditor(target, input.editor ?? options?.editor);
|
|
57
83
|
},
|
|
58
84
|
async openInFinder(input) {
|
|
59
|
-
const path = assertAllowedPath(input.path);
|
|
85
|
+
const path = await assertAllowedPath(input.path);
|
|
60
86
|
const { open } = await import("devframe/utils/open");
|
|
61
87
|
await open(path);
|
|
62
88
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devframes/service-open",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.9",
|
|
5
5
|
"description": "Devframe wire service that opens files in the user's editor or reveals them in the OS file explorer.",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"devframe": "0.9.
|
|
31
|
+
"devframe": "0.9.9"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"pathe": "^2.0.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^26.
|
|
37
|
+
"@types/node": "^26.4.0",
|
|
38
|
+
"devframe": "0.9.9",
|
|
38
39
|
"tsdown": "^0.22.14",
|
|
39
|
-
"vitest": "^4.1.
|
|
40
|
-
"devframe": "0.9.7"
|
|
40
|
+
"vitest": "^4.1.11"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsdown",
|