@tui-sandbox/library 1.0.0 → 1.0.2
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/src/client/validateMouseEvent.d.ts +1 -0
- package/dist/src/client/validateMouseEvent.js +13 -0
- package/dist/src/client/websocket-client.d.ts +25 -0
- package/dist/src/client/websocket-client.js +127 -0
- package/dist/src/server/connection/trpc.d.ts +44 -0
- package/dist/src/server/connection/trpc.js +5 -0
- package/dist/src/server/dirtree/index.d.ts +25 -0
- package/dist/src/server/dirtree/index.js +80 -0
- package/dist/src/server/dirtree/index.test.d.ts +1 -0
- package/dist/src/server/dirtree/index.test.js +172 -0
- package/dist/src/server/dirtree/json-to-zod.d.ts +1 -0
- package/dist/src/server/dirtree/json-to-zod.js +56 -0
- package/dist/src/server/index.d.ts +8 -0
- package/dist/src/server/index.js +49 -0
- package/dist/src/server/neovim/NeovimApplication.d.ts +25 -0
- package/dist/src/server/neovim/NeovimApplication.js +111 -0
- package/dist/src/server/neovim/environment/createTempDir.d.ts +3 -0
- package/dist/src/server/neovim/environment/createTempDir.js +41 -0
- package/dist/src/server/neovim/index.d.ts +13 -0
- package/dist/src/server/neovim/index.js +40 -0
- package/dist/src/server/server.d.ts +59 -0
- package/dist/src/server/server.js +55 -0
- package/dist/src/server/types.d.ts +13 -0
- package/dist/src/server/types.js +1 -0
- package/dist/src/server/updateTestdirectorySchemaFile.d.ts +6 -0
- package/dist/src/server/updateTestdirectorySchemaFile.js +19 -0
- package/dist/src/server/utilities/DisposableSingleApplication.d.ts +16 -0
- package/dist/src/server/utilities/DisposableSingleApplication.js +27 -0
- package/dist/src/server/utilities/Lazy.d.ts +6 -0
- package/dist/src/server/utilities/Lazy.js +13 -0
- package/dist/src/server/utilities/TerminalApplication.d.ts +21 -0
- package/dist/src/server/utilities/TerminalApplication.js +54 -0
- package/dist/src/server/utilities/tabId.d.ts +9 -0
- package/dist/src/server/utilities/tabId.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -2
- package/src/server/updateTestdirectorySchemaFile.ts +7 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateMouseEvent(data: string): string | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Function to parse mouse events
|
|
2
|
+
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Button-event-tracking
|
|
3
|
+
export function validateMouseEvent(data) {
|
|
4
|
+
const match = /\x1b\[<(\d+);(\d+);(\d+)([mM])/.exec(data);
|
|
5
|
+
if (match) {
|
|
6
|
+
const buttonCode = parseInt(match[1], 10);
|
|
7
|
+
const column = parseInt(match[2], 10);
|
|
8
|
+
const row = parseInt(match[3], 10);
|
|
9
|
+
const isRelease = match[4] === "m";
|
|
10
|
+
console.log(`Mouse event: buttonCode=${buttonCode}, column=${column}, row=${row}, isRelease=${isRelease}`);
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Terminal } from "@xterm/xterm";
|
|
2
|
+
import "@xterm/xterm/css/xterm.css";
|
|
3
|
+
import type { StartNeovimGenericArguments } from "../server/neovim/NeovimApplication.ts";
|
|
4
|
+
import type { TestDirectory } from "../server/types.ts";
|
|
5
|
+
import type { TabId } from "../server/utilities/tabId.ts";
|
|
6
|
+
import "./style.css";
|
|
7
|
+
export type StartTerminalOptions = {
|
|
8
|
+
onMouseEvent: (data: string) => void;
|
|
9
|
+
onKeyPress: (event: {
|
|
10
|
+
key: string;
|
|
11
|
+
domEvent: KeyboardEvent;
|
|
12
|
+
}) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function startTerminal(app: HTMLElement, options: StartTerminalOptions): Terminal;
|
|
15
|
+
/** An identifier unique to a browser tab, so that each tab can have its own
|
|
16
|
+
* unique session that persists across page reloads. */
|
|
17
|
+
export declare function getTabId(): TabId;
|
|
18
|
+
export declare class NeovimClient {
|
|
19
|
+
private readonly ready;
|
|
20
|
+
private readonly tabId;
|
|
21
|
+
private readonly terminal;
|
|
22
|
+
private readonly trpc;
|
|
23
|
+
constructor(app: HTMLElement);
|
|
24
|
+
startNeovim(args: StartNeovimGenericArguments): Promise<TestDirectory>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { flavors } from "@catppuccin/palette";
|
|
2
|
+
import { createTRPCClient, createWSClient, wsLink } from "@trpc/client";
|
|
3
|
+
import { FitAddon } from "@xterm/addon-fit";
|
|
4
|
+
import { Terminal } from "@xterm/xterm";
|
|
5
|
+
import "@xterm/xterm/css/xterm.css";
|
|
6
|
+
import z from "zod";
|
|
7
|
+
import "./style.css";
|
|
8
|
+
import { validateMouseEvent } from "./validateMouseEvent";
|
|
9
|
+
export function startTerminal(app, options) {
|
|
10
|
+
const terminal = new Terminal({
|
|
11
|
+
cursorBlink: false,
|
|
12
|
+
convertEol: true,
|
|
13
|
+
fontSize: 13,
|
|
14
|
+
});
|
|
15
|
+
const colors = flavors.macchiato.colors;
|
|
16
|
+
terminal.options.theme = {
|
|
17
|
+
background: colors.base.hex,
|
|
18
|
+
black: colors.crust.hex,
|
|
19
|
+
brightBlack: colors.surface2.hex,
|
|
20
|
+
blue: colors.blue.hex,
|
|
21
|
+
brightBlue: colors.blue.hex,
|
|
22
|
+
brightCyan: colors.sky.hex,
|
|
23
|
+
brightRed: colors.maroon.hex,
|
|
24
|
+
brightYellow: colors.yellow.hex,
|
|
25
|
+
cursor: colors.text.hex,
|
|
26
|
+
cyan: colors.sky.hex,
|
|
27
|
+
foreground: colors.text.hex,
|
|
28
|
+
green: colors.green.hex,
|
|
29
|
+
magenta: colors.lavender.hex,
|
|
30
|
+
red: colors.red.hex,
|
|
31
|
+
white: colors.text.hex,
|
|
32
|
+
yellow: colors.yellow.hex,
|
|
33
|
+
};
|
|
34
|
+
// The FitAddon makes the terminal fit the size of the container, the entire
|
|
35
|
+
// page in this case
|
|
36
|
+
const fitAddon = new FitAddon();
|
|
37
|
+
terminal.loadAddon(fitAddon);
|
|
38
|
+
terminal.open(app);
|
|
39
|
+
fitAddon.fit();
|
|
40
|
+
window.addEventListener("resize", () => {
|
|
41
|
+
fitAddon.fit();
|
|
42
|
+
});
|
|
43
|
+
terminal.onData(data => {
|
|
44
|
+
data;
|
|
45
|
+
// Send mouse clicks to the terminal application
|
|
46
|
+
//
|
|
47
|
+
// this gets called for mouse events. However, some mouse events seem to
|
|
48
|
+
// confuse Neovim, so for now let's just send click events
|
|
49
|
+
if (typeof data !== "string") {
|
|
50
|
+
throw new Error(`unexpected onData message type: '${JSON.stringify(data)}'`);
|
|
51
|
+
}
|
|
52
|
+
const mouseEvent = validateMouseEvent(data);
|
|
53
|
+
if (mouseEvent) {
|
|
54
|
+
options.onMouseEvent(mouseEvent);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
terminal.onKey(event => {
|
|
58
|
+
options.onKeyPress(event);
|
|
59
|
+
});
|
|
60
|
+
return terminal;
|
|
61
|
+
}
|
|
62
|
+
/** An identifier unique to a browser tab, so that each tab can have its own
|
|
63
|
+
* unique session that persists across page reloads. */
|
|
64
|
+
export function getTabId() {
|
|
65
|
+
// Other tabs will have a different id because sessionStorage is unique to
|
|
66
|
+
// each tab.
|
|
67
|
+
let tabId = z.string().safeParse(sessionStorage.getItem("tabId")).data;
|
|
68
|
+
if (!tabId) {
|
|
69
|
+
tabId = Math.random().toString(36);
|
|
70
|
+
sessionStorage.setItem("tabId", tabId);
|
|
71
|
+
}
|
|
72
|
+
return { tabId };
|
|
73
|
+
}
|
|
74
|
+
export class NeovimClient {
|
|
75
|
+
ready;
|
|
76
|
+
tabId;
|
|
77
|
+
terminal;
|
|
78
|
+
trpc;
|
|
79
|
+
constructor(app) {
|
|
80
|
+
const wsClient = createWSClient({ url: `ws://localhost:3000`, WebSocket });
|
|
81
|
+
const trpc = createTRPCClient({
|
|
82
|
+
links: [wsLink({ client: wsClient })],
|
|
83
|
+
});
|
|
84
|
+
this.trpc = trpc;
|
|
85
|
+
this.tabId = getTabId();
|
|
86
|
+
const tabId = this.tabId;
|
|
87
|
+
const terminal = startTerminal(app, {
|
|
88
|
+
onMouseEvent(data) {
|
|
89
|
+
void trpc.neovim.sendStdin.mutate({ tabId, data }).catch((error) => {
|
|
90
|
+
console.error(`Error sending mouse event`, error);
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
onKeyPress(event) {
|
|
94
|
+
void trpc.neovim.sendStdin.mutate({ tabId, data: event.key });
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
this.terminal = terminal;
|
|
98
|
+
// start listening to Neovim stdout - this will take some (short) amount of
|
|
99
|
+
// time to complete
|
|
100
|
+
this.ready = new Promise(resolve => {
|
|
101
|
+
console.log("Subscribing to Neovim stdout");
|
|
102
|
+
trpc.neovim.onStdout.subscribe({ client: tabId }, {
|
|
103
|
+
onStarted() {
|
|
104
|
+
resolve();
|
|
105
|
+
},
|
|
106
|
+
onData(data) {
|
|
107
|
+
terminal.write(data);
|
|
108
|
+
},
|
|
109
|
+
onError(err) {
|
|
110
|
+
console.error(`Error from Neovim`, err);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async startNeovim(args) {
|
|
116
|
+
await this.ready;
|
|
117
|
+
const neovim = await this.trpc.neovim.start.mutate({
|
|
118
|
+
startNeovimArguments: args,
|
|
119
|
+
tabId: this.tabId,
|
|
120
|
+
terminalDimensions: {
|
|
121
|
+
cols: this.terminal.cols,
|
|
122
|
+
rows: this.terminal.rows,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return neovim;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CreateWSSContextFnOptions } from "@trpc/server/adapters/ws";
|
|
2
|
+
import type { Socket } from "net";
|
|
3
|
+
import type { WebSocket } from "ws";
|
|
4
|
+
export type Connection = {
|
|
5
|
+
clientId: WebSocket;
|
|
6
|
+
socket: Socket;
|
|
7
|
+
};
|
|
8
|
+
export declare function createContext(opts: CreateWSSContextFnOptions): Connection;
|
|
9
|
+
export declare const trpc: {
|
|
10
|
+
_config: import("@trpc/server/unstable-core-do-not-import").RootConfig<{
|
|
11
|
+
ctx: Connection;
|
|
12
|
+
meta: object;
|
|
13
|
+
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
14
|
+
transformer: false;
|
|
15
|
+
}>;
|
|
16
|
+
procedure: import("@trpc/server/unstable-core-do-not-import").ProcedureBuilder<Connection, object, object, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, false>;
|
|
17
|
+
middleware: <$ContextOverrides>(fn: import("@trpc/server/unstable-core-do-not-import").MiddlewareFunction<Connection, object, object, $ContextOverrides, unknown>) => import("@trpc/server/unstable-core-do-not-import").MiddlewareBuilder<Connection, object, $ContextOverrides, unknown>;
|
|
18
|
+
router: {
|
|
19
|
+
<TInput extends import("@trpc/server").RouterRecord>(input: TInput): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
|
|
20
|
+
ctx: Connection;
|
|
21
|
+
meta: object;
|
|
22
|
+
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
23
|
+
transformer: false;
|
|
24
|
+
}, TInput>;
|
|
25
|
+
<TInput extends import("@trpc/server/unstable-core-do-not-import").CreateRouterOptions>(input: TInput): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
|
|
26
|
+
ctx: Connection;
|
|
27
|
+
meta: object;
|
|
28
|
+
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
29
|
+
transformer: false;
|
|
30
|
+
}, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<TInput>>;
|
|
31
|
+
};
|
|
32
|
+
mergeRouters: typeof import("@trpc/server/unstable-core-do-not-import").mergeRouters;
|
|
33
|
+
createCallerFactory: <TRecord extends import("@trpc/server").RouterRecord>(router: Pick<import("@trpc/server/unstable-core-do-not-import").Router<{
|
|
34
|
+
ctx: Connection;
|
|
35
|
+
meta: object;
|
|
36
|
+
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
37
|
+
transformer: false;
|
|
38
|
+
}, TRecord>, "_def">) => import("@trpc/server/unstable-core-do-not-import").RouterCaller<{
|
|
39
|
+
ctx: Connection;
|
|
40
|
+
meta: object;
|
|
41
|
+
errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
|
|
42
|
+
transformer: false;
|
|
43
|
+
}, TRecord>;
|
|
44
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Dree } from "dree";
|
|
2
|
+
import { Type } from "dree";
|
|
3
|
+
type TreeResult = {
|
|
4
|
+
dree: Dree;
|
|
5
|
+
allFiles: Dree[];
|
|
6
|
+
};
|
|
7
|
+
/** Convert a directory tree to a TypeScript type. This is useful for testing
|
|
8
|
+
* as the initial state of the test directory is fully known in tests. */
|
|
9
|
+
export declare function getDirectoryTree(path: string): TreeResult;
|
|
10
|
+
type FileNode = {
|
|
11
|
+
type: Type.FILE;
|
|
12
|
+
name: string;
|
|
13
|
+
extension: string | undefined;
|
|
14
|
+
stem: string;
|
|
15
|
+
};
|
|
16
|
+
type DirectoryNode = {
|
|
17
|
+
type: Type.DIRECTORY;
|
|
18
|
+
name: string;
|
|
19
|
+
contents: Record<string, TreeNode>;
|
|
20
|
+
};
|
|
21
|
+
type TreeNode = FileNode | DirectoryNode;
|
|
22
|
+
export declare function convertDree(root: Dree): TreeNode;
|
|
23
|
+
export declare function buildSchemaForDirectoryTree(result: TreeResult, name: string): Promise<string>;
|
|
24
|
+
export declare function buildTestDirectorySchema(testDirectoryPath: string): Promise<string>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { scan, Type } from "dree";
|
|
3
|
+
import { format, resolveConfig } from "prettier";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { jsonToZod } from "./json-to-zod";
|
|
6
|
+
/** Convert a directory tree to a TypeScript type. This is useful for testing
|
|
7
|
+
* as the initial state of the test directory is fully known in tests. */
|
|
8
|
+
export function getDirectoryTree(path) {
|
|
9
|
+
const allFiles = [];
|
|
10
|
+
const result = scan(path, {
|
|
11
|
+
exclude: [/.repro/, /testdirs/],
|
|
12
|
+
hash: false,
|
|
13
|
+
size: false,
|
|
14
|
+
sizeInBytes: false,
|
|
15
|
+
}, file => {
|
|
16
|
+
allFiles.push(file);
|
|
17
|
+
}, dir => {
|
|
18
|
+
allFiles.push(dir);
|
|
19
|
+
});
|
|
20
|
+
return { dree: result, allFiles };
|
|
21
|
+
}
|
|
22
|
+
export function convertDree(root) {
|
|
23
|
+
if (root.type === Type.FILE) {
|
|
24
|
+
return {
|
|
25
|
+
name: root.name,
|
|
26
|
+
type: root.type,
|
|
27
|
+
extension: root.extension,
|
|
28
|
+
stem: root.extension ? root.name.slice(0, -root.extension.length) : root.name,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
assert(root.children);
|
|
32
|
+
const node = {
|
|
33
|
+
name: root.name,
|
|
34
|
+
type: root.type,
|
|
35
|
+
contents: {},
|
|
36
|
+
};
|
|
37
|
+
for (const child of root.children) {
|
|
38
|
+
node.contents[child.name] = convertDree(child);
|
|
39
|
+
}
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
export async function buildSchemaForDirectoryTree(result, name) {
|
|
43
|
+
const root = result.dree;
|
|
44
|
+
assert(root.type === Type.DIRECTORY);
|
|
45
|
+
const node = convertDree(root);
|
|
46
|
+
const schema = (await jsonToZod(node, `${name}Schema`)).split("\n");
|
|
47
|
+
const lines = `
|
|
48
|
+
// Note: This file is autogenerated. Do not edit it directly.
|
|
49
|
+
//
|
|
50
|
+
// Describes the contents of the test directory, which is a blueprint for
|
|
51
|
+
// files and directories. Tests can create a unique, safe environment for
|
|
52
|
+
// interacting with the contents of such a directory.
|
|
53
|
+
//
|
|
54
|
+
// Having strong typing for the test directory contents ensures that tests can
|
|
55
|
+
// be written with confidence that the files and directories they expect are
|
|
56
|
+
// actually found. Otherwise the tests are brittle and can break easily.
|
|
57
|
+
`.split("\n");
|
|
58
|
+
const allFilePaths = result.allFiles.map(f => f.relativePath);
|
|
59
|
+
const ContentsSchema = `${name}ContentsSchema`;
|
|
60
|
+
const ContentsSchemaType = `${name}ContentsSchemaType`;
|
|
61
|
+
return [
|
|
62
|
+
...lines,
|
|
63
|
+
...schema,
|
|
64
|
+
`export const ${ContentsSchema} = ${name}Schema.shape.contents`,
|
|
65
|
+
`export type ${ContentsSchemaType} = z.infer<typeof ${name}Schema>`,
|
|
66
|
+
"",
|
|
67
|
+
`export type ${name} = ${ContentsSchemaType}["contents"]`,
|
|
68
|
+
"",
|
|
69
|
+
`export const testDirectoryFiles = z.enum(${JSON.stringify(allFilePaths, null, 2)})`,
|
|
70
|
+
`export type MyTestDirectoryFile = z.infer<typeof testDirectoryFiles>`,
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
74
|
+
export async function buildTestDirectorySchema(testDirectoryPath) {
|
|
75
|
+
const dree = getDirectoryTree(testDirectoryPath);
|
|
76
|
+
let text = await buildSchemaForDirectoryTree(dree, "MyTestDirectory");
|
|
77
|
+
const options = await resolveConfig(__filename);
|
|
78
|
+
text = await format(text, { ...options, parser: "typescript" });
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { buildSchemaForDirectoryTree, getDirectoryTree } from ".";
|
|
4
|
+
import { Lazy } from "../utilities/Lazy";
|
|
5
|
+
describe("dirtree", () => {
|
|
6
|
+
const output = new Lazy(() => getDirectoryTree(path.join(__dirname, "..", "..", "..", "..", "integration-tests", "test-environment")));
|
|
7
|
+
it("can get a list of all the files", () => {
|
|
8
|
+
const result = output.get().allFiles;
|
|
9
|
+
expect(result.length).toBeGreaterThan(1);
|
|
10
|
+
expect(result[0].relativePath).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
it("should be able to build a typescript type for the tree", async () => {
|
|
13
|
+
const result = await buildSchemaForDirectoryTree(output.get(), "MyDirectoryTree");
|
|
14
|
+
expect(result).toMatchInlineSnapshot(`
|
|
15
|
+
"
|
|
16
|
+
// Note: This file is autogenerated. Do not edit it directly.
|
|
17
|
+
//
|
|
18
|
+
// Describes the contents of the test directory, which is a blueprint for
|
|
19
|
+
// files and directories. Tests can create a unique, safe environment for
|
|
20
|
+
// interacting with the contents of such a directory.
|
|
21
|
+
//
|
|
22
|
+
// Having strong typing for the test directory contents ensures that tests can
|
|
23
|
+
// be written with confidence that the files and directories they expect are
|
|
24
|
+
// actually found. Otherwise the tests are brittle and can break easily.
|
|
25
|
+
|
|
26
|
+
import { z } from "zod"
|
|
27
|
+
|
|
28
|
+
export const MyDirectoryTreeSchema = z.object({
|
|
29
|
+
name: z.literal("test-environment"),
|
|
30
|
+
type: z.literal("directory"),
|
|
31
|
+
contents: z.object({
|
|
32
|
+
"config-modifications": z.object({
|
|
33
|
+
name: z.literal("config-modifications"),
|
|
34
|
+
type: z.literal("directory"),
|
|
35
|
+
contents: z.object({
|
|
36
|
+
"add_command_to_count_open_buffers.lua": z.object({
|
|
37
|
+
name: z.literal("add_command_to_count_open_buffers.lua"),
|
|
38
|
+
type: z.literal("file"),
|
|
39
|
+
extension: z.literal("lua"),
|
|
40
|
+
stem: z.literal("add_command_to_count_open_buffers."),
|
|
41
|
+
}),
|
|
42
|
+
"use_light_neovim_colorscheme.lua": z.object({
|
|
43
|
+
name: z.literal("use_light_neovim_colorscheme.lua"),
|
|
44
|
+
type: z.literal("file"),
|
|
45
|
+
extension: z.literal("lua"),
|
|
46
|
+
stem: z.literal("use_light_neovim_colorscheme."),
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
"dir with spaces": z.object({
|
|
51
|
+
name: z.literal("dir with spaces"),
|
|
52
|
+
type: z.literal("directory"),
|
|
53
|
+
contents: z.object({
|
|
54
|
+
"file1.txt": z.object({
|
|
55
|
+
name: z.literal("file1.txt"),
|
|
56
|
+
type: z.literal("file"),
|
|
57
|
+
extension: z.literal("txt"),
|
|
58
|
+
stem: z.literal("file1."),
|
|
59
|
+
}),
|
|
60
|
+
"file2.txt": z.object({
|
|
61
|
+
name: z.literal("file2.txt"),
|
|
62
|
+
type: z.literal("file"),
|
|
63
|
+
extension: z.literal("txt"),
|
|
64
|
+
stem: z.literal("file2."),
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
"file.txt": z.object({
|
|
69
|
+
name: z.literal("file.txt"),
|
|
70
|
+
type: z.literal("file"),
|
|
71
|
+
extension: z.literal("txt"),
|
|
72
|
+
stem: z.literal("file."),
|
|
73
|
+
}),
|
|
74
|
+
"initial-file.txt": z.object({
|
|
75
|
+
name: z.literal("initial-file.txt"),
|
|
76
|
+
type: z.literal("file"),
|
|
77
|
+
extension: z.literal("txt"),
|
|
78
|
+
stem: z.literal("initial-file."),
|
|
79
|
+
}),
|
|
80
|
+
"other-subdirectory": z.object({
|
|
81
|
+
name: z.literal("other-subdirectory"),
|
|
82
|
+
type: z.literal("directory"),
|
|
83
|
+
contents: z.object({
|
|
84
|
+
"other-sub-file.txt": z.object({
|
|
85
|
+
name: z.literal("other-sub-file.txt"),
|
|
86
|
+
type: z.literal("file"),
|
|
87
|
+
extension: z.literal("txt"),
|
|
88
|
+
stem: z.literal("other-sub-file."),
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
routes: z.object({
|
|
93
|
+
name: z.literal("routes"),
|
|
94
|
+
type: z.literal("directory"),
|
|
95
|
+
contents: z.object({
|
|
96
|
+
"posts.$postId": z.object({
|
|
97
|
+
name: z.literal("posts.$postId"),
|
|
98
|
+
type: z.literal("directory"),
|
|
99
|
+
contents: z.object({
|
|
100
|
+
"adjacent-file.txt": z.object({
|
|
101
|
+
name: z.literal("adjacent-file.txt"),
|
|
102
|
+
type: z.literal("file"),
|
|
103
|
+
extension: z.literal("txt"),
|
|
104
|
+
stem: z.literal("adjacent-file."),
|
|
105
|
+
}),
|
|
106
|
+
"route.tsx": z.object({
|
|
107
|
+
name: z.literal("route.tsx"),
|
|
108
|
+
type: z.literal("file"),
|
|
109
|
+
extension: z.literal("tsx"),
|
|
110
|
+
stem: z.literal("route."),
|
|
111
|
+
}),
|
|
112
|
+
"should-be-excluded-file.txt": z.object({
|
|
113
|
+
name: z.literal("should-be-excluded-file.txt"),
|
|
114
|
+
type: z.literal("file"),
|
|
115
|
+
extension: z.literal("txt"),
|
|
116
|
+
stem: z.literal("should-be-excluded-file."),
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
122
|
+
subdirectory: z.object({
|
|
123
|
+
name: z.literal("subdirectory"),
|
|
124
|
+
type: z.literal("directory"),
|
|
125
|
+
contents: z.object({
|
|
126
|
+
"subdirectory-file.txt": z.object({
|
|
127
|
+
name: z.literal("subdirectory-file.txt"),
|
|
128
|
+
type: z.literal("file"),
|
|
129
|
+
extension: z.literal("txt"),
|
|
130
|
+
stem: z.literal("subdirectory-file."),
|
|
131
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
}),
|
|
134
|
+
"test-setup.lua": z.object({
|
|
135
|
+
name: z.literal("test-setup.lua"),
|
|
136
|
+
type: z.literal("file"),
|
|
137
|
+
extension: z.literal("lua"),
|
|
138
|
+
stem: z.literal("test-setup."),
|
|
139
|
+
}),
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
export const MyDirectoryTreeContentsSchema = MyDirectoryTreeSchema.shape.contents
|
|
144
|
+
export type MyDirectoryTreeContentsSchemaType = z.infer<typeof MyDirectoryTreeSchema>
|
|
145
|
+
|
|
146
|
+
export type MyDirectoryTree = MyDirectoryTreeContentsSchemaType["contents"]
|
|
147
|
+
|
|
148
|
+
export const testDirectoryFiles = z.enum([
|
|
149
|
+
"config-modifications/add_command_to_count_open_buffers.lua",
|
|
150
|
+
"config-modifications/use_light_neovim_colorscheme.lua",
|
|
151
|
+
"config-modifications",
|
|
152
|
+
"dir with spaces/file1.txt",
|
|
153
|
+
"dir with spaces/file2.txt",
|
|
154
|
+
"dir with spaces",
|
|
155
|
+
"file.txt",
|
|
156
|
+
"initial-file.txt",
|
|
157
|
+
"other-subdirectory/other-sub-file.txt",
|
|
158
|
+
"other-subdirectory",
|
|
159
|
+
"routes/posts.$postId/adjacent-file.txt",
|
|
160
|
+
"routes/posts.$postId/route.tsx",
|
|
161
|
+
"routes/posts.$postId/should-be-excluded-file.txt",
|
|
162
|
+
"routes/posts.$postId",
|
|
163
|
+
"routes",
|
|
164
|
+
"subdirectory/subdirectory-file.txt",
|
|
165
|
+
"subdirectory",
|
|
166
|
+
"test-setup.lua",
|
|
167
|
+
"."
|
|
168
|
+
])
|
|
169
|
+
export type MyTestDirectoryFile = z.infer<typeof testDirectoryFiles>"
|
|
170
|
+
`);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function jsonToZod(object: unknown, name?: string): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { format, resolveConfig } from "prettier";
|
|
2
|
+
import babelParser from "prettier/parser-babel";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
export async function jsonToZod(object, name = "schema") {
|
|
6
|
+
const parse = (o, seen) => {
|
|
7
|
+
switch (typeof o) {
|
|
8
|
+
case "string":
|
|
9
|
+
return `z.literal("${o}")`;
|
|
10
|
+
case "number":
|
|
11
|
+
return "z.number()";
|
|
12
|
+
case "bigint":
|
|
13
|
+
return "z.number().int()";
|
|
14
|
+
case "boolean":
|
|
15
|
+
return "z.boolean()";
|
|
16
|
+
case "object":
|
|
17
|
+
if (o === null) {
|
|
18
|
+
return "z.null()";
|
|
19
|
+
}
|
|
20
|
+
if (seen.find(_obj => Object.is(_obj, o))) {
|
|
21
|
+
throw new Error("Circular objects are not supported");
|
|
22
|
+
}
|
|
23
|
+
seen.push(o);
|
|
24
|
+
if (Array.isArray(o)) {
|
|
25
|
+
const options = o
|
|
26
|
+
.map(obj => parse(obj, seen))
|
|
27
|
+
.reduce((acc, curr) => (acc.includes(curr) ? acc : [...acc, curr]), []);
|
|
28
|
+
if (options.length === 1) {
|
|
29
|
+
return `z.array(${options[0]})`;
|
|
30
|
+
}
|
|
31
|
+
else if (options.length > 1) {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
33
|
+
return `z.array(z.union([${options}]))`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return `z.array(z.unknown())`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
40
|
+
return `z.object({${Object.entries(o).map(([k, v]) => `'${k}':${parse(v, seen)}`)}})`;
|
|
41
|
+
case "undefined":
|
|
42
|
+
return "z.undefined()";
|
|
43
|
+
case "function":
|
|
44
|
+
return "z.function()";
|
|
45
|
+
case "symbol":
|
|
46
|
+
default:
|
|
47
|
+
return "z.unknown()";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const prettierConfig = await resolveConfig(__filename);
|
|
51
|
+
return format(`import {z} from "zod"\n\nexport const ${name}=${parse(object, [])}`, {
|
|
52
|
+
...(prettierConfig || {}),
|
|
53
|
+
parser: "babel",
|
|
54
|
+
plugins: [babelParser],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnyRouter } from "@trpc/server";
|
|
2
|
+
import "core-js/proposals/async-explicit-resource-management";
|
|
3
|
+
import type { TestServerConfig } from "./updateTestdirectorySchemaFile";
|
|
4
|
+
export declare class TestServer {
|
|
5
|
+
private readonly port;
|
|
6
|
+
constructor(port: number);
|
|
7
|
+
startAndRun<TRouter extends AnyRouter>(appRouter: TRouter, config: TestServerConfig): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
|
2
|
+
import "core-js/proposals/async-explicit-resource-management";
|
|
3
|
+
import { once } from "events";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import { createContext } from "./connection/trpc";
|
|
6
|
+
import { updateTestdirectorySchemaFile } from "./updateTestdirectorySchemaFile";
|
|
7
|
+
export class TestServer {
|
|
8
|
+
port;
|
|
9
|
+
constructor(port) {
|
|
10
|
+
this.port = port;
|
|
11
|
+
}
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
13
|
+
async startAndRun(appRouter, config) {
|
|
14
|
+
console.log("🚀 Server starting");
|
|
15
|
+
await updateTestdirectorySchemaFile(config);
|
|
16
|
+
const wss = new WebSocketServer({ port: this.port });
|
|
17
|
+
const handler = applyWSSHandler({
|
|
18
|
+
wss,
|
|
19
|
+
router: appRouter,
|
|
20
|
+
createContext,
|
|
21
|
+
// Enable heartbeat messages to keep connection open (disabled by default)
|
|
22
|
+
keepAlive: {
|
|
23
|
+
enabled: true,
|
|
24
|
+
// server ping message interval in milliseconds
|
|
25
|
+
pingMs: 30_000,
|
|
26
|
+
// connection is terminated if pong message is not received in this many milliseconds
|
|
27
|
+
pongWaitMs: 5000,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
wss.on("connection", socket => {
|
|
31
|
+
console.log(`➕➕ Connection (${wss.clients.size})`);
|
|
32
|
+
socket.once("close", () => {
|
|
33
|
+
console.log(`➖➖ Connection (${wss.clients.size})`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
console.log(`✅ WebSocket Server listening on ws://localhost:${this.port}`);
|
|
37
|
+
await Promise.race([once(process, "SIGTERM"), once(process, "SIGINT")]);
|
|
38
|
+
console.log("Shutting down...");
|
|
39
|
+
handler.broadcastReconnectNotification();
|
|
40
|
+
wss.close(error => {
|
|
41
|
+
if (error) {
|
|
42
|
+
console.error("Error closing WebSocket server", error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
console.log("WebSocket server closed");
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import EventEmitter from "events";
|
|
2
|
+
import type { TestDirectory } from "../types";
|
|
3
|
+
import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication";
|
|
4
|
+
export type StdoutMessage = "stdout";
|
|
5
|
+
export type StartNeovimGenericArguments = {
|
|
6
|
+
terminalDimensions?: {
|
|
7
|
+
cols: number;
|
|
8
|
+
rows: number;
|
|
9
|
+
};
|
|
10
|
+
filename?: string | {
|
|
11
|
+
openInVerticalSplits: string[];
|
|
12
|
+
};
|
|
13
|
+
startupScriptModifications?: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare class NeovimApplication extends DisposableSingleApplication {
|
|
16
|
+
private readonly testEnvironmentPath;
|
|
17
|
+
private testDirectory;
|
|
18
|
+
readonly events: EventEmitter;
|
|
19
|
+
constructor(testEnvironmentPath: string);
|
|
20
|
+
/**
|
|
21
|
+
* Kill the current application and start a new one with the given arguments.
|
|
22
|
+
*/
|
|
23
|
+
startNextAndKillCurrent(testDirectory: TestDirectory, startArgs: StartNeovimGenericArguments): Promise<void>;
|
|
24
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
25
|
+
}
|