Skip to content

Commit cce0a45

Browse files
authored
Merge pull request #8 from inputlogic/typescript
Switch to typescript
2 parents c2b2a70 + eae679a commit cce0a45

4 files changed

Lines changed: 166 additions & 73 deletions

File tree

bin/ghostpipe.js renamed to bin/ghostpipe.ts

Lines changed: 91 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
#!/usr/bin/env node
2-
const crypto = require('crypto')
3-
const { Command } = require('commander')
4-
const { Doc: YDoc } = require('yjs')
5-
const { WebrtcProvider } = require('y-webrtc')
6-
const wrtc = require('@roamhq/wrtc')
7-
const fs = require('fs')
8-
const path = require('path')
9-
const readline = require('readline')
10-
const chokidar = require('chokidar')
11-
const { execSync } = require('child_process')
12-
const chalk = require('chalk')
2+
import * as crypto from 'crypto'
3+
import { Command } from 'commander'
4+
import { Doc as YDoc } from 'yjs'
5+
import { WebrtcProvider } from 'y-webrtc'
6+
import * as wrtc from '@roamhq/wrtc'
7+
import * as fs from 'fs'
8+
import * as path from 'path'
9+
import * as readline from 'readline'
10+
import * as chokidar from 'chokidar'
11+
import { execSync } from 'child_process'
12+
import chalk from 'chalk'
13+
import * as os from 'os'
14+
15+
interface Interface {
16+
name: string
17+
host: string
18+
file: string
19+
url?: string
20+
ydoc?: YDoc
21+
provider?: WebrtcProvider
22+
}
23+
24+
interface Config {
25+
signalingServer: string
26+
globalConfigPath: string
27+
localConfigPath: string
28+
defaultDiffBaseBranch: string
29+
diff?: string | boolean
30+
isGitRepo?: boolean
31+
interfaces?: Interface[]
32+
}
33+
34+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8'))
1335

1436
const program = new Command()
15-
const DEFAULT_CONFIG = {
37+
const DEFAULT_CONFIG: Config = {
1638
signalingServer: 'wss://signaling.ghostpipe.dev',
17-
globalConfigPath: path.join(require('os').homedir(), '.config', 'ghostpipe', 'config.json'),
39+
globalConfigPath: path.join(os.homedir(), '.config', 'ghostpipe', 'config.json'),
1840
localConfigPath: 'ghostpipe.config.json',
1941
defaultDiffBaseBranch: 'main',
2042
}
@@ -23,34 +45,34 @@ let VERBOSE = false
2345
program
2446
.name('ghostpipe')
2547
.description('Interfaces for your codebase')
26-
.version(require('../package.json').version)
48+
.version(packageJson.version)
2749
.argument('[url]', 'Interface URL')
2850
.argument('[file]', 'The file this interface can use')
2951
.option('--verbose', 'Enable verbose logging')
3052
.option('--diff [branch]', 'Base branch for diff comparison')
3153

32-
program.action(async (url, file, options) => {
33-
VERBOSE = options.verbose
54+
program.action(async (url: string | undefined, file: string | undefined, options: { verbose?: boolean, diff?: string | boolean }) => {
55+
VERBOSE = options.verbose || false
3456
await main(url, file, options)
3557
})
3658

37-
const log = (...args) => VERBOSE && console.log(chalk.gray(...args))
38-
log.error = (...args) => VERBOSE && console.error(chalk.red(...args))
39-
log.warn = (...args) => VERBOSE && console.warn(chalk.yellow(...args))
59+
const log = (...args: any[]) => VERBOSE && console.log(chalk.gray(...args))
60+
log.error = (...args: any[]) => VERBOSE && console.error(chalk.red(...args))
61+
log.warn = (...args: any[]) => VERBOSE && console.warn(chalk.yellow(...args))
4062

41-
const main = async (url, file, {diff}) => {
42-
const config = await buildConfig({url, file, diff})
63+
const main = async (url: string | undefined, file: string | undefined, options: { diff?: string | boolean }) => {
64+
const config = await buildConfig({ url, file, diff: options.diff })
4365
.then(validateConfig)
4466
.then(connectInterfaces)
45-
config.interfaces.forEach(
67+
config.interfaces?.forEach(
4668
intf => console.log(chalk.cyan(`${intf.name}: `) + chalk.underline(intf.url))
4769
)
4870
}
4971

50-
const buildConfig = async ({url, file, diff}) => {
72+
const buildConfig = async ({ url, file, diff }: { url?: string, file?: string, diff?: string | boolean }): Promise<Config> => {
5173
const globalConfig = readJson(DEFAULT_CONFIG.globalConfigPath)
5274
const localConfig = readJson(globalConfig?.localConfigPath || DEFAULT_CONFIG.localConfigPath)
53-
const inlineInterface = await prepareInlineInterface({url, file})
75+
const inlineInterface = await prepareInlineInterface({ url, file })
5476
return {
5577
...DEFAULT_CONFIG,
5678
...globalConfig,
@@ -61,7 +83,7 @@ const buildConfig = async ({url, file, diff}) => {
6183
}
6284
}
6385

64-
const validateConfig = config => {
86+
const validateConfig = (config: Config): Config => {
6587
if (config.diff && !config.isGitRepo) {
6688
console.warn(chalk.yellow('Warning: --diff flag specified but current directory is not a git repository'))
6789
process.exit(0)
@@ -83,34 +105,34 @@ const validateConfig = config => {
83105
return config
84106
}
85107

86-
const connectInterfaces = config => ({
108+
const connectInterfaces = (config: Config): Config => ({
87109
...config,
88-
interfaces: config.interfaces.map(intf => connectInterface(intf, config))
110+
interfaces: config.interfaces?.map(intf => connectInterface(intf, config))
89111
})
90112

91-
const connectInterface = (intf, config) => {
113+
const connectInterface = (intf: Interface, config: Config): Interface => {
92114
const connectedInterface = connectInterfaceToYjs(intf, config)
93115
watchLocalFile(connectedInterface, config)
94116
return connectedInterface
95117
}
96118

97-
const connectInterfaceToYjs = (intf, config) => {
119+
const connectInterfaceToYjs = (intf: Interface, config: Config): Interface => {
98120
const pipe = crypto.randomBytes(16).toString('hex')
99-
const params = new URLSearchParams({pipe, signaling: config.signalingServer})
121+
const params = new URLSearchParams({ pipe, signaling: config.signalingServer })
100122
const ydoc = new YDoc()
101-
const provider = new WebrtcProvider(pipe, ydoc, {signaling: [config.signalingServer], peerOpts: { wrtc }})
123+
const provider = new WebrtcProvider(pipe, ydoc, { signaling: [config.signalingServer], peerOpts: { wrtc } })
102124
const meta = ydoc.getMap('meta')
103125
if (config.isGitRepo) {
104-
meta.set('base-branch', config.diff)
126+
meta.set('base-branch', config.diff as string)
105127
meta.set('head-branch', getHeadBranch())
106128
}
107129
ydoc.getMap('data').observe((event, transaction) => {
108-
log(intf.name, 'transaction origin:', transaction.origin?.peerId || 'local')
109-
if (!transaction.origin?.peerId) return
130+
log(intf.name, 'transaction origin:', (transaction.origin as any)?.peerId || 'local')
131+
if (!(transaction.origin as any)?.peerId) return
110132
event.changes.keys.forEach((change, key) => {
111133
if (change.action === 'update' || change.action === 'add') {
112134
log(intf.name, 'ydoc event', change.action, key)
113-
debouncedWriteFile(intf.file, ydoc, intf, config.diff)
135+
debouncedWriteFile(intf.file, ydoc, intf, config.diff as string)
114136
} else if (change.action === 'delete') {
115137
log('TODO: handle delete file')
116138
}
@@ -124,28 +146,28 @@ const connectInterfaceToYjs = (intf, config) => {
124146
}
125147
}
126148

127-
const watchLocalFile = (intf, config) => {
149+
const watchLocalFile = (intf: Interface, config: Config): void => {
128150
const watcher = chokidar.watch([intf.file], {
129151
persistent: true,
130152
ignoreInitial: false
131153
})
132154

133-
watcher.on('error', error => {
134-
console.error(chalk.red('ERROR:'), chalk.red(error.message))
155+
watcher.on('error', (error) => {
156+
console.error(chalk.red('ERROR:'), chalk.red((error as Error).message))
135157
process.exit(1)
136158
})
137159

138160
watcher.on('all', (event, path) => {
139161
if (event === 'add') {
140-
debouncedAdd(path, intf, config.diff)
162+
debouncedAdd(path, intf, config.diff as string)
141163
}
142164
if (event === 'change') {
143-
debouncedChange(path, intf, config.diff)
165+
debouncedChange(path, intf, config.diff as string)
144166
}
145167
})
146168
}
147169

148-
const prepareInlineInterface = async ({url, file}) => {
170+
const prepareInlineInterface = async ({ url, file }: { url?: string, file?: string }): Promise<Interface | null> => {
149171
if (!url) return null
150172
if (!file) {
151173
file = await getFilePath(`${url.replace(/^https?:\/\//, '').replace(/[^a-zA-Z0-9.-]/g, '_')}.txt`)
@@ -158,7 +180,7 @@ const prepareInlineInterface = async ({url, file}) => {
158180
}
159181
}
160182

161-
const createFileIfDoesNotExist = file => {
183+
const createFileIfDoesNotExist = (file: string): void => {
162184
if (!fs.existsSync(path.dirname(file))) {
163185
fs.mkdirSync(path.dirname(file), { recursive: true })
164186
}
@@ -167,7 +189,7 @@ const createFileIfDoesNotExist = file => {
167189
}
168190
}
169191

170-
const getFilePath = async (defaultPath) => {
192+
const getFilePath = async (defaultPath: string): Promise<string> => {
171193
if (fs.existsSync(defaultPath))
172194
return defaultPath
173195

@@ -184,7 +206,7 @@ const getFilePath = async (defaultPath) => {
184206
})
185207
}
186208

187-
const isGitRepo = () => {
209+
const isGitRepo = (): boolean => {
188210
try {
189211
execSync('git rev-parse --git-dir', { encoding: 'utf8', stdio: 'pipe' })
190212
return true
@@ -193,18 +215,18 @@ const isGitRepo = () => {
193215
}
194216
}
195217

196-
const addDiffFile = ({intf, diff, file}) => {
218+
const addDiffFile = ({ intf, diff, file }: { intf: Interface, diff: string, file: string }): void => {
197219
if (!diff || !isGitRepo()) return
198220
try {
199-
const content = execSync(`git show ${diff}:${file}`, {encoding: 'utf8', stdio: 'pipe'})
200-
intf.ydoc.getMap('base-data').set('content', content)
221+
const content = execSync(`git show ${diff}:${file}`, { encoding: 'utf8', stdio: 'pipe' })
222+
intf.ydoc?.getMap('base-data').set('content', content)
201223
log(`Loaded diff file for ${intf.name}: ${file}`)
202224
} catch (error) {
203225
log.error(`File not in ${diff} branch: ${file}`)
204226
}
205227
}
206228

207-
const readJson = (file) => {
229+
const readJson = (file: string): any => {
208230
try {
209231
return JSON.parse(fs.readFileSync(file, 'utf8'))
210232
} catch (error) {
@@ -213,53 +235,54 @@ const readJson = (file) => {
213235
}
214236
}
215237

216-
const debounceByKey = (func, delay) => {
217-
const timers = new Map()
218-
return (...args) => {
219-
const key = args[0]
238+
const debounceByKey = <T extends (...args: any[]) => void>(func: T, delay: number): T => {
239+
const timers = new Map<string, NodeJS.Timeout>()
240+
return ((...args: Parameters<T>) => {
241+
const key = args[0] as string
220242
clearTimeout(timers.get(key))
221243
timers.set(key, setTimeout(() => {
222244
timers.delete(key)
223-
func.apply(this, args)
245+
func.apply(null, args)
224246
}, delay))
225-
}
247+
}) as T
226248
}
227249

228-
const debouncedAdd = debounceByKey((path, intf, diff) => {
250+
const debouncedAdd = debounceByKey((path: string, intf: Interface, diff: string) => {
229251
const content = fs.readFileSync(path, 'utf8')
230-
intf.ydoc.transact(() => {
231-
intf.ydoc.getMap('data').set('content', content)
252+
intf.ydoc?.transact(() => {
253+
intf.ydoc?.getMap('data').set('content', content)
232254
})
233-
addDiffFile({intf, diff, file: path})
255+
addDiffFile({ intf, diff, file: path })
234256
}, 300)
235257

236-
const debouncedChange = debounceByKey((path, intf, diff) => {
258+
const debouncedChange = debounceByKey((path: string, intf: Interface, diff: string) => {
237259
const fileContent = fs.readFileSync(path, 'utf8')
238-
const content = intf.ydoc.getMap('data').get('content')
260+
const content = intf.ydoc?.getMap('data').get('content')
239261
if (content !== fileContent) {
240262
log('file change local', path)
241-
intf.ydoc.transact(() => {
242-
intf.ydoc.getMap('data').set('content', fileContent)
263+
intf.ydoc?.transact(() => {
264+
intf.ydoc?.getMap('data').set('content', fileContent)
243265
})
244266
}
245-
addDiffFile({intf, diff, file: path})
267+
addDiffFile({ intf, diff, file: path })
246268
}, 300)
247269

248-
const debouncedWriteFile = debounceByKey((key, ydoc, intf, diff) => {
270+
const debouncedWriteFile = debounceByKey((key: string, ydoc: YDoc, intf: Interface, diff: string) => {
249271
const content = ydoc.getMap('data').get('content')
250272
const fileContent = fs.readFileSync(key, 'utf8')
251273
if (content === fileContent) return
252274
log(intf.name, 'file change remote', key)
253-
fs.writeFileSync(key, content, 'utf8')
254-
addDiffFile({diff, intf, file: key})
275+
fs.writeFileSync(key, content as string, 'utf8')
276+
addDiffFile({ diff, intf, file: key })
255277
}, 300)
256278

257-
const getHeadBranch = () => {
279+
const getHeadBranch = (): string | null => {
258280
if (!isGitRepo()) return null
259281
try {
260282
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim()
261283
} catch (error) {
262-
log.error('Error getting branch:', error.message)
284+
log.error('Error getting branch:', (error as Error).message)
285+
return null
263286
}
264287
}
265288

package-lock.json

Lines changed: 38 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)