-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathmain.js
More file actions
268 lines (232 loc) · 6.88 KB
/
main.js
File metadata and controls
268 lines (232 loc) · 6.88 KB
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
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { cwd } from 'node:process'
import { fileURLToPath } from 'node:url'
import SuriError from './error.js'
/**
* The path to the root directory of this repository.
*
* @private
* @constant {string}
*/
const SURI_DIR_PATH = join(dirname(fileURLToPath(import.meta.url)), '..')
/**
* Tagged template function for composing HTML.
*
* @private
* @function
* @param {string} htmlString The template literal with optional substitutions.
* @returns {string} The raw HTML string of the given template literal.
*/
const html = String.raw
/**
* The default config values that `suri.config.json` supersedes.
*
* @private
* @namespace
* @property {boolean} js Whether to redirect with JavaScript instead of a `<meta>` refresh.
*/
const defaultConfig = {
js: false,
}
/**
* Check whether an error is the result of a file not existing.
*
* @private
* @param {Error} error The error that was thrown.
* @returns {boolean}
*/
function isErrorFileNotExists(error) {
return error.code === 'ENOENT'
}
/**
* Load the config from `suri.config.json`, if it exists, merged with defaults.
*
* @private
* @param {Object} params
* @param {string} params.path The path to the `suri.config.json` file to load.
* @throws {SuriError} If `suri.config.json` fails to be read or parsed.
* @returns {Object} The parsed and merged config.
*/
async function loadConfig({ path }) {
console.log(`Config file: ${path}`)
let config
try {
config = await readFile(path)
} catch (cause) {
if (!isErrorFileNotExists(cause)) {
throw new SuriError('Failed to load config file', { cause })
}
console.log('No config file found, using default config')
return { ...defaultConfig }
}
try {
config = JSON.parse(config)
} catch (cause) {
throw new SuriError('Failed to parse config as JSON', { cause })
}
return {
...defaultConfig,
...config,
}
}
/**
* Load the links from `links.json`.
*
* @private
* @param {Object} params
* @param {string} params.path The path to the `links.json` file to load.
* @throws {SuriError} If `links.json` fails to be loaded or parsed.
* @returns {Object} The parsed links.
*/
async function loadLinks({ path }) {
console.log(`Links file: ${path}`)
let links
try {
links = await readFile(path)
} catch (cause) {
throw new SuriError('Failed to load links file', { cause })
}
try {
links = JSON.parse(links)
} catch (cause) {
throw new SuriError('Failed to parse links as JSON', { cause })
}
return links
}
/**
* Build the HTML page for a link.
*
* @private
* @param {Object} params
* @param {string} params.redirectURL The target URL to redirect to.
* @param {Object} params.config The parsed and merged config.
* @returns {string} The HTML page.
*/
function buildLinkPage({ redirectURL, config }) {
return html`
<!doctype html>
${config.js
? html`
<script>
window.location.replace('${redirectURL}')
</script>
`
: html`<meta http-equiv="refresh" content="0; url=${redirectURL}" />`}
`
}
/**
* Create a link by building the HTML page and saving it to the build directory.
*
* @private
* @param {Object} params
* @param {string} params.linkPath The short link path to redirect from.
* @param {string} params.redirectURL The target URL to redirect to.
* @param {Object} params.config The parsed and merged config.
* @param {string} params.buildDirPath The path to the build directory.
* @throws {SuriError} If the directory/file fails to be created.
* @returns {true} If the link was created.
*/
async function createLink({ linkPath, redirectURL, config, buildDirPath }) {
const linkDirPath = join(buildDirPath, linkPath)
console.log(`Creating link: ${linkPath}`)
try {
await mkdir(linkDirPath, { recursive: true })
} catch (cause) {
throw new SuriError(`Failed to create link directory: ${linkPath}`, {
cause,
})
}
try {
await writeFile(
join(linkDirPath, 'index.html'),
buildLinkPage({ redirectURL, config }),
)
} catch (cause) {
throw new SuriError(`Failed to create link file: ${linkPath}`, { cause })
}
return true
}
/**
* Copy the public directories/files to the build directory.
*
* The directory in this repository of "default" files is copied first, followed
* by the directory in the source directory, if it exists.
*
* @private
* @param {Object} params
* @param {string} params.path The path to the public directory to copy.
* @param {string} params.buildDirPath The path to the build directory.
* @throws {SuriError} If a directory/file fails to be copied.
* @returns {true} If the directories/files were copied.
*/
async function copyPublic({ path, buildDirPath }) {
const publicDirPaths = [join(SURI_DIR_PATH, 'public'), path]
for (const publicDirPath of publicDirPaths) {
console.log(`Copying public directory: ${publicDirPath}`)
try {
await cp(publicDirPath, buildDirPath, {
preserveTimestamps: true,
recursive: true,
})
} catch (cause) {
if (!isErrorFileNotExists(cause)) {
throw new SuriError('Failed to copy public directory', { cause })
}
console.log('No public directory found, skipping')
}
}
return true
}
/**
* Remove the build directory and all of its child directories/files.
*
* @private
* @param {Object} params
* @param {string} params.path The path to the build directory to remove.
* @throws {SuriError} If the directory fails to be removed.
* @returns {undefined} If the directory was removed.
*/
async function removeBuild({ path }) {
try {
return await rm(path, { recursive: true, force: true })
} catch (cause) {
throw new SuriError('Failed to remove build directory', { cause })
}
}
/**
* Build the static site from a `links.json` file.
*
* @memberof module:suri
* @param {Object} [params]
* @param {string} [params.path] The path to the directory to build from. Defaults to the current working directory of the Node.js process.
* @throws {SuriError} If the build fails.
* @returns {true} If the build succeeds.
*/
async function main({ path = cwd() } = {}) {
try {
await removeBuild({ path: join(path, 'build') })
const config = await loadConfig({ path: join(path, 'suri.config.json') })
const links = await loadLinks({ path: join(path, 'src', 'links.json') })
for (const [linkPath, redirectURL] of Object.entries(links)) {
await createLink({
linkPath,
redirectURL,
config,
buildDirPath: join(path, 'build'),
})
}
await copyPublic({
path: join(path, 'public'),
buildDirPath: join(path, 'build'),
})
console.log('Done!')
return true
} catch (error) {
await removeBuild({ path: join(path, 'build') })
throw error
}
}
/** @module suri */
export default main
export { SuriError }