@tui-sandbox/library 6.0.2 → 7.0.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/browser/assets/{index-DJEG76in.js → index-Bup5cvb2.js} +3 -3
  3. package/dist/browser/index.html +1 -1
  4. package/dist/src/browser/neovim-client.d.ts +3 -1
  5. package/dist/src/browser/neovim-client.js +5 -2
  6. package/dist/src/client/index.d.ts +1 -1
  7. package/dist/src/client/index.js +1 -1
  8. package/dist/src/client/terminal-client.d.ts +15 -0
  9. package/dist/src/client/{neovim-client.js → terminal-client.js} +10 -1
  10. package/dist/src/server/cypress-support/contents.js +19 -1
  11. package/dist/src/server/cypress-support/contents.test.js +19 -1
  12. package/dist/src/server/cypress-support/createCypressSupportFile.js +2 -1
  13. package/dist/src/server/neovim/NeovimApplication.d.ts +10 -1
  14. package/dist/src/server/neovim/NeovimApplication.js +15 -12
  15. package/dist/src/server/neovim/environment/createTempDir.test.js +2 -4
  16. package/dist/src/server/neovim/index.d.ts +4 -1
  17. package/dist/src/server/neovim/index.js +34 -0
  18. package/dist/src/server/server.d.ts +54 -4
  19. package/dist/src/server/server.js +13 -72
  20. package/dist/src/server/types.d.ts +7 -0
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +2 -2
  23. package/src/browser/neovim-client.ts +11 -3
  24. package/src/client/index.ts +1 -1
  25. package/src/client/{neovim-client.ts → terminal-client.ts} +13 -3
  26. package/src/server/cypress-support/contents.test.ts +19 -1
  27. package/src/server/cypress-support/contents.ts +19 -1
  28. package/src/server/cypress-support/createCypressSupportFile.ts +2 -1
  29. package/src/server/neovim/NeovimApplication.ts +22 -15
  30. package/src/server/neovim/environment/createTempDir.test.ts +1 -1
  31. package/src/server/neovim/index.ts +46 -1
  32. package/src/server/server.ts +18 -6
  33. package/src/server/types.ts +12 -0
  34. package/dist/src/client/neovim-client.d.ts +0 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tui-sandbox/library",
3
- "version": "6.0.2",
3
+ "version": "7.0.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "@types/command-exists": "1.2.3",
32
32
  "@types/cors": "2.8.17",
33
33
  "@types/express": "5.0.0",
34
- "@types/node": "22.9.0",
34
+ "@types/node": "22.9.1",
35
35
  "nodemon": "3.1.7",
36
36
  "vite": "5.4.11",
37
37
  "vitest": "2.1.5"
@@ -1,12 +1,13 @@
1
- import { NeovimClient } from "../client/index.js"
2
- import type { StartNeovimGenericArguments, TestDirectory } from "../server/types.js"
1
+ import { TerminalClient } from "../client/index.js"
2
+ import type { BlockingCommandClientInput } from "../server/server.js"
3
+ import type { BlockingShellCommandOutput, StartNeovimGenericArguments, TestDirectory } from "../server/types.js"
3
4
 
4
5
  const app = document.querySelector<HTMLElement>("#app")
5
6
  if (!app) {
6
7
  throw new Error("No app element found")
7
8
  }
8
9
 
9
- const client = new NeovimClient(app)
10
+ const client = new TerminalClient(app)
10
11
 
11
12
  /** Entrypoint for the test runner (cypress) */
12
13
  window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): Promise<TestDirectory> {
@@ -19,8 +20,15 @@ window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): P
19
20
  return testDirectory
20
21
  }
21
22
 
23
+ window.runBlockingShellCommand = async function (
24
+ input: BlockingCommandClientInput
25
+ ): Promise<BlockingShellCommandOutput> {
26
+ return client.runBlockingShellCommand(input)
27
+ }
28
+
22
29
  declare global {
23
30
  interface Window {
24
31
  startNeovim(startArguments?: StartNeovimGenericArguments): Promise<TestDirectory>
32
+ runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
25
33
  }
26
34
  }
@@ -1,4 +1,4 @@
1
1
  // This is the public client api. Semantic versioning will be applied to this.
2
2
 
3
3
  export { rgbify } from "./color-utilities.js"
4
- export { NeovimClient } from "./neovim-client.js"
4
+ export { TerminalClient } from "./terminal-client.js"
@@ -1,12 +1,14 @@
1
1
  import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client"
2
2
  import type { Terminal } from "@xterm/xterm"
3
3
  import "@xterm/xterm/css/xterm.css"
4
- import type { AppRouter } from "../server/server.ts"
5
- import type { StartNeovimGenericArguments, TestDirectory } from "../server/types.js"
4
+ import type { AppRouter, BlockingCommandClientInput } from "../server/server.js"
5
+ import type { BlockingShellCommandOutput, StartNeovimGenericArguments, TestDirectory } from "../server/types.js"
6
6
  import "./style.css"
7
7
  import { getTabId, startTerminal } from "./websocket-client.js"
8
8
 
9
- export class NeovimClient {
9
+ /** Manages the terminal state in the browser as well as the (browser's)
10
+ * connection to the server side terminal application api. */
11
+ export class TerminalClient {
10
12
  private readonly ready: Promise<void>
11
13
  private readonly tabId: { tabId: string }
12
14
  private readonly terminal: Terminal
@@ -82,4 +84,12 @@ export class NeovimClient {
82
84
 
83
85
  return testDirectory
84
86
  }
87
+
88
+ public async runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput> {
89
+ await this.ready
90
+ return this.trpc.neovim.runBlockingShellCommand.mutate({
91
+ ...input,
92
+ tabId: this.tabId,
93
+ })
94
+ }
85
95
  }
@@ -7,7 +7,11 @@ it("should return the expected contents", async () => {
7
7
  //
8
8
  // This file is autogenerated by tui-sandbox. Do not edit it directly.
9
9
  //
10
- import type { StartNeovimGenericArguments } from "@tui-sandbox/library/dist/src/server/types"
10
+ import type { BlockingCommandClientInput } from "@tui-sandbox/library/dist/src/server/server"
11
+ import type {
12
+ BlockingShellCommandOutput,
13
+ StartNeovimGenericArguments,
14
+ } from "@tui-sandbox/library/dist/src/server/types"
11
15
  import type { OverrideProperties } from "type-fest"
12
16
  import type { MyTestDirectory, MyTestDirectoryFile } from "../../MyTestDirectory"
13
17
 
@@ -19,6 +23,7 @@ it("should return the expected contents", async () => {
19
23
  declare global {
20
24
  interface Window {
21
25
  startNeovim(startArguments?: MyStartNeovimServerArguments): Promise<NeovimContext>
26
+ runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
22
27
  }
23
28
  }
24
29
 
@@ -37,6 +42,12 @@ it("should return the expected contents", async () => {
37
42
  })
38
43
  })
39
44
 
45
+ Cypress.Commands.add("runBlockingShellCommand", (input: BlockingCommandClientInput) => {
46
+ cy.window().then(async win => {
47
+ return await win.runBlockingShellCommand(input)
48
+ })
49
+ })
50
+
40
51
  Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial<Cypress.TypeOptions>) => {
41
52
  // the syntax for keys is described here:
42
53
  // https://docs.cypress.io/api/commands/type
@@ -54,7 +65,14 @@ it("should return the expected contents", async () => {
54
65
  namespace Cypress {
55
66
  interface Chainable {
56
67
  startNeovim(args?: MyStartNeovimServerArguments): Chainable<NeovimContext>
68
+
69
+ /** Types text into the terminal, making the terminal application receive
70
+ * the keystrokes as input. Requires neovim to be running. */
57
71
  typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): Chainable<void>
72
+
73
+ /** Runs a shell command in a blocking manner, waiting for the command to
74
+ * finish before returning. Requires neovim to be running. */
75
+ runBlockingShellCommand(input: BlockingCommandClientInput): Chainable<BlockingShellCommandOutput>
58
76
  }
59
77
  }
60
78
  }
@@ -11,8 +11,12 @@ export async function createCypressSupportFileContents(): Promise<string> {
11
11
  //
12
12
  // This file is autogenerated by tui-sandbox. Do not edit it directly.
13
13
  //
14
+ import type { BlockingCommandClientInput } from "@tui-sandbox/library/dist/src/server/server"
15
+ import type {
16
+ BlockingShellCommandOutput,
17
+ StartNeovimGenericArguments,
18
+ } from "@tui-sandbox/library/dist/src/server/types"
14
19
  import type { OverrideProperties } from "type-fest"
15
- import type { StartNeovimGenericArguments } from "@tui-sandbox/library/dist/src/server/types"
16
20
  import type { MyTestDirectory, MyTestDirectoryFile } from "../../MyTestDirectory"
17
21
 
18
22
  export type NeovimContext = {
@@ -23,6 +27,7 @@ export type NeovimContext = {
23
27
  declare global {
24
28
  interface Window {
25
29
  startNeovim(startArguments?: MyStartNeovimServerArguments): Promise<NeovimContext>
30
+ runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
26
31
  }
27
32
  }
28
33
 
@@ -41,6 +46,12 @@ Cypress.Commands.add("startNeovim", (startArguments?: MyStartNeovimServerArgumen
41
46
  })
42
47
  })
43
48
 
49
+ Cypress.Commands.add("runBlockingShellCommand", (input: BlockingCommandClientInput) => {
50
+ cy.window().then(async win => {
51
+ return await win.runBlockingShellCommand(input)
52
+ })
53
+ })
54
+
44
55
  Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial<Cypress.TypeOptions>) => {
45
56
  // the syntax for keys is described here:
46
57
  // https://docs.cypress.io/api/commands/type
@@ -58,7 +69,14 @@ declare global {
58
69
  namespace Cypress {
59
70
  interface Chainable {
60
71
  startNeovim(args?: MyStartNeovimServerArguments): Chainable<NeovimContext>
72
+
73
+ /** Types text into the terminal, making the terminal application receive
74
+ * the keystrokes as input. Requires neovim to be running. */
61
75
  typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): Chainable<void>
76
+
77
+ /** Runs a shell command in a blocking manner, waiting for the command to
78
+ * finish before returning. Requires neovim to be running. */
79
+ runBlockingShellCommand(input: BlockingCommandClientInput): Chainable<BlockingShellCommandOutput>
62
80
  }
63
81
  }
64
82
  }
@@ -25,13 +25,14 @@ export async function createCypressSupportFile({
25
25
  try {
26
26
  oldSchema = readFileSync(outputFilePath, "utf-8")
27
27
  } catch (error) {
28
- console.log(`No existing cypress support file found at ${outputFilePath}, creating a new one`)
28
+ console.log(`No existing cypress support file found at ${outputFilePath}`)
29
29
  }
30
30
 
31
31
  if (oldSchema !== text) {
32
32
  // it's important to not write the file if the schema hasn't changed
33
33
  // because file watchers will trigger on file changes and we don't want to
34
34
  // trigger a build if the schema hasn't changed
35
+ console.log(`🪛 Writing cypress support file to ${outputFilePath}`)
35
36
  writeFileSync(outputFilePath, text)
36
37
  return "updated"
37
38
  } else {
@@ -76,7 +76,7 @@ type ResettableState = {
76
76
  }
77
77
 
78
78
  export class NeovimApplication {
79
- private state: ResettableState | undefined
79
+ public state: ResettableState | undefined
80
80
  public readonly events: EventEmitter
81
81
 
82
82
  public constructor(
@@ -135,20 +135,7 @@ export class NeovimApplication {
135
135
  const stdout = this.events
136
136
 
137
137
  await this.application.startNextAndKillCurrent(async () => {
138
- const env = {
139
- ...process.env,
140
- HOME: testDirectory.rootPathAbsolute,
141
-
142
- // this is needed so that neovim can load its configuration, emulating
143
- // a common setup real neovim users have
144
- XDG_CONFIG_HOME: join(testDirectory.rootPathAbsolute, ".config"),
145
- // the data directory is where lazy.nvim stores its plugins. To prevent
146
- // downloading a new set of plugins for each test, share the data
147
- // directory.
148
- XDG_DATA_HOME: join(testDirectory.testEnvironmentPath, ".repro", "data"),
149
-
150
- ...startArgs.additionalEnvironmentVariables,
151
- }
138
+ const env = this.getEnvironmentVariables(testDirectory, startArgs.additionalEnvironmentVariables)
152
139
  return TerminalApplication.start({
153
140
  command: "nvim",
154
141
  args: neovimArguments,
@@ -176,6 +163,26 @@ export class NeovimApplication {
176
163
  console.log(`🚀 Started Neovim instance ${processId}`)
177
164
  }
178
165
 
166
+ public getEnvironmentVariables(
167
+ testDirectory: TestDirectory,
168
+ additionalEnvironmentVariables?: Record<string, string>
169
+ ): NodeJS.ProcessEnv {
170
+ return {
171
+ ...process.env,
172
+ HOME: testDirectory.rootPathAbsolute,
173
+
174
+ // this is needed so that neovim can load its configuration, emulating
175
+ // a common setup real neovim users have
176
+ XDG_CONFIG_HOME: join(testDirectory.rootPathAbsolute, ".config"),
177
+ // the data directory is where lazy.nvim stores its plugins. To prevent
178
+ // downloading a new set of plugins for each test, share the data
179
+ // directory.
180
+ XDG_DATA_HOME: join(testDirectory.testEnvironmentPath, ".repro", "data"),
181
+
182
+ ...additionalEnvironmentVariables,
183
+ }
184
+ }
185
+
179
186
  async [Symbol.asyncDispose](): Promise<void> {
180
187
  await this.application[Symbol.asyncDispose]()
181
188
 
@@ -23,7 +23,7 @@ class TempDirectory implements Disposable {
23
23
 
24
24
  it("should create a temp dir with no contents", async () => {
25
25
  // typically the user will want to have contents, but this should not be an error
26
- await using dir = TempDirectory.create()
26
+ using dir = TempDirectory.create()
27
27
  const result = await createTempDir({
28
28
  testEnvironmentPath: dir.path,
29
29
  outputFilePath: nodePath.join(dir.path, "MyTestDirectory.ts"),
@@ -1,5 +1,9 @@
1
1
  import assert from "assert"
2
- import type { StartNeovimGenericArguments, TestDirectory } from "../types.js"
2
+ import { exec } from "child_process"
3
+ import "core-js/proposals/async-explicit-resource-management.js"
4
+ import util from "util"
5
+ import type { BlockingCommandInput } from "../server.js"
6
+ import type { BlockingShellCommandOutput, StartNeovimGenericArguments, TestDirectory } from "../types.js"
3
7
  import type { TestServerConfig } from "../updateTestdirectorySchemaFile.js"
4
8
  import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js"
5
9
  import type { TabId } from "../utilities/tabId.js"
@@ -61,3 +65,44 @@ export async function sendStdin(options: { tabId: TabId; data: string }): Promis
61
65
 
62
66
  await neovim.application.write(options.data)
63
67
  }
68
+
69
+ export async function runBlockingShellCommand(
70
+ signal: AbortSignal | undefined,
71
+ input: BlockingCommandInput
72
+ ): Promise<BlockingShellCommandOutput> {
73
+ const neovim = neovims.get(input.tabId.tabId)
74
+ assert(
75
+ neovim !== undefined,
76
+ `Neovim instance for clientId not found - cannot run blocking shell command. Maybe neovim's not started yet?`
77
+ )
78
+ const testDirectory = neovim.state?.testDirectory
79
+ assert(testDirectory, `Test directory not found for client id ${input.tabId.tabId}. Maybe neovim's not started yet?`)
80
+
81
+ const execPromise = util.promisify(exec)
82
+ const env = neovim.getEnvironmentVariables(testDirectory, input.envOverrides)
83
+ const processPromise = execPromise(input.command, {
84
+ signal: signal,
85
+ shell: input.shell,
86
+ uid: input.uid,
87
+ gid: input.gid,
88
+ cwd: input.cwd ?? env["HOME"],
89
+ env,
90
+ })
91
+
92
+ try {
93
+ const result = await processPromise
94
+ console.log(
95
+ `Successfully ran shell blockingCommand (${input.command}) with stdout: ${result.stdout}, stderr: ${result.stderr}`
96
+ )
97
+ return {
98
+ type: "success",
99
+ stdout: result.stdout,
100
+ stderr: result.stderr,
101
+ } satisfies BlockingShellCommandOutput
102
+ } catch (e) {
103
+ console.warn(`Error running shell blockingCommand (${input.command})`, e)
104
+ return {
105
+ type: "failed",
106
+ }
107
+ }
108
+ }
@@ -1,5 +1,6 @@
1
1
  import type { inferRouterInputs } from "@trpc/server"
2
2
  import "core-js/proposals/async-explicit-resource-management.js"
3
+ import type { Except } from "type-fest"
3
4
  import { z } from "zod"
4
5
  import { trpc } from "./connection/trpc.js"
5
6
  import * as neovim from "./neovim/index.js"
@@ -8,13 +9,20 @@ import type { TestServerConfig } from "./updateTestdirectorySchemaFile.js"
8
9
  import { applicationAvailable } from "./utilities/applicationAvailable.js"
9
10
  import { tabIdSchema } from "./utilities/tabId.js"
10
11
 
11
- /** Stack for managing resources that need to be disposed of when the server
12
- * shuts down */
13
- await using autocleanup = new AsyncDisposableStack()
14
- autocleanup.defer(() => {
15
- console.log("Closing any open test applications")
12
+ const blockingCommandInputSchema = z.object({
13
+ command: z.string(),
14
+ shell: z.string().optional(),
15
+ tabId: tabIdSchema,
16
+
17
+ // child_process.ProcessEnvOptions
18
+ uid: z.number().optional(),
19
+ gid: z.number().optional(),
20
+ cwd: z.string().optional(),
21
+ envOverrides: z.record(z.string()).optional(),
16
22
  })
17
- export { autocleanup }
23
+
24
+ export type BlockingCommandClientInput = Except<BlockingCommandInput, "tabId">
25
+ export type BlockingCommandInput = z.infer<typeof blockingCommandInputSchema>
18
26
 
19
27
  /** @private */
20
28
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -59,6 +67,10 @@ export async function createAppRouter(config: TestServerConfig) {
59
67
  sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
60
68
  return neovim.sendStdin(options.input)
61
69
  }),
70
+
71
+ runBlockingShellCommand: trpc.procedure.input(blockingCommandInputSchema).mutation(async options => {
72
+ return neovim.runBlockingShellCommand(options.signal, options.input)
73
+ }),
62
74
  }),
63
75
  })
64
76
 
@@ -30,3 +30,15 @@ export type TestDirectory = {
30
30
  }
31
31
 
32
32
  export type { StartNeovimGenericArguments } from "../server/neovim/NeovimApplication.js"
33
+
34
+ export type BlockingShellCommandOutput =
35
+ | {
36
+ type: "success"
37
+ stdout: string
38
+ stderr: string
39
+ }
40
+ | {
41
+ type: "failed"
42
+ // for now we log the error to the server's console output. It will be
43
+ // visible when running the tests.
44
+ }
@@ -1,11 +0,0 @@
1
- import "@xterm/xterm/css/xterm.css";
2
- import type { StartNeovimGenericArguments, TestDirectory } from "../server/types.js";
3
- import "./style.css";
4
- export declare class NeovimClient {
5
- private readonly ready;
6
- private readonly tabId;
7
- private readonly terminal;
8
- private readonly trpc;
9
- constructor(app: HTMLElement);
10
- startNeovim(args: StartNeovimGenericArguments): Promise<TestDirectory>;
11
- }