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.
@@ -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
+ }