@tui-sandbox/library 2.1.0 → 2.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.0](https://github.com/mikavilpas/tui-sandbox/compare/library-v2.2.0...library-v2.3.0) (2024-11-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **wip:** allow access to neovim via a type safe socket connection ([#109](https://github.com/mikavilpas/tui-sandbox/issues/109)) ([1c4d919](https://github.com/mikavilpas/tui-sandbox/commit/1c4d9194ec8961bde0e8f84d500c27363c66a61b))
9
+
10
+ ## [2.2.0](https://github.com/mikavilpas/tui-sandbox/compare/library-v2.1.0...library-v2.2.0) (2024-11-03)
11
+
12
+
13
+ ### Features
14
+
15
+ * check that neovim is available before starting the server ([#107](https://github.com/mikavilpas/tui-sandbox/issues/107)) ([bcc817f](https://github.com/mikavilpas/tui-sandbox/commit/bcc817fe25a9811cdd7b1832f6811fa4f92974bd))
16
+
3
17
  ## [2.1.0](https://github.com/mikavilpas/tui-sandbox/compare/library-v2.0.2...library-v2.1.0) (2024-11-02)
4
18
 
5
19
 
@@ -15,7 +15,7 @@ export type StartNeovimGenericArguments = {
15
15
  export declare class NeovimApplication {
16
16
  private readonly testEnvironmentPath;
17
17
  readonly application: DisposableSingleApplication;
18
- private testDirectory;
18
+ private state;
19
19
  readonly events: EventEmitter;
20
20
  constructor(testEnvironmentPath: string, application?: DisposableSingleApplication);
21
21
  /**
@@ -1,14 +1,16 @@
1
1
  import assert from "assert";
2
2
  import { exec } from "child_process";
3
3
  import EventEmitter from "events";
4
- import { existsSync } from "fs";
4
+ import { access } from "fs/promises";
5
+ import { tmpdir } from "os";
5
6
  import path from "path";
6
7
  import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication";
7
8
  import { TerminalApplication } from "../utilities/TerminalApplication";
9
+ import { connectNeovimApi } from "./NeovimJavascriptApiClient";
8
10
  export class NeovimApplication {
9
11
  testEnvironmentPath;
10
12
  application;
11
- testDirectory;
13
+ state;
12
14
  events;
13
15
  constructor(testEnvironmentPath, application = new DisposableSingleApplication()) {
14
16
  this.testEnvironmentPath = testEnvironmentPath;
@@ -20,13 +22,16 @@ export class NeovimApplication {
20
22
  */
21
23
  async startNextAndKillCurrent(testDirectory, startArgs) {
22
24
  await this[Symbol.asyncDispose]();
23
- this.testDirectory = testDirectory;
25
+ assert(this.state === undefined, "NeovimApplication state should be undefined after disposing so that no previous state is reused.");
24
26
  const neovimArguments = ["-u", "test-setup.lua"];
25
27
  if (startArgs.startupScriptModifications) {
26
28
  for (const modification of startArgs.startupScriptModifications) {
27
29
  const file = path.join(testDirectory.rootPathAbsolute, "config-modifications", modification);
28
- if (!existsSync(file)) {
29
- throw new Error(`startupScriptModifications file does not exist: ${file}`);
30
+ try {
31
+ await access(file);
32
+ }
33
+ catch (e) {
34
+ throw new Error(`startupScriptModifications file does not exist: ${file}. Error: ${String(e)}`);
30
35
  }
31
36
  neovimArguments.push("-c", `lua dofile('${file}')`);
32
37
  }
@@ -43,6 +48,9 @@ export class NeovimApplication {
43
48
  neovimArguments.push(filePath);
44
49
  }
45
50
  }
51
+ const id = Math.random().toString().slice(2, 8);
52
+ const socketPath = `${tmpdir()}/tui-sandbox-nvim-socket-${id}`;
53
+ neovimArguments.push("--listen", socketPath);
46
54
  const stdout = this.events;
47
55
  await this.application.startNextAndKillCurrent(async () => {
48
56
  return TerminalApplication.start({
@@ -59,12 +67,25 @@ export class NeovimApplication {
59
67
  });
60
68
  const processId = this.application.processId();
61
69
  assert(processId !== undefined, "Neovim was started without a process ID. This is a bug - please open an issue.");
70
+ this.state = {
71
+ testDirectory,
72
+ socketPath,
73
+ client: connectNeovimApi(socketPath),
74
+ };
62
75
  console.log(`🚀 Started Neovim instance ${processId}`);
63
76
  }
64
77
  async [Symbol.asyncDispose]() {
65
78
  await this.application[Symbol.asyncDispose]();
66
- if (this.testDirectory) {
67
- exec(`rm -rf ${this.testDirectory.rootPathAbsolute}`);
79
+ if (!this.state)
80
+ return;
81
+ exec(`rm -rf ${this.state.testDirectory.rootPathAbsolute}`);
82
+ try {
83
+ await access(this.state.socketPath);
84
+ throw new Error(`Socket file ${this.state.socketPath} should have been removed by neovim when it exited.`);
85
+ }
86
+ catch (e) {
87
+ // all good
68
88
  }
89
+ this.state = undefined;
69
90
  }
70
91
  }
@@ -0,0 +1,5 @@
1
+ import type { NeovimClient as NeovimApiClient } from "neovim";
2
+ import { Lazy } from "../utilities/Lazy";
3
+ export type NeovimJavascriptApiClient = NeovimApiClient;
4
+ export type PollingInterval = 100;
5
+ export declare function connectNeovimApi(socketPath: string): Lazy<Promise<NeovimJavascriptApiClient>>;
@@ -0,0 +1,21 @@
1
+ import { access } from "fs/promises";
2
+ import { attach } from "neovim";
3
+ import { Lazy } from "../utilities/Lazy";
4
+ export function connectNeovimApi(socketPath) {
5
+ // it takes about 100ms for the socket file to be created - best make this
6
+ // Lazy so that we don't wait for it unnecessarily.
7
+ return new Lazy(async () => {
8
+ for (let i = 0; i < 100; i++) {
9
+ try {
10
+ await access(socketPath);
11
+ console.log(`socket file ${socketPath} created after at attempt ${i + 1}`);
12
+ break;
13
+ }
14
+ catch (e) {
15
+ console.log(`polling for socket file ${socketPath} to be created (attempt ${i + 1})`);
16
+ await new Promise(resolve => setTimeout(resolve, 100));
17
+ }
18
+ }
19
+ return attach({ socket: socketPath });
20
+ });
21
+ }
@@ -0,0 +1,33 @@
1
+ import { access } from "fs/promises";
2
+ import { attach } from "neovim";
3
+ import { connectNeovimApi } from "./NeovimJavascriptApiClient";
4
+ vi.mock("neovim");
5
+ vi.mock("fs/promises");
6
+ const mocked = {
7
+ attach: vi.mocked(attach),
8
+ access: vi.mocked(access),
9
+ log: vi.spyOn(console, "log").mockImplementation(() => {
10
+ //
11
+ }),
12
+ };
13
+ const pollingInterval = 100;
14
+ beforeEach(() => {
15
+ vi.useFakeTimers();
16
+ });
17
+ afterEach(() => {
18
+ vi.useRealTimers();
19
+ });
20
+ it("is lazy - does not connect right away", async () => {
21
+ mocked.access.mockRejectedValue(new Error("no such file or directory"));
22
+ connectNeovimApi("foosocket");
23
+ vi.advanceTimersByTime(pollingInterval);
24
+ expect(mocked.attach).not.toHaveBeenCalled();
25
+ });
26
+ it("connects right away if the socket file is already there", async () => {
27
+ mocked.access.mockResolvedValue(undefined);
28
+ const lazyClient = connectNeovimApi("foosocket");
29
+ await lazyClient.get();
30
+ vi.advanceTimersByTime(pollingInterval);
31
+ expect(mocked.attach).toHaveBeenCalledWith({ socket: "foosocket" });
32
+ expect(mocked.attach).toHaveBeenCalledTimes(1);
33
+ });
@@ -5,7 +5,8 @@ import type { TestServerConfig } from "./updateTestdirectorySchemaFile";
5
5
  * shuts down */
6
6
  declare const autocleanup: AsyncDisposableStack;
7
7
  export { autocleanup };
8
- declare function createAppRouter(config: TestServerConfig): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
8
+ /** @private */
9
+ export declare function createAppRouter(config: TestServerConfig): Promise<import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
9
10
  ctx: object;
10
11
  meta: object;
11
12
  errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
@@ -53,7 +54,7 @@ declare function createAppRouter(config: TestServerConfig): import("@trpc/server
53
54
  output: void;
54
55
  }>;
55
56
  }>;
56
- }>>;
57
- export type AppRouter = ReturnType<typeof createAppRouter>;
57
+ }>>>;
58
+ export type AppRouter = Awaited<ReturnType<typeof createAppRouter>>;
58
59
  export type RouterInput = inferRouterInputs<AppRouter>;
59
60
  export declare function startTestServer(config: TestServerConfig): Promise<TestServer>;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { trpc } from "./connection/trpc";
3
3
  import * as neovim from "./neovim";
4
4
  import { TestServer } from "./TestServer";
5
+ import { applicationAvailable } from "./utilities/applicationAvailable";
5
6
  import { tabIdSchema } from "./utilities/tabId";
6
7
  /** Stack for managing resources that need to be disposed of when the server
7
8
  * shuts down */
@@ -10,7 +11,12 @@ autocleanup.defer(() => {
10
11
  console.log("Closing any open test applications");
11
12
  });
12
13
  export { autocleanup };
13
- function createAppRouter(config) {
14
+ /** @private */
15
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
16
+ export async function createAppRouter(config) {
17
+ if (!(await applicationAvailable("nvim"))) {
18
+ throw new Error("Neovim is not installed. Please install Neovim (nvim).");
19
+ }
14
20
  const appRouter = trpc.router({
15
21
  neovim: trpc.router({
16
22
  start: trpc.procedure
@@ -45,7 +51,7 @@ function createAppRouter(config) {
45
51
  }
46
52
  export async function startTestServer(config) {
47
53
  const testServer = new TestServer(3000);
48
- const appRouter = createAppRouter(config);
54
+ const appRouter = await createAppRouter(config);
49
55
  await testServer.startAndRun(appRouter, config);
50
56
  return testServer;
51
57
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { createAppRouter } from "./server";
2
+ import { applicationAvailable } from "./utilities/applicationAvailable";
3
+ vi.mock("./utilities/applicationAvailable");
4
+ const mocked = {
5
+ applicationAvailable: vi.mocked(applicationAvailable),
6
+ };
7
+ describe("Neovim server", () => {
8
+ it("complains when neovim is not installed", async () => {
9
+ await expect(createAppRouter({
10
+ outputFilePath: "outputFilePath",
11
+ testEnvironmentPath: "testEnvironmentPath",
12
+ })).rejects.toThrow("Neovim is not installed. Please install Neovim (nvim).");
13
+ expect(mocked.applicationAvailable).toHaveBeenCalledWith("nvim");
14
+ });
15
+ it("creates a router when neovim is installed", async () => {
16
+ mocked.applicationAvailable.mockResolvedValue("nvim");
17
+ await expect(createAppRouter({
18
+ outputFilePath: "outputFilePath",
19
+ testEnvironmentPath: "testEnvironmentPath",
20
+ })).resolves.toBeDefined();
21
+ expect(mocked.applicationAvailable).toHaveBeenCalledWith("nvim");
22
+ });
23
+ });
@@ -0,0 +1 @@
1
+ export declare function applicationAvailable(command: string): Promise<string | null>;
@@ -0,0 +1,4 @@
1
+ import commandExists from "command-exists";
2
+ export async function applicationAvailable(command) {
3
+ return commandExists(command);
4
+ }
@@ -0,0 +1,12 @@
1
+ import { applicationAvailable } from "./applicationAvailable";
2
+ describe("sanity checks for mocking", () => {
3
+ // because it makes no sense to mock the actual implementation if we don't
4
+ // know what it does in the current version, we better check what it's
5
+ // expected to do
6
+ it("can find neovim using the actual implementation", async () => {
7
+ await expect(applicationAvailable("nvim")).resolves.toBe("nvim");
8
+ });
9
+ it("complains when a nonexistent command is checked", async () => {
10
+ await expect(applicationAvailable("thisCommandDoesNotExist")).rejects.toBe(null);
11
+ });
12
+ });