@tui-sandbox/library 11.11.1 → 12.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.
- package/dist/browser/assets/index-CYUPHpRk.css +1 -0
- package/dist/browser/assets/index-DPQpUaDL.js +9 -0
- package/dist/browser/index.html +2 -2
- package/dist/src/browser/neovim-client.d.ts +2 -0
- package/dist/src/browser/neovim-client.js +2 -0
- package/dist/src/browser/neovim-client.js.map +1 -1
- package/dist/src/client/index.d.ts +1 -0
- package/dist/src/client/neovim-terminal-client.d.ts +0 -2
- package/dist/src/client/neovim-terminal-client.js +0 -2
- package/dist/src/client/neovim-terminal-client.js.map +1 -1
- package/dist/src/client/startTerminal.d.ts +0 -2
- package/dist/src/client/startTerminal.js +0 -2
- package/dist/src/client/startTerminal.js.map +1 -1
- package/dist/src/client/terminal-terminal-client.d.ts +0 -1
- package/dist/src/client/terminal-terminal-client.js +0 -1
- package/dist/src/client/terminal-terminal-client.js.map +1 -1
- package/dist/src/server/cypress-support/contents.js +9 -11
- package/dist/src/server/cypress-support/contents.js.map +1 -1
- package/dist/src/server/index.d.ts +4 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -9
- package/CHANGELOG.md +0 -933
- package/dist/browser/assets/index-BQzArJW3.js +0 -9
- package/dist/browser/assets/index-hkmOP7rU.css +0 -1
- package/index.html +0 -11
- package/src/browser/neovim-client.ts +0 -126
- package/src/client/MyNeovimConfigModification.test.ts +0 -25
- package/src/client/MyNeovimConfigModification.ts +0 -8
- package/src/client/color-utilities.test.ts +0 -10
- package/src/client/color-utilities.ts +0 -9
- package/src/client/cypress-assertions.ts +0 -35
- package/src/client/index.ts +0 -4
- package/src/client/neovim-terminal-client.ts +0 -130
- package/src/client/public/DejaVuSansMNerdFontMono-Regular.ttf +0 -0
- package/src/client/startTerminal.ts +0 -97
- package/src/client/style.css +0 -32
- package/src/client/terminal-config.ts +0 -25
- package/src/client/terminal-terminal-client.ts +0 -106
- package/src/client/validateMouseEvent.ts +0 -22
- package/src/scripts/commands/commandRun.ts +0 -68
- package/src/scripts/commands/commandTuiNeovimExec.ts +0 -36
- package/src/scripts/commands/commandTuiNeovimPrepare.ts +0 -12
- package/src/scripts/commands/commandTuiStart.ts +0 -36
- package/src/scripts/parseArguments.test.ts +0 -28
- package/src/scripts/parseArguments.ts +0 -66
- package/src/scripts/resolveConfig.test.ts +0 -78
- package/src/scripts/resolveTuiConfig.ts +0 -65
- package/src/scripts/tui.ts +0 -77
- package/src/server/TestServer.ts +0 -73
- package/src/server/applications/neovim/NeovimApplication.ts +0 -236
- package/src/server/applications/neovim/NeovimJavascriptApiClient.test.ts +0 -48
- package/src/server/applications/neovim/NeovimJavascriptApiClient.ts +0 -68
- package/src/server/applications/neovim/api.ts +0 -257
- package/src/server/applications/neovim/environment/TempDirectory.ts +0 -18
- package/src/server/applications/neovim/environment/createTempDir.test.ts +0 -29
- package/src/server/applications/neovim/environment/createTempDir.ts +0 -91
- package/src/server/applications/neovim/neovimRouter.ts +0 -100
- package/src/server/applications/neovim/prepareNewTestDirectory.test.ts +0 -32
- package/src/server/applications/neovim/prepareNewTestDirectory.ts +0 -21
- package/src/server/applications/terminal/TerminalTestApplication.ts +0 -101
- package/src/server/applications/terminal/api.ts +0 -89
- package/src/server/applications/terminal/runBlockingShellCommand.test.ts +0 -41
- package/src/server/applications/terminal/runBlockingShellCommand.ts +0 -64
- package/src/server/applications/terminal/terminalRouter.ts +0 -47
- package/src/server/blockingCommandInputSchema.test.ts +0 -24
- package/src/server/blockingCommandInputSchema.ts +0 -27
- package/src/server/config.test.ts +0 -7
- package/src/server/config.ts +0 -18
- package/src/server/connection/trpc.ts +0 -3
- package/src/server/cypress-support/contents.ts +0 -245
- package/src/server/cypress-support/createCypressSupportFile.test.ts +0 -69
- package/src/server/cypress-support/createCypressSupportFile.ts +0 -56
- package/src/server/dirtree/index.test.ts +0 -352
- package/src/server/dirtree/index.ts +0 -144
- package/src/server/dirtree/json-to-zod.ts +0 -60
- package/src/server/index.ts +0 -6
- package/src/server/server.ts +0 -34
- package/src/server/types.ts +0 -98
- package/src/server/updateTestdirectorySchemaFile.test.ts +0 -49
- package/src/server/updateTestdirectorySchemaFile.ts +0 -71
- package/src/server/utilities/DisposableSingleApplication.test.ts +0 -92
- package/src/server/utilities/DisposableSingleApplication.ts +0 -49
- package/src/server/utilities/Lazy.ts +0 -16
- package/src/server/utilities/TerminalApplication.ts +0 -100
- package/src/server/utilities/generator.test.ts +0 -50
- package/src/server/utilities/generator.ts +0 -12
- package/src/server/utilities/tabId.ts +0 -4
- package/src/server/utilities/timeout.ts +0 -3
- package/src/server/utilities/timeoutable.ts +0 -19
- package/tsconfig.json +0 -31
- package/vite.config.js +0 -27
package/src/server/types.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { VimValue } from "neovim/lib/types/VimValue.js"
|
|
2
|
-
import * as z from "zod"
|
|
3
|
-
|
|
4
|
-
/** Describes the contents of the test directory, which is a blueprint for
|
|
5
|
-
* files and directories. Tests can create a unique, safe environment for
|
|
6
|
-
* interacting with the contents of such a directory.
|
|
7
|
-
*
|
|
8
|
-
* Having strong typing for the test directory contents ensures that tests can
|
|
9
|
-
* be written with confidence that the files and directories they expect are
|
|
10
|
-
* actually found. Otherwise the tests are brittle and can break easily.
|
|
11
|
-
*/
|
|
12
|
-
export type TestDirectory<TContents extends Record<string, unknown> = Record<string, unknown>> = {
|
|
13
|
-
/** The path to the unique test directory (the root).
|
|
14
|
-
*
|
|
15
|
-
* @example /Users/mikavilpas/git/tui-sandbox/packages/integration-tests/test-environment/testdirs/dir-0199UZ
|
|
16
|
-
*/
|
|
17
|
-
rootPathAbsolute: string
|
|
18
|
-
|
|
19
|
-
/** The path to the test environment directory, which is the blueprint for
|
|
20
|
-
*
|
|
21
|
-
* the test directory.
|
|
22
|
-
* @example /Users/mikavilpas/git/tui-sandbox/packages/integration-tests/test-environment
|
|
23
|
-
* */
|
|
24
|
-
testEnvironmentPath: string
|
|
25
|
-
|
|
26
|
-
/** The relative path from the {@link testEnvironmentPath} to {@link rootPathAbsolute}.
|
|
27
|
-
*
|
|
28
|
-
* @example testdirs/dir-0199UZ/
|
|
29
|
-
*/
|
|
30
|
-
testEnvironmentPathRelative: string
|
|
31
|
-
|
|
32
|
-
latestEnvironmentSymlink: string
|
|
33
|
-
|
|
34
|
-
contents: TContents
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const serverTestDirectorySchema = z.object({
|
|
38
|
-
rootPathAbsolute: z.string(),
|
|
39
|
-
testEnvironmentPath: z.string(),
|
|
40
|
-
testEnvironmentPathRelative: z.string(),
|
|
41
|
-
latestEnvironmentSymlink: z.string(),
|
|
42
|
-
contents: z.record(z.string(), z.unknown()),
|
|
43
|
-
})
|
|
44
|
-
export type ServerTestDirectory = z.infer<typeof serverTestDirectorySchema>
|
|
45
|
-
|
|
46
|
-
export type TestEnvironmentCommonEnvironmentVariables = {
|
|
47
|
-
HOME: string
|
|
48
|
-
|
|
49
|
-
/** This is needed so that the application being tested can load its
|
|
50
|
-
* configuration, emulating a common setup real users have
|
|
51
|
-
*/
|
|
52
|
-
XDG_CONFIG_HOME: string
|
|
53
|
-
|
|
54
|
-
/** The data directory is where the application stores its data. To prevent
|
|
55
|
-
* downloading a new set of plugins/whatever for each test, share the data
|
|
56
|
-
* directory.
|
|
57
|
-
*/
|
|
58
|
-
XDG_DATA_HOME: string
|
|
59
|
-
|
|
60
|
-
/** The path to the test environment directory, which is the blueprint for
|
|
61
|
-
* the test directory.
|
|
62
|
-
* @example /Users/mikavilpas/git/tui-sandbox/packages/integration-tests/test-environment
|
|
63
|
-
* */
|
|
64
|
-
TUI_SANDBOX_TEST_ENVIRONMENT_PATH: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export type { StartNeovimGenericArguments } from "./applications/neovim/NeovimApplication.js"
|
|
68
|
-
|
|
69
|
-
export type BlockingShellCommandOutput =
|
|
70
|
-
| {
|
|
71
|
-
type: "success"
|
|
72
|
-
stdout: string
|
|
73
|
-
stderr: string
|
|
74
|
-
}
|
|
75
|
-
| {
|
|
76
|
-
type: "failed"
|
|
77
|
-
// for now we log the error to the server's console output. It will be
|
|
78
|
-
// visible when running the tests.
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type RunLuaCodeOutput = {
|
|
82
|
-
value?: VimValue
|
|
83
|
-
// to catch errors, use pcall() in the Lua code
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type RunExCommandOutput = { value?: string }
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Require all of an object's keys to be set explicitly. This is useful for
|
|
90
|
-
* readability: writing all the keys makes their presence explicit. They could
|
|
91
|
-
* also be added via a spread operator (...obj), but in that case the following
|
|
92
|
-
* cases are unclear:
|
|
93
|
-
*
|
|
94
|
-
* - extra keys might be included because TypeScript does not check for them
|
|
95
|
-
* - if the target object type has optional keys, they might be missing. In
|
|
96
|
-
* this case, they will never get set
|
|
97
|
-
**/
|
|
98
|
-
export type AllKeys<T extends Record<never, never>> = Record<keyof T, unknown>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "fs"
|
|
2
|
-
import type { PartialDeep } from "type-fest"
|
|
3
|
-
import { describe, expect, it, vi } from "vitest"
|
|
4
|
-
import { buildTestDirectorySchema } from "./dirtree/index.js"
|
|
5
|
-
import type { TestServerConfig, UpdateTestdirectorySchemaFileResult } from "./updateTestdirectorySchemaFile.js"
|
|
6
|
-
import { updateTestdirectorySchemaFile } from "./updateTestdirectorySchemaFile.js"
|
|
7
|
-
|
|
8
|
-
vi.mock("fs")
|
|
9
|
-
vi.mock("./dirtree")
|
|
10
|
-
vi.spyOn(console, "log").mockImplementation(vi.fn())
|
|
11
|
-
|
|
12
|
-
const mock = {
|
|
13
|
-
readFileSync: vi.mocked(readFileSync),
|
|
14
|
-
writeFileSync: vi.mocked(writeFileSync),
|
|
15
|
-
buildTestDirectorySchema: vi.mocked(buildTestDirectorySchema),
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("when the schema has not changed", () => {
|
|
19
|
-
it("does not write the file", async () => {
|
|
20
|
-
mock.buildTestDirectorySchema.mockResolvedValue("schema")
|
|
21
|
-
mock.readFileSync.mockImplementation(() => "schema")
|
|
22
|
-
|
|
23
|
-
const result = await updateTestdirectorySchemaFile({
|
|
24
|
-
directories: {
|
|
25
|
-
testEnvironmentPath: "path",
|
|
26
|
-
outputFilePath: "path",
|
|
27
|
-
},
|
|
28
|
-
} satisfies PartialDeep<TestServerConfig> as TestServerConfig)
|
|
29
|
-
|
|
30
|
-
expect(result).toBe("did-nothing" satisfies UpdateTestdirectorySchemaFileResult)
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe("when the schema has changed", () => {
|
|
35
|
-
it("writes the file", async () => {
|
|
36
|
-
mock.buildTestDirectorySchema.mockResolvedValue("new schema")
|
|
37
|
-
mock.readFileSync.mockImplementation(() => "old schema")
|
|
38
|
-
|
|
39
|
-
const result = await updateTestdirectorySchemaFile({
|
|
40
|
-
directories: {
|
|
41
|
-
testEnvironmentPath: "path",
|
|
42
|
-
outputFilePath: "path",
|
|
43
|
-
},
|
|
44
|
-
} satisfies PartialDeep<TestServerConfig> as TestServerConfig)
|
|
45
|
-
|
|
46
|
-
expect(result).toBe("updated" satisfies UpdateTestdirectorySchemaFileResult)
|
|
47
|
-
expect(mock.writeFileSync).toHaveBeenCalledWith("path", "new schema")
|
|
48
|
-
})
|
|
49
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "fs"
|
|
2
|
-
import { debuglog } from "util"
|
|
3
|
-
import * as z from "zod"
|
|
4
|
-
import { buildTestDirectorySchema } from "./dirtree/index.js"
|
|
5
|
-
|
|
6
|
-
const log = debuglog(`tui-sandbox.${updateTestdirectorySchemaFile.name}`)
|
|
7
|
-
|
|
8
|
-
export type DirectoriesConfig = TestServerConfig["directories"]
|
|
9
|
-
|
|
10
|
-
export type TestServerConfig = z.output<typeof testServerConfigSchema>
|
|
11
|
-
|
|
12
|
-
export type TestServerConfigMetadata = {
|
|
13
|
-
configFilePath: string
|
|
14
|
-
config: TestServerConfig
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type Dictionary = Record<string, string>
|
|
18
|
-
|
|
19
|
-
export type CustomizeEnv = (env: Dictionary) => Promise<Dictionary>
|
|
20
|
-
|
|
21
|
-
export type CustomizeMiseEnvironmentVariables = (
|
|
22
|
-
env: Dictionary,
|
|
23
|
-
defaultImplementation: CustomizeEnv
|
|
24
|
-
) => Promise<Record<string, string>>
|
|
25
|
-
|
|
26
|
-
export type NeovimIntegrationDefaultAppName = "nvim"
|
|
27
|
-
const neovimIntegration = z.strictObject({
|
|
28
|
-
NVIM_APPNAMEs: z
|
|
29
|
-
.array(z.string())
|
|
30
|
-
.min(1)
|
|
31
|
-
.default(["nvim" satisfies NeovimIntegrationDefaultAppName]),
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
export type NeovimIntegrationConfig = z.output<typeof neovimIntegration>
|
|
35
|
-
|
|
36
|
-
export const testServerConfigSchema = z.strictObject({
|
|
37
|
-
directories: z.object({
|
|
38
|
-
testEnvironmentPath: z.string(),
|
|
39
|
-
outputFilePath: z.string(),
|
|
40
|
-
latestSymlinkName: z.string().optional().default("latest"),
|
|
41
|
-
}),
|
|
42
|
-
port: z.number().int().min(1).max(65535),
|
|
43
|
-
integrations: z.strictObject({
|
|
44
|
-
neovim: neovimIntegration,
|
|
45
|
-
}),
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
export type UpdateTestdirectorySchemaFileResult = "updated" | "did-nothing"
|
|
49
|
-
|
|
50
|
-
export async function updateTestdirectorySchemaFile(
|
|
51
|
-
config: TestServerConfig
|
|
52
|
-
): Promise<UpdateTestdirectorySchemaFileResult> {
|
|
53
|
-
const newSchema: string = await buildTestDirectorySchema(config)
|
|
54
|
-
let oldSchema = ""
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
oldSchema = readFileSync(config.directories.outputFilePath, "utf-8")
|
|
58
|
-
} catch {
|
|
59
|
-
log("No existing schema file found, creating a new one")
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (oldSchema !== newSchema) {
|
|
63
|
-
// it's important to not write the file if the schema hasn't changed
|
|
64
|
-
// because file watchers will trigger on file changes and we don't want to
|
|
65
|
-
// trigger a build if the schema hasn't changed
|
|
66
|
-
writeFileSync(config.directories.outputFilePath, newSchema)
|
|
67
|
-
return "updated"
|
|
68
|
-
} else {
|
|
69
|
-
return "did-nothing"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest"
|
|
2
|
-
import type { StartableApplication } from "./DisposableSingleApplication.js"
|
|
3
|
-
import { DisposableSingleApplication } from "./DisposableSingleApplication.js"
|
|
4
|
-
import type { ExitInfo } from "./TerminalApplication.js"
|
|
5
|
-
|
|
6
|
-
vi.spyOn(console, "log").mockImplementation(vi.fn())
|
|
7
|
-
|
|
8
|
-
class TestDisposableSingleApplication extends DisposableSingleApplication {
|
|
9
|
-
public getApplication() {
|
|
10
|
-
return this.application
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const fakeApp = {
|
|
15
|
-
processId: 123,
|
|
16
|
-
write: vi.fn(),
|
|
17
|
-
killAndWait: vi.fn(),
|
|
18
|
-
untilExit: Promise.resolve<ExitInfo>({ exitCode: 0, signal: undefined }),
|
|
19
|
-
} satisfies StartableApplication
|
|
20
|
-
|
|
21
|
-
describe("DisposableSingleApplication", () => {
|
|
22
|
-
it("has no application when created", () => {
|
|
23
|
-
const app = new TestDisposableSingleApplication()
|
|
24
|
-
expect(app.processId()).toBeUndefined()
|
|
25
|
-
expect(app.getApplication()).toBeUndefined()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it("can start an application", async () => {
|
|
29
|
-
const app = new TestDisposableSingleApplication()
|
|
30
|
-
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
31
|
-
expect(app.processId()).toBe(123)
|
|
32
|
-
expect(app.getApplication()).toBe(fakeApp)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it("can write to the application", async () => {
|
|
36
|
-
const app = new TestDisposableSingleApplication()
|
|
37
|
-
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
38
|
-
await app.write("hello")
|
|
39
|
-
expect(fakeApp.write).toHaveBeenCalledWith("hello")
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it("fails to write if the application is not started", async () => {
|
|
43
|
-
// there is no need to support soft failing in the ui, so we do hard
|
|
44
|
-
// failing to make this error obvious
|
|
45
|
-
const app = new TestDisposableSingleApplication()
|
|
46
|
-
await expect(app.write("hello")).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
47
|
-
`[AssertionError: The application not started yet. It makes no sense to write to it, so this looks like a bug.]`
|
|
48
|
-
)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe("untilExit allows waiting for the application to exit", () => {
|
|
52
|
-
it("successful exit works", async () => {
|
|
53
|
-
const app = new TestDisposableSingleApplication()
|
|
54
|
-
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
55
|
-
fakeApp.untilExit = Promise.resolve({ exitCode: 1, signal: 9 })
|
|
56
|
-
await expect(app.untilExit()).resolves.toStrictEqual({
|
|
57
|
-
exitCode: 1,
|
|
58
|
-
signal: 9,
|
|
59
|
-
} satisfies ExitInfo)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it("when the application throws an error, the error is propagated", async () => {
|
|
63
|
-
const app = new TestDisposableSingleApplication()
|
|
64
|
-
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
65
|
-
fakeApp.untilExit = Promise.reject(new Error("fake error"))
|
|
66
|
-
await expect(app.untilExit()).rejects.toThrowError(new Error("fake error"))
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe("disposing", () => {
|
|
71
|
-
it("disposes the application when disposed", async () => {
|
|
72
|
-
// it's important to make sure there are no dangling applications when
|
|
73
|
-
// starting new tests or ending the user session entirely and closing the
|
|
74
|
-
// application
|
|
75
|
-
const app = new TestDisposableSingleApplication()
|
|
76
|
-
|
|
77
|
-
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
78
|
-
expect(app.getApplication()).toBe(fakeApp)
|
|
79
|
-
|
|
80
|
-
await app[Symbol.asyncDispose]()
|
|
81
|
-
expect(fakeApp.killAndWait).toHaveBeenCalledOnce()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it("does nothing if there is no application to dispose", async () => {
|
|
85
|
-
const app = new TestDisposableSingleApplication()
|
|
86
|
-
expect(app.getApplication()).toBeUndefined()
|
|
87
|
-
expect(app.processId()).toBeUndefined()
|
|
88
|
-
|
|
89
|
-
expect(() => app[Symbol.asyncDispose]()).not.toThrow()
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import assert from "assert"
|
|
2
|
-
import { debuglog } from "util"
|
|
3
|
-
import type { ExitInfo, TerminalApplication } from "./TerminalApplication.js"
|
|
4
|
-
|
|
5
|
-
export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait" | "untilExit">
|
|
6
|
-
|
|
7
|
-
const log = debuglog("tui-sandbox.DisposableSingleApplication")
|
|
8
|
-
|
|
9
|
-
/** A testable application that can be started, killed, and given input. For a
|
|
10
|
-
* single instance of this interface, only a single instance can be running at
|
|
11
|
-
* a time.
|
|
12
|
-
*/
|
|
13
|
-
export class DisposableSingleApplication implements AsyncDisposable {
|
|
14
|
-
protected application: StartableApplication | undefined
|
|
15
|
-
|
|
16
|
-
public async startNextAndKillCurrent(startNext: () => Promise<StartableApplication>): Promise<void> {
|
|
17
|
-
await this[Symbol.asyncDispose]()
|
|
18
|
-
this.application = await startNext()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
public async untilExit(): Promise<ExitInfo> {
|
|
22
|
-
assert(
|
|
23
|
-
this.application,
|
|
24
|
-
"The application not started yet. It makes no sense to wait for it to exit, so this looks like a bug."
|
|
25
|
-
)
|
|
26
|
-
return this.application.untilExit
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public async write(input: string): Promise<void> {
|
|
30
|
-
assert(
|
|
31
|
-
this.application,
|
|
32
|
-
"The application not started yet. It makes no sense to write to it, so this looks like a bug."
|
|
33
|
-
)
|
|
34
|
-
this.application.write(input)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public processId(): number | undefined {
|
|
38
|
-
return this.application?.processId
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Kill the current application if it exists. */
|
|
42
|
-
public async [Symbol.asyncDispose](): Promise<void> {
|
|
43
|
-
if (this.processId() === undefined) {
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
log(`Killing current application ${this.processId()}...`)
|
|
47
|
-
await this.application?.killAndWait()
|
|
48
|
-
}
|
|
49
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export class Lazy<T> {
|
|
2
|
-
private value?: T
|
|
3
|
-
|
|
4
|
-
constructor(private readonly factory: () => T) {}
|
|
5
|
-
|
|
6
|
-
get(): T {
|
|
7
|
-
if (this.value === undefined) {
|
|
8
|
-
this.value = this.factory()
|
|
9
|
-
}
|
|
10
|
-
return this.value
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
set(value: T): void {
|
|
14
|
-
this.value = value
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type winston from "winston"
|
|
2
|
-
import { createLogger, format, transports } from "winston"
|
|
3
|
-
|
|
4
|
-
import type { ITerminalDimensions } from "@xterm/addon-fit"
|
|
5
|
-
import type { IPty } from "node-pty"
|
|
6
|
-
import pty from "node-pty"
|
|
7
|
-
import { debuglog } from "util"
|
|
8
|
-
import type { StartableApplication } from "./DisposableSingleApplication.js"
|
|
9
|
-
|
|
10
|
-
const log = debuglog("tui-sandbox.TerminalApplication")
|
|
11
|
-
|
|
12
|
-
export type ExitInfo = { exitCode: number; signal: number | undefined }
|
|
13
|
-
|
|
14
|
-
// NOTE separating stdout and stderr is not supported by node-pty
|
|
15
|
-
// https://github.com/microsoft/node-pty/issues/71
|
|
16
|
-
export class TerminalApplication implements StartableApplication {
|
|
17
|
-
public readonly processId: number
|
|
18
|
-
|
|
19
|
-
public readonly logger: winston.Logger
|
|
20
|
-
|
|
21
|
-
private constructor(
|
|
22
|
-
private readonly subProcess: IPty,
|
|
23
|
-
public readonly onStdoutOrStderr: (data: string) => void,
|
|
24
|
-
public readonly untilExit: Promise<ExitInfo>,
|
|
25
|
-
public readonly name: string
|
|
26
|
-
) {
|
|
27
|
-
this.processId = subProcess.pid
|
|
28
|
-
|
|
29
|
-
this.logger = createLogger({
|
|
30
|
-
transports: [new transports.Console()],
|
|
31
|
-
defaultMeta: { pid: this.processId },
|
|
32
|
-
format: format.combine(format.colorize(), format.cli()),
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
this.logger.debug(`started`)
|
|
36
|
-
|
|
37
|
-
subProcess.onExit(({ exitCode, signal }) => {
|
|
38
|
-
signal satisfies number | undefined
|
|
39
|
-
const msg = `Child process ${this.processId} (${this.name}) exited with code ${String(exitCode)} and signal ${String(signal)}`
|
|
40
|
-
this.onStdoutOrStderr(msg)
|
|
41
|
-
this.logger.debug(msg)
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** @constructor Start a new terminal application. */
|
|
46
|
-
public static start({
|
|
47
|
-
onStdoutOrStderr,
|
|
48
|
-
command,
|
|
49
|
-
args,
|
|
50
|
-
cwd,
|
|
51
|
-
env,
|
|
52
|
-
dimensions,
|
|
53
|
-
}: {
|
|
54
|
-
onStdoutOrStderr: (data: string) => void
|
|
55
|
-
command: string
|
|
56
|
-
args: string[]
|
|
57
|
-
cwd: string
|
|
58
|
-
env?: NodeJS.ProcessEnv
|
|
59
|
-
dimensions: ITerminalDimensions
|
|
60
|
-
}): TerminalApplication {
|
|
61
|
-
log(`Starting '${command}' with args '${args.join(" ")}' in cwd '${cwd}'`)
|
|
62
|
-
|
|
63
|
-
const ptyProcess = pty.spawn(command, args, {
|
|
64
|
-
name: "xterm-color",
|
|
65
|
-
cwd,
|
|
66
|
-
env,
|
|
67
|
-
cols: dimensions.cols,
|
|
68
|
-
rows: dimensions.rows,
|
|
69
|
-
})
|
|
70
|
-
ptyProcess.onData(onStdoutOrStderr)
|
|
71
|
-
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
72
|
-
log(`Child process exited with code ${exitCode} and signal ${signal}`)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const processId = ptyProcess.pid
|
|
76
|
-
|
|
77
|
-
if (!processId) {
|
|
78
|
-
throw new Error("Failed to spawn child process")
|
|
79
|
-
}
|
|
80
|
-
const untilExit = new Promise<ExitInfo>(resolve => {
|
|
81
|
-
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
82
|
-
// console.log(`Child process ${processId} exited with code ${exitCode} and signal ${signal}`)
|
|
83
|
-
resolve({ exitCode, signal })
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
return new TerminalApplication(ptyProcess, onStdoutOrStderr, untilExit, ptyProcess.process satisfies string)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Write to the terminal's stdin. */
|
|
91
|
-
public write(data: string): void {
|
|
92
|
-
this.subProcess.write(data)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
public async killAndWait(): Promise<void> {
|
|
96
|
-
log(`💣 Killing process ${this.processId}`)
|
|
97
|
-
this.subProcess.kill()
|
|
98
|
-
log(`💥 Killed process ${this.processId}`)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "stream"
|
|
2
|
-
import { describe, expect, it } from "vitest"
|
|
3
|
-
import { convertEventEmitterToAsyncGenerator } from "./generator.js"
|
|
4
|
-
|
|
5
|
-
describe("when a listener is attached", () => {
|
|
6
|
-
it("forwards events from an EventEmitter to an async generator", async () => {
|
|
7
|
-
const eventEmitter = new EventEmitter()
|
|
8
|
-
const generator = convertEventEmitterToAsyncGenerator(eventEmitter, "test")
|
|
9
|
-
|
|
10
|
-
{
|
|
11
|
-
const promise = generator.next()
|
|
12
|
-
eventEmitter.emit("test", "message")
|
|
13
|
-
|
|
14
|
-
expect(await promise).toMatchInlineSnapshot(`
|
|
15
|
-
{
|
|
16
|
-
"done": false,
|
|
17
|
-
"value": "message",
|
|
18
|
-
}
|
|
19
|
-
`)
|
|
20
|
-
|
|
21
|
-
const promise2 = generator.next()
|
|
22
|
-
eventEmitter.emit("test", "message2")
|
|
23
|
-
|
|
24
|
-
expect(await promise2).toMatchInlineSnapshot(`
|
|
25
|
-
{
|
|
26
|
-
"done": false,
|
|
27
|
-
"value": "message2",
|
|
28
|
-
}
|
|
29
|
-
`)
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe("when no listener is attached, messages are lost permanently", () => {
|
|
35
|
-
it("does not resend lost events after a subscriber attaches", async () => {
|
|
36
|
-
const eventEmitter = new EventEmitter()
|
|
37
|
-
eventEmitter.emit("test", "this message should be lost")
|
|
38
|
-
|
|
39
|
-
const generator = convertEventEmitterToAsyncGenerator(eventEmitter, "test")
|
|
40
|
-
const promise = generator.next()
|
|
41
|
-
eventEmitter.emit("test", "new message that will be received")
|
|
42
|
-
|
|
43
|
-
expect(await promise).toMatchInlineSnapshot(`
|
|
44
|
-
{
|
|
45
|
-
"done": false,
|
|
46
|
-
"value": "new message that will be received",
|
|
47
|
-
}
|
|
48
|
-
`)
|
|
49
|
-
})
|
|
50
|
-
})
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type EventEmitter from "events"
|
|
2
|
-
|
|
3
|
-
export async function* convertEventEmitterToAsyncGenerator(
|
|
4
|
-
emitter: EventEmitter,
|
|
5
|
-
eventName: string
|
|
6
|
-
): AsyncGenerator<string, void, unknown> {
|
|
7
|
-
while (true) {
|
|
8
|
-
yield await new Promise(resolve => {
|
|
9
|
-
emitter.once(eventName, resolve)
|
|
10
|
-
})
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert"
|
|
2
|
-
|
|
3
|
-
export async function timeoutable<T>(timeoutMs: number, promise: Promise<T>): Promise<T> {
|
|
4
|
-
let timeoutHandle: NodeJS.Timeout
|
|
5
|
-
|
|
6
|
-
const timeoutPromise = new Promise<T>((_, reject) => {
|
|
7
|
-
timeoutHandle = setTimeout(() => {
|
|
8
|
-
reject(new Error(`Operation timed out after ${timeoutMs}ms`))
|
|
9
|
-
}, timeoutMs)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
return await Promise.race([promise, timeoutPromise])
|
|
14
|
-
} finally {
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
16
|
-
assert(timeoutHandle!)
|
|
17
|
-
clearTimeout(timeoutHandle)
|
|
18
|
-
}
|
|
19
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"skipLibCheck": true,
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"sourceMap": true,
|
|
6
|
-
"noImplicitOverride": true,
|
|
7
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"moduleResolution": "NodeNext",
|
|
10
|
-
"target": "ES2022",
|
|
11
|
-
"isolatedModules": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"allowJs": true,
|
|
14
|
-
"outDir": "dist",
|
|
15
|
-
"lib": ["ES2023", "ESNext.Disposable"],
|
|
16
|
-
"types": ["node", "cypress"],
|
|
17
|
-
"composite": true,
|
|
18
|
-
|
|
19
|
-
/* Linting */
|
|
20
|
-
"strict": true,
|
|
21
|
-
"noUnusedLocals": false,
|
|
22
|
-
"noUnusedParameters": true,
|
|
23
|
-
"noFallthroughCasesInSwitch": true
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
"include": [
|
|
27
|
-
//
|
|
28
|
-
"./src/",
|
|
29
|
-
"src/scripts/tui.ts"
|
|
30
|
-
]
|
|
31
|
-
}
|
package/vite.config.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vite"
|
|
2
|
-
|
|
3
|
-
// https://vitejs.dev/config/
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
server: {
|
|
6
|
-
host: "127.0.0.1",
|
|
7
|
-
https: false,
|
|
8
|
-
strictPort: true,
|
|
9
|
-
proxy: {
|
|
10
|
-
// inside @tui-sandbox/library, a vite development server can be used. It
|
|
11
|
-
// will proxy requests to the backend.
|
|
12
|
-
//
|
|
13
|
-
// When running in production, the backend starts its own static file
|
|
14
|
-
// server that is used (in development, it's just ignored).
|
|
15
|
-
"/trpc": "http://localhost:3000",
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
build: {
|
|
19
|
-
outDir: "./dist/browser/",
|
|
20
|
-
},
|
|
21
|
-
test: {
|
|
22
|
-
exclude: ["node_modules", "dist"],
|
|
23
|
-
globals: true, // This will make describe, it, etc. available globally
|
|
24
|
-
environment: "node",
|
|
25
|
-
mockReset: true,
|
|
26
|
-
},
|
|
27
|
-
})
|