-
Notifications
You must be signed in to change notification settings - Fork 23
/
Rojo.ts
281 lines (231 loc) · 7.66 KB
/
Rojo.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import * as childProcess from 'child_process'
import * as vscode from 'vscode'
import * as fs from 'fs-extra'
import * as path from 'path'
import { isInterfaceOpened, getConfiguration } from './Util'
import { Bridge } from './Bridge'
type treeBranch = { [index: string]: treeBranch | string }
/**
* Windows-specific Rojo instance. Handles interfacing with the binary.
* @export
* @class Rojo
*/
export class Rojo extends vscode.Disposable {
public workspacePath: string
private rojoPath: string
private server?: childProcess.ChildProcess
private watcher?: fs.FSWatcher
private configPath: string
private outputChannel: vscode.OutputChannel
/**
* Creates an instance of RojoWin32.
* @param {vscode.WorkspaceFolder} workspace The workspace folder for which this Rojo instance belongs.
* @param {string} rojoPath The path to the rojo binary.
* @memberof Rojo
*/
constructor (workspace: vscode.WorkspaceFolder, private bridge: Bridge) {
super(() => this.dispose())
this.workspacePath = workspace.uri.fsPath
this.configPath = path.join(this.workspacePath, bridge.getConfigFileName())
this.outputChannel = vscode.window.createOutputChannel(`Rojo: ${workspace.name}`)
this.rojoPath = bridge.rojoPath
}
/**
* Determines if this instance is currently serving with "rojo serve.
* @readonly
* @type {boolean}
* @memberof Rojo
*/
public get serving (): boolean {
return !!this.server
}
/**
* A wrapper for "rojo init".
* @memberof Rojo
*/
public init (): void {
childProcess.execFileSync(this.rojoPath, ['init'], {
cwd: this.workspacePath
})
this.openConfiguration()
}
/**
* A wrapper for "rojo build".
* @memberof Rojo
*/
public async build (): Promise<void> {
if (!this.bridge.isEpiphany()) {
vscode.window.showErrorMessage('Rojo Build is only supported on 0.5.x or newer.')
return
}
const outputConfig = getConfiguration().get('buildOutputPath') as string
const outputFile = `${outputConfig}.${this.isConfigRootDataModel() ? 'rbxl' : 'rbxm'}`
const outputPath = path.join(this.workspacePath, outputFile)
await fs.ensureDir(path.dirname(outputPath))
try {
this.sendToOutput(
childProcess.execFileSync(this.rojoPath, ['build', '-o', outputPath], {
cwd: this.workspacePath
}),
true
)
} catch (e) {
this.sendToOutput(e.toString(), true)
}
}
public sendToOutput (data: string | Buffer, show?: boolean) {
this.outputChannel.append(data.toString())
if (show) {
this.outputChannel.show()
}
}
/**
* A wrapper for "rojo serve".
* Also adds a watcher to "rojo.json" and reloads the server if it changes.
* @memberof Rojo
*/
public serve (): void {
if (this.server) {
this.stop()
}
this.server = childProcess.spawn(this.rojoPath, ['serve'], {
cwd: this.workspacePath
})
this.server.stdout.on('data', data => this.sendToOutput(data))
this.server.stderr.on('data', data => this.sendToOutput(data))
// This is what makes the output channel snap open we start serving.
this.outputChannel.show()
// Start watching for "rojo.json" changes.
this.watch()
}
/**
* Stops "rojo serve" if it's currently serving.
* Also handles closing out the file watcher.
* @memberof Rojo
*/
public stop (): void {
if (!this.server) return
this.server.kill()
this.server = undefined
if (this.watcher) {
this.watcher.close()
this.watcher = undefined
}
}
/**
* A method to be called when cleaning up this instance.
* Terminates the file watcher and the server, if it's running.
* @memberof Rojo
*/
public dispose (): void {
this.outputChannel.dispose()
if (this.server) {
this.stop()
}
}
public loadProjectConfig<T extends object> () {
const currentConfigString = fs.readFileSync(this.configPath, 'utf8')
let currentConfig: T
try {
currentConfig = JSON.parse(currentConfigString)
} catch (e) {
return false
}
return currentConfig
}
public createPartitionRojo04 (partitionPath: string, partitionTarget: string): boolean {
const currentConfig = this.loadProjectConfig<{partitions: {[index: string]: {path: string, target: string}}}>()
if (!currentConfig) {
return false
}
let newName = path.basename(partitionPath, '.lua')
while (currentConfig.partitions[newName] != null) {
const numberPattern = / \((\d+)\)/
const numberMatch = newName.match(numberPattern)
if (numberMatch) {
newName = newName.replace(numberPattern, ` (${(parseInt(numberMatch[1], 10) + 1).toString()})`)
} else {
newName += ' (2)'
}
}
currentConfig.partitions[newName] = {
path: path.relative(path.dirname(this.configPath), partitionPath).replace(/\\/g, '/'),
target: partitionTarget
}
fs.writeFileSync(this.configPath, JSON.stringify(currentConfig, undefined, 2))
return true
}
private isConfigRootDataModel (): boolean {
if (!this.bridge.isEpiphany()) {
throw new Error('Attempt to check if root is DataModel on 0.4.x')
}
const config = this.loadProjectConfigEpiphany()
return config && config.tree && config.tree.$className === 'DataModel' || false
}
private loadProjectConfigEpiphany () {
return this.loadProjectConfig<{
tree?: treeBranch
}>()
}
/**
* Creates a partition in the rojo.json file.
* @param {string} partitionPath The partition path
* @param {string} partitionTarget The partition target
* @returns {boolean} Successful?
* @memberof Rojo
*/
public createPartitionEpiphany (partitionPath: string, partitionTarget: string): boolean {
const currentConfig = this.loadProjectConfigEpiphany()
if (!currentConfig || currentConfig.tree === undefined) {
return false
}
// TODO: Lift this restriction. Need to update the picker too.
if (!this.isConfigRootDataModel()) {
vscode.window.showErrorMessage('Cannot automatically create partitions when tree root is not DataModel')
return false
}
const ancestors = partitionTarget.split('.')
let parent = currentConfig.tree
while (ancestors.length > 0) {
const name = ancestors.shift()!
if (!parent[name]) {
parent[name] = {
...parent === currentConfig.tree && {
$className: name
}
} as treeBranch
}
parent = parent[name] as treeBranch
}
parent.$path = path.relative(path.dirname(this.configPath), partitionPath).replace(/\\/g, '/')
fs.writeFileSync(this.configPath, JSON.stringify(currentConfig, undefined, 2))
return true
}
public createPartition (partitionPath: string, partitionTarget: string): boolean {
if (this.bridge.isEpiphany()) {
return this.createPartitionEpiphany(partitionPath, partitionTarget)
} else {
return this.createPartitionRojo04(partitionPath, partitionTarget)
}
}
/**
* Called internally by "serve". Handles setting up the file watcher for "rojo.json".
* @private
* @memberof Rojo
*/
private watch (): void {
this.watcher = fs.watch(this.configPath, () => {
this.stop()
this.outputChannel.appendLine('Project configuration changed, reloading Rojo.')
this.serve()
})
}
/**
* Opens the current rojo.json file in the editor.
* @memberof Rojo
*/
public openConfiguration (): void {
// Open in column #2 if the interface is open so we don't make people lose progress on the guide
vscode.workspace.openTextDocument(this.configPath).then(doc => vscode.window.showTextDocument(doc, isInterfaceOpened() ? vscode.ViewColumn.Two : undefined))
}
}