History

effect native open service

Julius Marminge committed 8 months ago

Commit 3ee2e101 parent e5542b72 files · +82 −55Browse files

Unified · Split

2 files
  1. apps/server/src/open.test.ts +61 −33
  2. apps/server/src/open.ts +21 −22
apps/server/src/open.test.ts+61−33
@@ -1,45 +1,73 @@
1−import { describe, expect, it } from "vitest";
1+import { assert, describe, it } from "@effect/vitest";
22
33 import { launchDetached, resolveEditorLaunch } from "./open";
4+import { Effect } from "effect";
5+import { assertSuccess } from "@effect/vitest/utils";
46
57 describe("resolveEditorLaunch", () => {
6− it("returns cursor command for cursor editor", () => {
7− expect(resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "cursor" }, "darwin")).toEqual({
8− command: "cursor",
9− args: ["/tmp/workspace"],
10− });
11− });
8+ it.effect("returns cursor command for cursor editor", () =>
9+ Effect.gen(function* () {
10+ const launch = yield* resolveEditorLaunch(
11+ { cwd: "/tmp/workspace", editor: "cursor" },
12+ "darwin",
13+ );
14+ assert.deepEqual(launch, {
15+ command: "cursor",
16+ args: ["/tmp/workspace"],
17+ });
18+ }),
19+ );
20+
21+ it.effect("maps file-manager editor to OS open commands", () =>
22+ Effect.gen(function* () {
23+ const launch1 = yield* resolveEditorLaunch(
24+ { cwd: "/tmp/workspace", editor: "file-manager" },
25+ "darwin",
26+ );
27+ assert.deepEqual(launch1, {
28+ command: "open",
29+ args: ["/tmp/workspace"],
30+ });
31+
32+ const launch2 = yield* resolveEditorLaunch(
33+ { cwd: "C:\\workspace", editor: "file-manager" },
34+ "win32",
35+ );
36+ assert.deepEqual(launch2, {
37+ command: "explorer",
38+ args: ["C:\\workspace"],
39+ });
1240
13− it("maps file-manager editor to OS open commands", () => {
14− expect(
15− resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "file-manager" }, "darwin"),
16− ).toEqual({
17− command: "open",
18− args: ["/tmp/workspace"],
19− });
20− expect(resolveEditorLaunch({ cwd: "C:\\workspace", editor: "file-manager" }, "win32")).toEqual({
21− command: "explorer",
22− args: ["C:\\workspace"],
23− });
24− expect(resolveEditorLaunch({ cwd: "/tmp/workspace", editor: "file-manager" }, "linux")).toEqual(
25− {
41+ const launch3 = yield* resolveEditorLaunch(
42+ { cwd: "/tmp/workspace", editor: "file-manager" },
43+ "linux",
44+ );
45+ assert.deepEqual(launch3, {
2646 command: "xdg-open",
2747 args: ["/tmp/workspace"],
28− },
29− );
30− });
48+ });
49+ }),
50+ );
3151 });
3252
3353 describe("launchDetached", () => {
34− it("resolves when command can be spawned", async () => {
35− await expect(launchDetached(process.execPath, ["-e", "process.exit(0)"])).resolves.toBe(
36− undefined,
37− );
38− });
54+ it.effect("resolves when command can be spawned", () =>
55+ Effect.gen(function* () {
56+ const result = yield* launchDetached({
57+ command: process.execPath,
58+ args: ["-e", "process.exit(0)"],
59+ }).pipe(Effect.result);
60+ assertSuccess(result, undefined);
61+ }),
62+ );
3963
40− it("rejects when command does not exist", async () => {
41− await expect(launchDetached(`t3code-no-such-command-${Date.now()}`, [])).rejects.toBeInstanceOf(
42− Error,
43− );
44− });
64+ it.effect("rejects when command does not exist", () =>
65+ Effect.gen(function* () {
66+ const result = yield* launchDetached({
67+ command: `t3code-no-such-command-${Date.now()}`,
68+ args: [],
69+ }).pipe(Effect.result);
70+ assert.equal(result._tag, "Failure");
71+ }),
72+ );
4573 });
apps/server/src/open.ts+21−22
@@ -3,6 +3,10 @@ import { spawn } from "node:child_process"; · 2 unmodified lines
33 import { EDITORS, type EditorId } from "@t3tools/contracts";
44 import { ServiceMap, Schema, Effect, Layer } from "effect";
55
6+// ==============================
7+// Definitions
8+// ==============================
9+
610 export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError", {
711 message: Schema.String,
812 cause: Schema.optional(Schema.Defect),
@@ -25,13 +29,17 @@ export interface OpenShape { · 16 unmodified lines
2529
2630 export class Open extends ServiceMap.Service<Open, OpenShape>()("server/Open") {}
2731
28−export function resolveEditorLaunch(
32+// ==============================
33+// Implementations
34+// ==============================
35+
36+export const resolveEditorLaunch = Effect.fnUntraced(function* (
2937 input: OpenInEditorInput,
3038 platform: NodeJS.Platform = process.platform,
31−): EditorLaunch {
39+): Effect.fn.Return<EditorLaunch, OpenError> {
3240 const editorDef = EDITORS.find((editor) => editor.id === input.editor);
3341 if (!editorDef) {
34− throw new OpenError({ message: `Unknown editor: ${input.editor}` });
42+ return yield* new OpenError({ message: `Unknown editor: ${input.editor}` });
3543 }
3644
3745 if (editorDef.command) {
@@ -39,7 +47,7 @@ export function resolveEditorLaunch( · 1 unmodified line
3947 }
4048
4149 if (editorDef.id !== "file-manager") {
42− throw new OpenError({ message: `Unsupported editor: ${input.editor}` });
50+ return yield* new OpenError({ message: `Unsupported editor: ${input.editor}` });
4351 }
4452
4553 switch (platform) {
@@ -50,24 +58,25 @@ export function resolveEditorLaunch( · 4 unmodified lines
5058 default:
5159 return { command: "xdg-open", args: [input.cwd] };
5260 }
53−}
61+});
5462
55−export function launchDetached(command: string, args: ReadonlyArray<string>): Promise<void> {
56− return new Promise((resolve, reject) => {
57− const child = spawn(command, [...args], {
63+export const launchDetached = (launch: EditorLaunch) =>
64+ Effect.callback<void, OpenError>((resume) => {
65+ const child = spawn(launch.command, [...launch.args], {
5866 detached: true,
5967 stdio: "ignore",
6068 });
6169
6270 const handleSpawn = () => {
6371 child.unref();
64− resolve();
72+ resume(Effect.void);
6573 };
6674
6775 child.once("spawn", handleSpawn);
68− child.once("error", (error) => reject(error));
76+ child.once("error", (cause) =>
77+ resume(Effect.fail(new OpenError({ message: "failed to spawn detached process", cause }))),
78+ );
6979 });
70−}
7180
7281 const make = Effect.gen(function* () {
7382 const open = yield* Effect.promise(() => import("open"));
@@ -78,17 +87,7 @@ const make = Effect.gen(function* () { · 4 unmodified lines
7887 try: () => open.default(target),
7988 catch: (cause) => new OpenError({ message: "Browser auto-open failed", cause }),
8089 }),
81− openInEditor: (input) =>
82− Effect.tryPromise({
83− try: () => {
84− const launch = resolveEditorLaunch(input);
85− return launchDetached(launch.command, launch.args);
86− },
87− catch: (cause) =>
88− cause instanceof OpenError
89− ? cause
90− : new OpenError({ message: "Failed to open editor", cause }),
91− }),
90+ openInEditor: (input) => Effect.flatMap(resolveEditorLaunch(input), launchDetached),
9291 } satisfies OpenShape;
9392 });
9493

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t