web-documentation 1.0.0
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/build/deploy.js +71 -0
- package/package.json +336 -0
- package/readme.md +37 -0
- package/source/deploy.ts +721 -0
- package/source/dummyDistributionBundle.zip +0 -0
- package/source/dummyReadme.md +266 -0
- package/source/image/arrowDown.png +0 -0
- package/source/image/blacktocat.png +0 -0
- package/source/image/favicon.png +0 -0
- package/source/image/folderBlue.png +0 -0
- package/source/image/folderGray.png +0 -0
- package/source/index.css +492 -0
- package/source/index.html.ejs +303 -0
- package/source/index.ts +431 -0
- package/source/renderMarkdown.mjs +31 -0
- package/source/test.ts +35 -0
- package/source/type.ts +39 -0
package/source/deploy.ts
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
// #!/usr/bin/env babel-node
|
|
2
|
+
// -*- coding: utf-8 -*-
|
|
3
|
+
/** @module deploy */
|
|
4
|
+
'use strict'
|
|
5
|
+
/* !
|
|
6
|
+
region header
|
|
7
|
+
[Project page](https://torben.website/website-utilities)
|
|
8
|
+
|
|
9
|
+
Copyright Torben Sickert (info["~at~"]torben.website) 16.12.2012
|
|
10
|
+
|
|
11
|
+
License
|
|
12
|
+
-------
|
|
13
|
+
|
|
14
|
+
This library written by Torben Sickert stand under a creative commons
|
|
15
|
+
naming 3.0 unported license.
|
|
16
|
+
See https://creativecommons.org/licenses/by/3.0/deed.de
|
|
17
|
+
endregion
|
|
18
|
+
*/
|
|
19
|
+
// region imports
|
|
20
|
+
import archiver from 'archiver'
|
|
21
|
+
import {execSync, ExecSyncOptionsWithStringEncoding} from 'child_process'
|
|
22
|
+
import {
|
|
23
|
+
camelCaseToDelimited,
|
|
24
|
+
evaluate,
|
|
25
|
+
evaluateDynamicData,
|
|
26
|
+
EvaluationResult,
|
|
27
|
+
File,
|
|
28
|
+
isDirectory,
|
|
29
|
+
isFile,
|
|
30
|
+
Logger,
|
|
31
|
+
Mapping,
|
|
32
|
+
optionalRequire,
|
|
33
|
+
PositiveEvaluationResult,
|
|
34
|
+
PlainObject,
|
|
35
|
+
represent,
|
|
36
|
+
walkDirectoryRecursively
|
|
37
|
+
} from 'clientnode'
|
|
38
|
+
import {createReadStream, createWriteStream} from 'fs'
|
|
39
|
+
import {
|
|
40
|
+
copyFile, mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile
|
|
41
|
+
} from 'fs/promises'
|
|
42
|
+
import {tmpdir} from 'node:os'
|
|
43
|
+
import {basename, dirname, extname, join, relative, resolve} from 'path'
|
|
44
|
+
import {Stream} from 'stream'
|
|
45
|
+
import {Extract} from 'unzipper'
|
|
46
|
+
// endregion
|
|
47
|
+
// region types
|
|
48
|
+
interface MAKE_TEMPORARY_FILE_OPTIONS {
|
|
49
|
+
directory: boolean
|
|
50
|
+
encoding?: BufferEncoding | null
|
|
51
|
+
extension: string
|
|
52
|
+
prefix: string
|
|
53
|
+
}
|
|
54
|
+
interface SCOPE_TYPE extends Mapping<unknown> {
|
|
55
|
+
description?: string
|
|
56
|
+
documentationWebsite?: PlainObject
|
|
57
|
+
files?: Array<string>
|
|
58
|
+
main?: string
|
|
59
|
+
name: string
|
|
60
|
+
scripts?: Mapping
|
|
61
|
+
version: string
|
|
62
|
+
}
|
|
63
|
+
// endregion
|
|
64
|
+
const log = new Logger({name: 'web-documentation.deploy'})
|
|
65
|
+
// region globals
|
|
66
|
+
/// region locations
|
|
67
|
+
const DOCUMENTATION_BUILD_PATH = resolve('./build/')
|
|
68
|
+
const DATA_PATH = resolve('./data/')
|
|
69
|
+
const API_DOCUMENTATION_PATHS = ['apiDocumentation/', 'api/']
|
|
70
|
+
let API_DOCUMENTATION_PATH_SUFFIX = '${name}/${version}/'
|
|
71
|
+
const DISTRIBUTION_BUNDLE_FILE_PATH = join(DATA_PATH, 'distributionBundle.zip')
|
|
72
|
+
const DISTRIBUTION_BUNDLE_DIRECTORY_PATH =
|
|
73
|
+
join(DATA_PATH, 'distributionBundle')
|
|
74
|
+
const LOCATIONS_TO_TIDY_UP: Array<string> = []
|
|
75
|
+
/// endregion
|
|
76
|
+
const ALLOW_LOCAL_DOCUMENTATION_WEBSITE = true
|
|
77
|
+
const RUN_FINAL_BUILD = false
|
|
78
|
+
const BUILD_DOCUMENTATION_PAGE_COMMAND_TEMPLATE =
|
|
79
|
+
'`yarn build:web \'{__reference__: "${parametersFilePath}"}\'`'
|
|
80
|
+
const BUILD_DOCUMENTATION_PAGE_CONFIGURATION = {
|
|
81
|
+
module: {
|
|
82
|
+
preprocessor: {
|
|
83
|
+
ejs: {
|
|
84
|
+
options: {
|
|
85
|
+
locals: {__evaluate__: 'parameters'}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
/*
|
|
91
|
+
NOTE: We habe to disable offline features since the domains cache is
|
|
92
|
+
already in use for the main home page.
|
|
93
|
+
*/
|
|
94
|
+
offline: null
|
|
95
|
+
}
|
|
96
|
+
let CONTENT = ''
|
|
97
|
+
const DOCUMENTATION_WEBSITE_NAME = 'web-documentation'
|
|
98
|
+
const DOCUMENTATION_WEBSITE_REPOSITORY =
|
|
99
|
+
// `git@github.com:thaibault/${DOCUMENTATION_WEBSITE_NAME}`
|
|
100
|
+
`https://github.com/thaibault/${DOCUMENTATION_WEBSITE_NAME}.git`
|
|
101
|
+
const PROJECT_PAGE_COMMIT_MESSAGE = 'Update project homepage content.'
|
|
102
|
+
let SCOPE: SCOPE_TYPE = {name: '__dummy__', version: '1.0.0'}
|
|
103
|
+
let HAS_API_DOCUMENTATION = false
|
|
104
|
+
// endregion
|
|
105
|
+
// region functions
|
|
106
|
+
/**
|
|
107
|
+
* Creates temporary directories or provides temporary not yet used file
|
|
108
|
+
* locations.
|
|
109
|
+
* @param givenOptions - Defines options to influence the file path creation.
|
|
110
|
+
* @returns Determined file path.
|
|
111
|
+
*/
|
|
112
|
+
const makeTemporaryFile = async (
|
|
113
|
+
givenOptions: Partial<MAKE_TEMPORARY_FILE_OPTIONS> = {}
|
|
114
|
+
): Promise<string> => {
|
|
115
|
+
const options: MAKE_TEMPORARY_FILE_OPTIONS = {
|
|
116
|
+
directory: false,
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
extension: '',
|
|
119
|
+
prefix: 'DW',
|
|
120
|
+
...givenOptions
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const directoryPath: string =
|
|
124
|
+
await mkdtemp(join(tmpdir(), options.prefix), options.encoding)
|
|
125
|
+
|
|
126
|
+
if (options.directory)
|
|
127
|
+
return directoryPath
|
|
128
|
+
|
|
129
|
+
return join(directoryPath, `${options.prefix}${options.extension}`)
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Provides generic shell execution for given commands. When errors occur (none
|
|
133
|
+
* zero return code) they will result in a thrown exception.
|
|
134
|
+
* @param command - To execute.
|
|
135
|
+
* @param options - To be forward the node's native "execSync" function.
|
|
136
|
+
* @returns The resulting stdout. You need to forward them to console to make
|
|
137
|
+
* it visible.
|
|
138
|
+
*/
|
|
139
|
+
const run = (
|
|
140
|
+
command: string, options: Partial<ExecSyncOptionsWithStringEncoding> = {}
|
|
141
|
+
): string =>
|
|
142
|
+
execSync(command, {encoding: 'utf-8', shell: '/bin/bash', ...options})
|
|
143
|
+
/**
|
|
144
|
+
* Checks if given command throws an error or not.
|
|
145
|
+
* @param parameters - Parameters for "run" function.
|
|
146
|
+
* @returns Boolean indicating whether given command runs successful or not.
|
|
147
|
+
*/
|
|
148
|
+
const checkRun = (
|
|
149
|
+
...parameters: Parameters<typeof run>
|
|
150
|
+
): boolean => {
|
|
151
|
+
try {
|
|
152
|
+
run(...parameters)
|
|
153
|
+
return true
|
|
154
|
+
} catch (_error) {
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Converts a given stream into a buffer.
|
|
161
|
+
* @param stream - To Convert.
|
|
162
|
+
* @returns Converted buffer.
|
|
163
|
+
*/
|
|
164
|
+
const stream2buffer = async (stream: Stream): Promise<Buffer> => {
|
|
165
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
166
|
+
const chunks: Array<Uint8Array> = []
|
|
167
|
+
stream.on('data', (chunk: Uint8Array) => chunks.push(chunk))
|
|
168
|
+
stream.on('end', () => {
|
|
169
|
+
resolve(Buffer.concat(chunks))
|
|
170
|
+
})
|
|
171
|
+
stream.on('error', (error: Error) => {
|
|
172
|
+
reject(error)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Renders a new index.html file and copies new assets to generate a new
|
|
178
|
+
* documentation homepage.
|
|
179
|
+
* @param temporaryDocumentationFolderPath - Location where to build
|
|
180
|
+
* documentation build.
|
|
181
|
+
* @param distributionBundleFilePath - Location where to save the exported
|
|
182
|
+
* build artefacts.
|
|
183
|
+
* @returns A promise resolving when build process has finished.
|
|
184
|
+
*/
|
|
185
|
+
const generateAndPushNewDocumentationPage = async (
|
|
186
|
+
temporaryDocumentationFolderPath: string,
|
|
187
|
+
distributionBundleFilePath: null | string
|
|
188
|
+
): Promise<void> => {
|
|
189
|
+
log.info('Generate document website artefacts.')
|
|
190
|
+
|
|
191
|
+
if (distributionBundleFilePath) {
|
|
192
|
+
log.info('Prepare distribution files.')
|
|
193
|
+
|
|
194
|
+
const newDistributionBundleFilePath = join(
|
|
195
|
+
temporaryDocumentationFolderPath,
|
|
196
|
+
relative('./', DOCUMENTATION_BUILD_PATH),
|
|
197
|
+
relative('./', DISTRIBUTION_BUNDLE_FILE_PATH)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
await mkdir(dirname(newDistributionBundleFilePath), {recursive: true})
|
|
201
|
+
await copyFile(
|
|
202
|
+
distributionBundleFilePath, newDistributionBundleFilePath
|
|
203
|
+
)
|
|
204
|
+
await rm(distributionBundleFilePath)
|
|
205
|
+
|
|
206
|
+
const newDistributionBundleDirectoryPath = join(
|
|
207
|
+
temporaryDocumentationFolderPath,
|
|
208
|
+
relative('./', DOCUMENTATION_BUILD_PATH),
|
|
209
|
+
relative('./', DISTRIBUTION_BUNDLE_DIRECTORY_PATH)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
await mkdir(newDistributionBundleDirectoryPath, {recursive: true})
|
|
213
|
+
|
|
214
|
+
await new Promise<void>((
|
|
215
|
+
resolve: () => void, reject: (reason: Error) => void
|
|
216
|
+
) => {
|
|
217
|
+
createReadStream(newDistributionBundleFilePath)
|
|
218
|
+
.pipe(Extract({path: newDistributionBundleDirectoryPath}))
|
|
219
|
+
.on('close', () => {
|
|
220
|
+
resolve()
|
|
221
|
+
})
|
|
222
|
+
.on('error', (error: Error) => {
|
|
223
|
+
reject(error)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
log.info('Prepare favicon file.')
|
|
229
|
+
const faviconPath = 'favicon.png'
|
|
230
|
+
if (await isFile(faviconPath))
|
|
231
|
+
await copyFile(
|
|
232
|
+
faviconPath,
|
|
233
|
+
`${temporaryDocumentationFolderPath}/source/image/favicon.ico`
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
log.info('Render html.')
|
|
237
|
+
|
|
238
|
+
let parameters: Mapping<unknown> = {}
|
|
239
|
+
for (const [key, value] of Object.entries(
|
|
240
|
+
SCOPE.documentationWebsite || {}
|
|
241
|
+
))
|
|
242
|
+
parameters[camelCaseToDelimited(key).toUpperCase()] = value
|
|
243
|
+
if (!parameters.TAGLINE && SCOPE.description)
|
|
244
|
+
parameters.TAGLINE = SCOPE.description
|
|
245
|
+
if (!parameters.NAME && SCOPE.name)
|
|
246
|
+
parameters.NAME = SCOPE.name
|
|
247
|
+
|
|
248
|
+
log.debug(`Found parameters "${represent(parameters)}" to render.`)
|
|
249
|
+
|
|
250
|
+
let apiDocumentationPath: null | string = null
|
|
251
|
+
if (HAS_API_DOCUMENTATION) {
|
|
252
|
+
apiDocumentationPath =
|
|
253
|
+
API_DOCUMENTATION_PATHS[1] + API_DOCUMENTATION_PATH_SUFFIX
|
|
254
|
+
if (!(await isDirectory(apiDocumentationPath)))
|
|
255
|
+
apiDocumentationPath = API_DOCUMENTATION_PATHS[1]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
parameters = {
|
|
259
|
+
...parameters,
|
|
260
|
+
CONTENT,
|
|
261
|
+
API_DOCUMENTATION_PATH: apiDocumentationPath,
|
|
262
|
+
DISTRIBUTION_BUNDLE_FILE_PATH:
|
|
263
|
+
await isFile(DISTRIBUTION_BUNDLE_FILE_PATH) ?
|
|
264
|
+
relative('./', DISTRIBUTION_BUNDLE_FILE_PATH) :
|
|
265
|
+
null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const [key, value] of Object.entries(parameters))
|
|
269
|
+
if (typeof value === 'string')
|
|
270
|
+
parameters[key] = value.replace('!', '#%%%#')
|
|
271
|
+
|
|
272
|
+
const serializedParameters: string =
|
|
273
|
+
JSON.stringify(evaluateDynamicData(
|
|
274
|
+
BUILD_DOCUMENTATION_PAGE_CONFIGURATION, {parameters, ...SCOPE}
|
|
275
|
+
))
|
|
276
|
+
const parametersFilePath: string =
|
|
277
|
+
await makeTemporaryFile({extension: '.json'})
|
|
278
|
+
await writeFile(parametersFilePath, serializedParameters)
|
|
279
|
+
|
|
280
|
+
const evaluationResult: EvaluationResult = evaluate(
|
|
281
|
+
BUILD_DOCUMENTATION_PAGE_COMMAND_TEMPLATE,
|
|
282
|
+
{parameters, parametersFilePath, ...SCOPE}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if (evaluationResult.error)
|
|
286
|
+
throw new Error(evaluationResult.error)
|
|
287
|
+
|
|
288
|
+
const buildDocumentationPageCommand =
|
|
289
|
+
(evaluationResult as PositiveEvaluationResult).result
|
|
290
|
+
|
|
291
|
+
log.debug(`Use final parameters "${serializedParameters}".`)
|
|
292
|
+
log.info(`Run "${buildDocumentationPageCommand}".`)
|
|
293
|
+
log.debug(run(
|
|
294
|
+
buildDocumentationPageCommand, {cwd: temporaryDocumentationFolderPath}
|
|
295
|
+
))
|
|
296
|
+
await rm(parametersFilePath)
|
|
297
|
+
|
|
298
|
+
for (const filePath of await readdir('./'))
|
|
299
|
+
if (!(
|
|
300
|
+
[
|
|
301
|
+
resolve(temporaryDocumentationFolderPath),
|
|
302
|
+
resolve(API_DOCUMENTATION_PATHS[1])
|
|
303
|
+
].includes(resolve(filePath)) ||
|
|
304
|
+
await isFileIgnored(filePath)
|
|
305
|
+
))
|
|
306
|
+
await rm(filePath, {recursive: true})
|
|
307
|
+
|
|
308
|
+
log.info('Copy all build artefacts.')
|
|
309
|
+
|
|
310
|
+
const documentationBuildFolderPath = join(
|
|
311
|
+
temporaryDocumentationFolderPath,
|
|
312
|
+
relative('./', DOCUMENTATION_BUILD_PATH)
|
|
313
|
+
)
|
|
314
|
+
await walkDirectoryRecursively(
|
|
315
|
+
documentationBuildFolderPath,
|
|
316
|
+
(file: File): Promise<false | undefined> =>
|
|
317
|
+
copyRepositoryFile(documentationBuildFolderPath, './', file)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
await rm(temporaryDocumentationFolderPath, {recursive: true})
|
|
321
|
+
|
|
322
|
+
if (!checkRun('git config user.email'))
|
|
323
|
+
log.debug(run('git config user.email "github_actor@example.com"'))
|
|
324
|
+
if (!checkRun('git config user.name'))
|
|
325
|
+
log.debug(run('git config user.name "github_actor"'))
|
|
326
|
+
|
|
327
|
+
log.debug(run('git add --all'))
|
|
328
|
+
log.debug(
|
|
329
|
+
run(`git commit --message "${PROJECT_PAGE_COMMIT_MESSAGE}" --all`)
|
|
330
|
+
)
|
|
331
|
+
log.debug(run('git push'))
|
|
332
|
+
log.debug(run('git checkout main'))
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Creates a distribution bundle file as zip archiv.
|
|
336
|
+
* @returns Path to build distribution bundle or "null" of building failed.
|
|
337
|
+
*/
|
|
338
|
+
const createDistributionBundle = async (): Promise<null | string> => {
|
|
339
|
+
if (
|
|
340
|
+
SCOPE.scripts &&
|
|
341
|
+
(
|
|
342
|
+
SCOPE.scripts['build:bundle:compatible'] ||
|
|
343
|
+
SCOPE.scripts['build:bundle'] ||
|
|
344
|
+
SCOPE.scripts.build
|
|
345
|
+
)
|
|
346
|
+
) {
|
|
347
|
+
const buildCommand =
|
|
348
|
+
'yarn ' +
|
|
349
|
+
(
|
|
350
|
+
SCOPE.scripts['build:bundle:compatible'] ?
|
|
351
|
+
'build:bundle:compatible' :
|
|
352
|
+
SCOPE.scripts['build:bundle'] ?
|
|
353
|
+
'build:bundle' :
|
|
354
|
+
'build'
|
|
355
|
+
)
|
|
356
|
+
log.info(`Build distribution bundle via "${buildCommand}".`)
|
|
357
|
+
log.debug(run(buildCommand))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
log.info('Pack to a zip archive.')
|
|
361
|
+
const distributionBundleFilePath: string =
|
|
362
|
+
await makeTemporaryFile({extension: '.zip'})
|
|
363
|
+
|
|
364
|
+
const filePaths = SCOPE.files || []
|
|
365
|
+
if (SCOPE.main)
|
|
366
|
+
filePaths.push(SCOPE.main)
|
|
367
|
+
|
|
368
|
+
if (filePaths.length === 0)
|
|
369
|
+
return null
|
|
370
|
+
|
|
371
|
+
const determineFilePaths = async (
|
|
372
|
+
filePaths: Array<string>
|
|
373
|
+
): Promise<Array<string>> => {
|
|
374
|
+
let result: Array<string> = []
|
|
375
|
+
|
|
376
|
+
for (let filePath of filePaths) {
|
|
377
|
+
filePath = resolve(filePath)
|
|
378
|
+
|
|
379
|
+
if (!(await isFileIgnored(filePath)))
|
|
380
|
+
if (await isDirectory(filePath))
|
|
381
|
+
result = result.concat(await determineFilePaths(
|
|
382
|
+
(await readdir(filePath)).map((path: string): string =>
|
|
383
|
+
resolve(filePath, path)
|
|
384
|
+
)
|
|
385
|
+
))
|
|
386
|
+
else {
|
|
387
|
+
log.debug(`Add "${filePath}" to distribution bundle.`)
|
|
388
|
+
|
|
389
|
+
result.push(filePath)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return result
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const archive = archiver('zip', {zlib: {level: 9}})
|
|
397
|
+
archive.pipe(createWriteStream(distributionBundleFilePath))
|
|
398
|
+
|
|
399
|
+
const promise = new Promise<void>((
|
|
400
|
+
resolve: () => void, reject: (reason: Error) => void
|
|
401
|
+
): void => {
|
|
402
|
+
archive.on('error', (error: Error): void => {
|
|
403
|
+
reject(error)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
archive.on('warning', (error: Error): void => {
|
|
407
|
+
log.warn(error)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
archive.on('progress', ({entries: {total, processed}}): void => {
|
|
411
|
+
if (total === processed)
|
|
412
|
+
resolve()
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
for (const filePath of await determineFilePaths(filePaths))
|
|
417
|
+
archive.append(
|
|
418
|
+
await stream2buffer(createReadStream(filePath)),
|
|
419
|
+
{name: relative('./', filePath)}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
await archive.finalize()
|
|
423
|
+
|
|
424
|
+
await promise
|
|
425
|
+
|
|
426
|
+
return distributionBundleFilePath
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Checks if given file path points to a file which should not be distributed
|
|
430
|
+
* for generic reasons.
|
|
431
|
+
* @param filePath - File path to check.
|
|
432
|
+
* @returns Promise wrapping indicating boolean.
|
|
433
|
+
*/
|
|
434
|
+
const isFileIgnored = async (filePath: string): Promise<boolean> => (
|
|
435
|
+
basename(filePath, extname(filePath)).startsWith('.') &&
|
|
436
|
+
!basename(filePath).startsWith('.yarn') ||
|
|
437
|
+
basename(filePath, extname(filePath)) === 'dummyDocumentation' ||
|
|
438
|
+
await isDirectory(filePath) &&
|
|
439
|
+
['node_modules', 'build'].includes(basename(filePath)) ||
|
|
440
|
+
await isFile(filePath) &&
|
|
441
|
+
basename(filePath) === 'params.json'
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Copy the website documentation design repository.
|
|
446
|
+
* @param sourcePath - Location to copy from.
|
|
447
|
+
* @param targetPath - Location where to copy given source.
|
|
448
|
+
* @param file - Location to copy.
|
|
449
|
+
* @returns Promise resolving when finished coping.
|
|
450
|
+
*/
|
|
451
|
+
const copyRepositoryFile = async (
|
|
452
|
+
sourcePath: string, targetPath: string, file: File
|
|
453
|
+
): Promise<false | undefined> => {
|
|
454
|
+
if (await isFileIgnored(file.path) || basename(file.name) === 'readme.md')
|
|
455
|
+
return false
|
|
456
|
+
|
|
457
|
+
targetPath = join(targetPath, relative(sourcePath, file.path))
|
|
458
|
+
|
|
459
|
+
log.debug(`Copy "${file.path}" to "${targetPath}".`)
|
|
460
|
+
|
|
461
|
+
if (file.stats?.isFile())
|
|
462
|
+
await copyFile(file.path, targetPath)
|
|
463
|
+
else
|
|
464
|
+
await mkdir(targetPath)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Merges all readme file.
|
|
469
|
+
* @param file - File to check if it is a readme and should be added to the
|
|
470
|
+
* output content.
|
|
471
|
+
* @returns False or "null" indicating whether the readme file should be
|
|
472
|
+
* ignored.
|
|
473
|
+
*/
|
|
474
|
+
const addReadme = async (file: File): Promise<false | undefined> => {
|
|
475
|
+
if (await isFileIgnored(file.path))
|
|
476
|
+
return false
|
|
477
|
+
|
|
478
|
+
if (basename(file.name, extname(file.name)) === 'readme') {
|
|
479
|
+
log.info(`Handle "${file.path}".`)
|
|
480
|
+
|
|
481
|
+
if (CONTENT)
|
|
482
|
+
CONTENT += '\n'
|
|
483
|
+
|
|
484
|
+
CONTENT += await readFile(file.path, 'utf8')
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Removes build files.
|
|
489
|
+
* @returns A promise resolving to nothing when finished.
|
|
490
|
+
*/
|
|
491
|
+
const tidyUp = async (): Promise<void> => {
|
|
492
|
+
for (const path of LOCATIONS_TO_TIDY_UP)
|
|
493
|
+
await rm(path, {recursive: true})
|
|
494
|
+
|
|
495
|
+
const oldAPIDocumentationDirectoryPath = resolve(API_DOCUMENTATION_PATHS[1])
|
|
496
|
+
if (
|
|
497
|
+
HAS_API_DOCUMENTATION &&
|
|
498
|
+
!(await isDirectory(oldAPIDocumentationDirectoryPath))
|
|
499
|
+
)
|
|
500
|
+
run(`git checkout '${oldAPIDocumentationDirectoryPath}'`)
|
|
501
|
+
|
|
502
|
+
if (!run('git branch').includes('* main'))
|
|
503
|
+
log.debug(run('git checkout main'))
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Main procedure.
|
|
507
|
+
* @returns A promise resolving to nothing when finished.
|
|
508
|
+
*/
|
|
509
|
+
const main = async (): Promise<void> => {
|
|
510
|
+
if (!run('git branch --all').includes('gh-pages')) {
|
|
511
|
+
log.debug(run('git fetch --all'))
|
|
512
|
+
try {
|
|
513
|
+
/*
|
|
514
|
+
NOTE: The issue here that other configuration might
|
|
515
|
+
automatically add a new line at the end of the package manifest
|
|
516
|
+
file.
|
|
517
|
+
*/
|
|
518
|
+
log.debug(run('git checkout package.json'))
|
|
519
|
+
} catch (_error) {
|
|
520
|
+
// Do nothing regardless of an error.
|
|
521
|
+
}
|
|
522
|
+
log.debug(run('git checkout gh-pages'))
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!run('git branch').includes('* main'))
|
|
526
|
+
log.debug(run('git checkout main'))
|
|
527
|
+
|
|
528
|
+
log.debug(run('git pull'))
|
|
529
|
+
|
|
530
|
+
if (
|
|
531
|
+
run('git branch').includes('* main') &&
|
|
532
|
+
run('git branch --all').includes('gh-pages')
|
|
533
|
+
) {
|
|
534
|
+
SCOPE = optionalRequire(resolve('./package.json')) || SCOPE
|
|
535
|
+
|
|
536
|
+
const evaluationResult: EvaluationResult = evaluate(
|
|
537
|
+
`\`${API_DOCUMENTATION_PATH_SUFFIX}\``, SCOPE
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if (evaluationResult.error)
|
|
541
|
+
throw new Error(evaluationResult.error)
|
|
542
|
+
|
|
543
|
+
API_DOCUMENTATION_PATH_SUFFIX =
|
|
544
|
+
(evaluationResult as PositiveEvaluationResult).result
|
|
545
|
+
|
|
546
|
+
log.info('Read and Compile all markdown files and transform to html.')
|
|
547
|
+
|
|
548
|
+
await walkDirectoryRecursively('./', addReadme)
|
|
549
|
+
|
|
550
|
+
let distributionBundleFilePath: null | string = null
|
|
551
|
+
try {
|
|
552
|
+
distributionBundleFilePath = await createDistributionBundle()
|
|
553
|
+
} catch (error) {
|
|
554
|
+
log.error(
|
|
555
|
+
'Error occurred during building distribution bundle:', error
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
process.exitCode = 1
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (
|
|
562
|
+
distributionBundleFilePath &&
|
|
563
|
+
await isFile(distributionBundleFilePath)
|
|
564
|
+
) {
|
|
565
|
+
const targetFilePath =
|
|
566
|
+
join(DATA_PATH, basename(distributionBundleFilePath))
|
|
567
|
+
|
|
568
|
+
await mkdir(DATA_PATH, {recursive: true})
|
|
569
|
+
await copyFile(distributionBundleFilePath, targetFilePath)
|
|
570
|
+
|
|
571
|
+
LOCATIONS_TO_TIDY_UP.push(targetFilePath)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
HAS_API_DOCUMENTATION =
|
|
575
|
+
Boolean(SCOPE.scripts) &&
|
|
576
|
+
Object.prototype.hasOwnProperty.call(SCOPE.scripts, 'document')
|
|
577
|
+
if (HAS_API_DOCUMENTATION)
|
|
578
|
+
try {
|
|
579
|
+
log.debug(run('yarn document'))
|
|
580
|
+
} catch {
|
|
581
|
+
HAS_API_DOCUMENTATION = false
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
log.debug(run('git checkout gh-pages'))
|
|
585
|
+
log.debug(run('git pull'))
|
|
586
|
+
|
|
587
|
+
const apiDocumentationDirectoryPath: string =
|
|
588
|
+
resolve(API_DOCUMENTATION_PATHS[1])
|
|
589
|
+
if (await isDirectory(apiDocumentationDirectoryPath))
|
|
590
|
+
await rm(apiDocumentationDirectoryPath, {recursive: true})
|
|
591
|
+
|
|
592
|
+
if (await isDirectory(API_DOCUMENTATION_PATHS[0])) {
|
|
593
|
+
await rename(
|
|
594
|
+
resolve(API_DOCUMENTATION_PATHS[0]),
|
|
595
|
+
apiDocumentationDirectoryPath
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
LOCATIONS_TO_TIDY_UP.push(apiDocumentationDirectoryPath)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let temporaryDocumentationFolderPath = await makeTemporaryFile({
|
|
602
|
+
directory: true, prefix: DOCUMENTATION_WEBSITE_NAME
|
|
603
|
+
})
|
|
604
|
+
const localDocumentationWebsitePath: string =
|
|
605
|
+
resolve(`../${DOCUMENTATION_WEBSITE_NAME}`)
|
|
606
|
+
|
|
607
|
+
if (
|
|
608
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
609
|
+
ALLOW_LOCAL_DOCUMENTATION_WEBSITE &&
|
|
610
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
611
|
+
await isDirectory(localDocumentationWebsitePath)
|
|
612
|
+
) {
|
|
613
|
+
log.info(`Copy local existing ${DOCUMENTATION_WEBSITE_NAME}.`)
|
|
614
|
+
|
|
615
|
+
await walkDirectoryRecursively(
|
|
616
|
+
localDocumentationWebsitePath,
|
|
617
|
+
(file: File): Promise<false | undefined> =>
|
|
618
|
+
copyRepositoryFile(
|
|
619
|
+
localDocumentationWebsitePath,
|
|
620
|
+
temporaryDocumentationFolderPath,
|
|
621
|
+
file
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
/*
|
|
626
|
+
|
|
627
|
+
const nodeModulesDirectoryPath: string =
|
|
628
|
+
resolve(localDocumentationWebsitePath, 'node_modules')
|
|
629
|
+
if (await isDirectory(nodeModulesDirectoryPath)) {
|
|
630
|
+
// NOTE: Not working caused by nested symlinks.
|
|
631
|
+
const temporaryDocumentationNodeModulesDirectoryPath: string =
|
|
632
|
+
resolve(temporaryDocumentationFolderPath, 'node_modules')
|
|
633
|
+
/*
|
|
634
|
+
We copy just recursively reference files.
|
|
635
|
+
|
|
636
|
+
NOTE: Symlinks doesn't work since some node modules need the
|
|
637
|
+
right absolute location to work.
|
|
638
|
+
|
|
639
|
+
NOTE: Coping complete "node_modules" folder takes to long.
|
|
640
|
+
|
|
641
|
+
NOTE: Mounting "node_modules" folder needs root privileges.
|
|
642
|
+
* /
|
|
643
|
+
log.debug(run(`
|
|
644
|
+
cp \
|
|
645
|
+
--dereference \
|
|
646
|
+
--recursive \
|
|
647
|
+
--reflink=auto \
|
|
648
|
+
'${nodeModulesDirectoryPath}' \
|
|
649
|
+
'${temporaryDocumentationNodeModulesDirectoryPath}'
|
|
650
|
+
`))
|
|
651
|
+
} else
|
|
652
|
+
|
|
653
|
+
*/
|
|
654
|
+
} else {
|
|
655
|
+
log.info(
|
|
656
|
+
`No local existing ${DOCUMENTATION_WEBSITE_NAME} found`,
|
|
657
|
+
'getting it remotely.'
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
log.debug(
|
|
661
|
+
run(
|
|
662
|
+
'unset GIT_WORK_TREE; git clone ' +
|
|
663
|
+
`'${DOCUMENTATION_WEBSITE_REPOSITORY}'`,
|
|
664
|
+
{cwd: temporaryDocumentationFolderPath}
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
temporaryDocumentationFolderPath = resolve(
|
|
669
|
+
temporaryDocumentationFolderPath, DOCUMENTATION_WEBSITE_NAME
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
log.debug(
|
|
674
|
+
run('corepack enable', {cwd: temporaryDocumentationFolderPath})
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
log.debug(
|
|
678
|
+
run('corepack install', {cwd: temporaryDocumentationFolderPath})
|
|
679
|
+
)
|
|
680
|
+
log.debug(run(
|
|
681
|
+
'yarn install',
|
|
682
|
+
{
|
|
683
|
+
cwd: temporaryDocumentationFolderPath,
|
|
684
|
+
env: {...process.env, NODE_ENV: 'debug'}
|
|
685
|
+
}
|
|
686
|
+
))
|
|
687
|
+
log.debug(run('yarn clear', {cwd: temporaryDocumentationFolderPath}))
|
|
688
|
+
|
|
689
|
+
await generateAndPushNewDocumentationPage(
|
|
690
|
+
temporaryDocumentationFolderPath, distributionBundleFilePath
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
// region tidy up
|
|
694
|
+
for (const path of [
|
|
695
|
+
apiDocumentationDirectoryPath,
|
|
696
|
+
DATA_PATH,
|
|
697
|
+
temporaryDocumentationFolderPath
|
|
698
|
+
])
|
|
699
|
+
if (await isDirectory(path))
|
|
700
|
+
await rm(path, {recursive: true})
|
|
701
|
+
// endregion
|
|
702
|
+
|
|
703
|
+
if (
|
|
704
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
705
|
+
RUN_FINAL_BUILD &&
|
|
706
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
707
|
+
Boolean(SCOPE.scripts) &&
|
|
708
|
+
Object.prototype.hasOwnProperty.call(SCOPE.scripts, 'build')
|
|
709
|
+
)
|
|
710
|
+
// Prepare build artefacts for further local usage.
|
|
711
|
+
log.debug(run('yarn build'))
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// endregion
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
await main()
|
|
718
|
+
} catch (error) {
|
|
719
|
+
await tidyUp()
|
|
720
|
+
throw error
|
|
721
|
+
}
|