-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
229 lines (186 loc) · 7.32 KB
/
index.js
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
////////////////////////////////////////////////////////////////////////////////
//
// @small-tech/node-hugo
//
// A basic cross-platform interface to the Hugo binary from Node.js that uses
// the 64-bit release binaries to support Linux, macOS, and Windows.
//
// Copyright ⓒ 2020 Aral Balkan. Licensed under AGPLv3 or later.
// Shared with ♥ by the Small Technology Foundation.
//
// Like this? Fund us!
// https://small-tech.org/fund-us
//
////////////////////////////////////////////////////////////////////////////////
const os = require('os')
const path = require('path')
const fs = require('fs-extra')
const util = require('util')
const childProcess = require('child_process')
const exec = util.promisify(childProcess.exec)
const spawn = childProcess.spawn
const homeDir = os.homedir()
const hugoVersion = '0.78.0'
class HugoError extends Error {
constructor (message, output) {
super(message)
this.sdterr = output
this.name = 'HugoError'
}
}
class Hugo {
constructor (nodeHugoDir = path.join(homeDir, '.small-tech.org', 'node-hugo')) {
this.machine = {
platform: os.platform(),
architecture: os.arch()
}
this.nodeHugoDir = nodeHugoDir
// Ensure the node-hugo directory exists.
fs.ensureDirSync(nodeHugoDir)
this.hugoBinaryPath = this.hugoBinaryForThisMachine()
}
//
// Public.
//
// Returns the Hugo version.
get version () {
return hugoVersion
}
// Runs a generic, blocking Hugo command using the passed arguments.
async command (args) {
const hugoCommand = `${this.hugoBinaryPath} ${args}`
const options = {
env: process.env,
stdio: 'pipe',
}
const result = await exec(hugoCommand, options)
if (result.stderr !== '') {
// There was an error.
throw new HugoError(`Hugo command failed (arguments: ${args})`, result.stderr)
}
return result.stdout
}
// Builds the Hugo source from sourcePath and writes the output at
// destinationPath. Note that destinationPath is relevant to sourcePath
// (don’t shoot me, this is a Hugo convention so I’m mirroring it for
// consistency with regular Hugo usage). The returned result is the
// object returned from the exec() call with stdout and stderr properties.
async build (sourcePath = '.', destinationPath = 'public/', baseURL = 'http://localhost:1313') {
const hugoBuildCommand = `--source=${sourcePath} --destination=${destinationPath} --baseURL=${baseURL}`
return await this.command(hugoBuildCommand)
}
// Starts a Hugo server with defaults set for rendering to disk and using external live reload.
// (These are the defaults for our current use case on Site.js).
//
// Returns an object that contains a promise that’s resolved to an object with the following
// properties once the initial Hugo build is complete:
//
// - a reference to the Hugo server process
// - the Hugo output so far
//
serve (sourcePath = '.', destinationPath = 'public/', baseURL = 'http://localhost:1313', buildDrafts = true) {
const args = [
'server',
`--source=${sourcePath}`,
`--destination=${destinationPath}`,
`--baseURL=${baseURL}`,
'--renderToDisk',
'--disableLiveReload',
'--appendPort=false',
'--disableFastRender'
]
if (buildDrafts) {
args.push('--buildDrafts')
}
return this.serverWithArgs(args)
}
// Starts a generic Hugo server.
serverWithArgs (args) {
// Args should be an array. Automatically convert an arguments string to one.
if (typeof args === 'string') {
args = args.split(' ')
}
const options = { env: process.env }
const hugoServerProcess = spawn(this.hugoBinaryPath, args, options)
const hugoServerPromise = new Promise((resolve, reject) => {
// Wait for the line telling us that the build is complete and that
// the server is ready and only then return.
let hugoBuildOutput = ''
let buildComplete = false
const stdoutHandler = (data) => {
const lines = data.toString('utf-8').split('\n')
lines.forEach(line => {
hugoBuildOutput += `\n${line}`
if (line.startsWith('Built in')) {
// The site has been built. Let’s resolve the promise.
buildComplete = true
}
})
if (buildComplete) {
// Clean up. If the consumer wants to, they can add their own to the
// hugo server process instance that we will return.
hugoServerProcess.stdout.removeAllListeners()
hugoServerProcess.stderr.removeAllListeners()
// OK.
resolve({
hugoServerProcess,
hugoBuildOutput
})
}
}
const stderrHandler = (data) => {
const errorMessage = data.toString('utf-8')
reject(errorMessage)
}
hugoServerProcess.stdout.on('data', stdoutHandler)
hugoServerProcess.stderr.on('data', stderrHandler)
})
return hugoServerPromise
}
//
// Private.
//
// Returns the Hugo binary for this machine (platform + architecture) and
// throws an error if there isn’t one for it.
//
// Note: this expects the Hugo binaries to be manually renamed prior to being
// ===== added to the hugo-bin folder. The naming convention we use is the same
// as the one we use in the Auto Encrypt Localhost project for the mkcert binaries.
hugoBinaryForThisMachine () {
const platformMap = {
linux: 'linux',
darwin: 'darwin',
win32: 'windows'
}
const architectureMap = {
arm: 'arm',
arm64: 'arm64',
x64: 'amd64'
}
const platform = platformMap[this.machine.platform]
const architecture = architectureMap[this.machine.architecture]
if (platform === undefined) throw new Error('Unsupported platform', this.machine.platform)
if (architecture === undefined) throw new Error('Unsupported architecture', this.machine.architecture)
const hugoBinaryName = `hugo-v${hugoVersion}-${platform}-${architecture}`
const hugoBinaryRelativePath = path.join('hugo-bin', hugoBinaryName)
let hugoBinaryInternalPath = path.join(__dirname, hugoBinaryRelativePath)
if (platform === 'windows') hugoBinaryInternalPath += '.exe'
// Check if the platform + architecture combination is supported.
if (!fs.existsSync(hugoBinaryInternalPath)) throw new Error(`[node-hugo] Unsupported platform + architecture combination for ${platform}-${architecture}`)
// Copy the Hugo binary to the external node-hugo directory so that we can call execSync() on it if
// the app using this module is wrapped into an executable using Nexe (https://github.com/nexe/nexe) – like
// Site.js (https://sitejs.org) is, for example. We use readFileSync() and writeFileSync() as
// Nexe does not support copyFileSync() yet (see https://github.com/nexe/nexe/issues/607).
const hugoBinaryExternalPath = path.join(this.nodeHugoDir, hugoBinaryName)
if (!fs.existsSync(hugoBinaryExternalPath)) {
try {
const hugoBinaryBuffer = fs.readFileSync(hugoBinaryInternalPath, 'binary')
fs.writeFileSync(hugoBinaryExternalPath, hugoBinaryBuffer, {encoding: 'binary', mode: 0o755})
} catch (error) {
throw new Error(` 🤯 [node-hugo] Panic: Could not copy Hugo binary to external directory: ${error.message}`)
}
}
return hugoBinaryExternalPath
}
}
module.exports = Hugo