From aa60a93601e8f3a3ad1838754f8b79fd1f3266d9 Mon Sep 17 00:00:00 2001 From: Daniel Kuznetsov Date: Fri, 5 Jul 2024 22:52:01 +0300 Subject: [PATCH] fix: apply some rudimentary fixes for outstanding TS issues --- src/@types/nodeError.d.ts | 6 +++ src/cmd/build/index.ts | 32 +++++++----- src/commands/build/index.ts | 18 +++++-- src/commands/publish/index.spec.ts | 1 + src/commands/translate/__tests__/index.ts | 2 +- src/config/index.ts | 2 + src/program/index.ts | 6 +-- src/program/types.ts | 4 +- src/resolvers/lintPage.ts | 8 +-- src/resolvers/md2html.ts | 12 ++++- src/services/contributors.ts | 10 ++-- src/services/includers/batteries/generic.ts | 4 +- src/services/includers/batteries/unarchive.ts | 4 +- src/steps/processAssets.ts | 50 ++++++++----------- src/steps/processPages.ts | 2 + src/utils/markup.ts | 10 ++-- src/validator.ts | 10 ++-- tests/tsconfig.json | 1 + tsconfig.json | 3 +- 19 files changed, 114 insertions(+), 71 deletions(-) create mode 100644 src/@types/nodeError.d.ts diff --git a/src/@types/nodeError.d.ts b/src/@types/nodeError.d.ts new file mode 100644 index 00000000..590783bf --- /dev/null +++ b/src/@types/nodeError.d.ts @@ -0,0 +1,6 @@ +declare interface Error { + name: string; + message: string; + stack?: string; + code?: number | string; +} diff --git a/src/cmd/build/index.ts b/src/cmd/build/index.ts index d8a33ae0..9cfda82c 100644 --- a/src/cmd/build/index.ts +++ b/src/cmd/build/index.ts @@ -21,6 +21,8 @@ import { import {prepareMapFile} from '../../steps/processMapFile'; import {copyFiles, logger} from '../../utils'; import {upload as publishFilesToS3} from '../../commands/publish/upload'; +import {YfmArgv} from '../../models'; +import {Run} from '../../commands/publish'; export const build = { command: ['build', '$0'], @@ -174,7 +176,7 @@ function builder(argv: Argv) { ); } -async function handler(args: Arguments) { +async function handler(args: Arguments) { const userOutputFolder = resolve(args.output); const tmpInputFolder = resolve(args.output, TMP_INPUT_FOLDER); const tmpOutputFolder = resolve(args.output, TMP_OUTPUT_FOLDER); @@ -231,7 +233,6 @@ async function handler(args: Arguments) { outputFormat, outputBundlePath, tmpOutputFolder, - userOutputFolder, }); await processChangelogs(); @@ -255,20 +256,25 @@ async function handler(args: Arguments) { storageSecretKey: secretAccessKey, } = ArgvService.getConfig(); - await publishFilesToS3({ - input: userOutputFolder, - region: storageRegion, - ignore: [...ignore, TMP_INPUT_FOLDER, TMP_OUTPUT_FOLDER], - endpoint, - bucket, - prefix, - accessKeyId, - secretAccessKey, - }); + await publishFilesToS3( + new Run({ + input: userOutputFolder, + region: storageRegion, + hidden: [...ignore, TMP_INPUT_FOLDER, TMP_OUTPUT_FOLDER], + endpoint, + bucket, + prefix, + accessKeyId, + secretAccessKey, + quiet: false, + }), + ); } } } catch (err) { - logger.error('', err.message); + const message = err instanceof Error ? err.message : String(err); + + logger.error('', message); } finally { processLogs(tmpInputFolder); diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index da979e4f..e7d0fdf6 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -1,10 +1,10 @@ -import type {IProgram} from '~/program'; +import type {IProgram, ProgramArgs} from '~/program'; import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; import {Help} from 'commander'; import log from '@diplodoc/transform/lib/log'; -import {BaseProgram} from '~/program/base'; +import {BaseHooks, BaseProgram} from '~/program/base'; import {Command} from '~/config'; import {build} from '~/cmd'; @@ -15,6 +15,10 @@ export type BuildArgs = {}; export type BuildConfig = {}; const parser = yargs + // FIXME: Since we're adding more arguments to the mix with + // `argvValidator` (see src/cmd/build/index.ts#L170), the type safety just completely breaks here + // (which makes sense, because stuff we do is absurdly unsound) + // @ts-expect-error .command(build) .option('config', { alias: 'c', @@ -34,12 +38,12 @@ const parser = yargs type: 'boolean', }) .group(['config', 'strict', 'quiet', 'help', 'version'], 'Common options:') - .version(typeof VERSION !== 'undefined' ? VERSION : '') + .version(typeof VERSION === 'undefined' ? '' : VERSION) .help(); export class Build // eslint-disable-next-line new-cap - extends BaseProgram(command, { + extends BaseProgram(command, { config: { // scope: 'build', defaults: () => ({}), @@ -47,7 +51,8 @@ export class Build command: { isDefault: true, }, - hooks: () => {}, + // FIXME: This is a temporary workaround + hooks: (() => {}) as unknown as BaseHooks, }) implements IProgram { @@ -62,6 +67,9 @@ export class Build this.command.createHelp = function () { const help = new Help(); + // FIXME: This is not supposed to work, since `parser.getHelp` returns a Promise + // I'm not building a sync bridge for this right now + // @ts-expect-error help.formatHelp = () => parser.getHelp(); return help; }; diff --git a/src/commands/publish/index.spec.ts b/src/commands/publish/index.spec.ts index 70fc1e86..3e6cf7f1 100644 --- a/src/commands/publish/index.spec.ts +++ b/src/commands/publish/index.spec.ts @@ -1,4 +1,5 @@ import {describe, expect, it} from 'vitest'; +// @ts-expect-error FIXME import {runPublish as run, testConfig as test} from './__tests__'; describe('Publish command', () => { diff --git a/src/commands/translate/__tests__/index.ts b/src/commands/translate/__tests__/index.ts index 229c9dad..76391cde 100644 --- a/src/commands/translate/__tests__/index.ts +++ b/src/commands/translate/__tests__/index.ts @@ -63,7 +63,7 @@ export function testConfig(defaultArgs: string) { }); if (result instanceof Error || typeof result === 'string') { - const message = result.message || result; + const message = result instanceof Error ? result.message : result; await expect(async () => runTranslate(defaultArgs + ' ' + args)).rejects.toThrow( message, ); diff --git a/src/config/index.ts b/src/config/index.ts index f482108a..1bfbc6b2 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -72,6 +72,8 @@ export function args(command: Command | null) { let args: string[] | undefined; while (command) { + // FIXME: This is probably broken + // @ts-expect-error args = command.rawArgs || args; command = command.parent; } diff --git a/src/program/index.ts b/src/program/index.ts index 6c4b53af..7ce6998d 100644 --- a/src/program/index.ts +++ b/src/program/index.ts @@ -42,13 +42,13 @@ export class Program }), }, }) - implements IProgram + implements IProgram { readonly command: Command = new Command(NAME) .helpOption(true) .allowUnknownOption(false) .version( - typeof VERSION !== 'undefined' ? VERSION : '', + typeof VERSION === 'undefined' ? '' : VERSION, '--version', 'Output the version number', ) @@ -75,7 +75,7 @@ export class Program .helpOption(false) .allowUnknownOption(true); - private readonly modules: ICallable[] = [this.build, this.publish, this.translate]; + private readonly modules: ICallable[] = [this.build, this.publish, this.translate]; async init(argv: string[]) { const args = this.parser.parse(argv).opts() as ProgramArgs; diff --git a/src/program/types.ts b/src/program/types.ts index cd4384f7..9bfd492b 100644 --- a/src/program/types.ts +++ b/src/program/types.ts @@ -5,8 +5,8 @@ import type {Logger} from '~/logger'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Hooks = Record | HookMap>; -export interface ICallable { - apply(program?: IProgram): void; +export interface ICallable { + apply(program?: IProgram): void; } /** diff --git a/src/resolvers/lintPage.ts b/src/resolvers/lintPage.ts index a98ed0b7..db9e3d29 100644 --- a/src/resolvers/lintPage.ts +++ b/src/resolvers/lintPage.ts @@ -1,6 +1,6 @@ import {dirname, relative, resolve} from 'path'; import {load} from 'js-yaml'; -import log from '@diplodoc/transform/lib/log'; +import log, {LogLevels} from '@diplodoc/transform/lib/log'; import { LintMarkdownFunctionOptions, PluginOptions, @@ -76,15 +76,17 @@ function YamlFileLinter(content: string, lintOptions: FileTransformOptions): voi defaultLevel: log.LogLevels.ERROR, }); - const contentLinks = findAllValuesByKeys(load(content), LINK_KEYS); + const contentLinks = findAllValuesByKeys(load(content) as object, LINK_KEYS); const localLinks = contentLinks.filter( (link) => getLinksWithExtension(link) && isLocalUrl(link), ); + const loggerForLogLevel = logLevel === LogLevels.DISABLED ? () => undefined : log[logLevel]; + return localLinks.forEach( (link) => checkPathExists(link, currentFilePath) || - log[logLevel](`Link is unreachable: ${bold(link)} in ${bold(currentFilePath)}`), + loggerForLogLevel(`Link is unreachable: ${bold(link)} in ${bold(currentFilePath)}`), ); } diff --git a/src/resolvers/md2html.ts b/src/resolvers/md2html.ts index 1c59ab18..475369be 100644 --- a/src/resolvers/md2html.ts +++ b/src/resolvers/md2html.ts @@ -114,10 +114,13 @@ export async function resolveMd2HTML(options: ResolverOptions): Promise { const {result} = transformFn(content, {path}); return result?.html; @@ -221,7 +231,7 @@ function MdFileTransformer(content: string, transformOptions: FileTransformOptio return transform(content, { ...options, - plugins: plugins as MarkdownItPluginCb[], + plugins: plugins as MarkdownItPluginCb[], vars, root, path, diff --git a/src/services/contributors.ts b/src/services/contributors.ts index 6d4c2303..2ff7b8aa 100644 --- a/src/services/contributors.ts +++ b/src/services/contributors.ts @@ -71,7 +71,7 @@ async function getContributorsForNestedFiles( try { contentIncludeFile = await readFile(relativeIncludeFilePath, 'utf8'); } catch (err) { - if (err.code === 'ENOENT') { + if (err instanceof Error && err.code === 'ENOENT') { continue; } throw err; @@ -135,14 +135,18 @@ async function getFileIncludes( ); for (const relativeIncludeFilePath of relativeIncludeFilePaths.values()) { const relativeFilePath = relativeIncludeFilePath.substring(inputFolderPathLength + 1); - if (results.has(relativeFilePath)) continue; + + if (results.has(relativeFilePath)) { + continue; + } + results.add(relativeFilePath); let contentIncludeFile: string; try { contentIncludeFile = await readFile(relativeIncludeFilePath, 'utf8'); } catch (err) { - if (err.code === 'ENOENT') { + if (err instanceof Error && err.code === 'ENOENT') { continue; } throw err; diff --git a/src/services/includers/batteries/generic.ts b/src/services/includers/batteries/generic.ts index ece250e1..77e8d3d5 100644 --- a/src/services/includers/batteries/generic.ts +++ b/src/services/includers/batteries/generic.ts @@ -83,7 +83,9 @@ async function includerFunction(params: IncluderFunctionParams) { await writeFile(join(writePath, 'toc.yaml'), dump(toc)); } catch (err) { - throw new GenericIncluderError(err.toString(), tocPath); + const message = err instanceof Error ? err.message : String(err); + + throw new GenericIncluderError(message, tocPath); } } diff --git a/src/services/includers/batteries/unarchive.ts b/src/services/includers/batteries/unarchive.ts index 9de15a25..6fd5b7cb 100644 --- a/src/services/includers/batteries/unarchive.ts +++ b/src/services/includers/batteries/unarchive.ts @@ -92,7 +92,9 @@ async function includerFunction(params: IncluderFunctionParams) { try { await pipeline(contentPath, writePath); } catch (err) { - throw new UnarchiveIncluderError(err.toString(), tocPath); + const message = err instanceof Error ? err.message : String(err); + + throw new UnarchiveIncluderError(message, tocPath); } } diff --git a/src/steps/processAssets.ts b/src/steps/processAssets.ts index 435b0906..ce5b0ac1 100644 --- a/src/steps/processAssets.ts +++ b/src/steps/processAssets.ts @@ -17,7 +17,7 @@ import { RTL_LANGS, YFM_CONFIG_FILENAME, } from '../constants'; -import {Resources} from '../models'; +import {Resources, YfmArgv} from '../models'; import {resolveRelativePath} from '@diplodoc/transform/lib/utilsFS'; /** @@ -29,7 +29,7 @@ import {resolveRelativePath} from '@diplodoc/transform/lib/utilsFS'; */ type Props = { - args: string[]; + args: YfmArgv; outputBundlePath: string; outputFormat: string; tmpOutputFolder: string; @@ -48,7 +48,7 @@ export function processAssets({args, outputFormat, outputBundlePath, tmpOutputFo } } -function processAssetsHtmlRun({outputBundlePath}) { +function processAssetsHtmlRun({outputBundlePath}: Pick) { const {input: inputFolderPath, output: outputFolderPath, langs} = ArgvService.getConfig(); const documentationAssetFilePath: string[] = walkSync(inputFolderPath, { @@ -59,19 +59,21 @@ function processAssetsHtmlRun({outputBundlePath}) { copyFiles(inputFolderPath, outputFolderPath, documentationAssetFilePath); - const hasRTLlang = hasIntersection(langs, RTL_LANGS); + const hasRTLlang = hasIntersection(langs ?? [], RTL_LANGS); const bundleAssetFilePath: string[] = walkSync(ASSETS_FOLDER, { directories: false, includeBasePath: false, - ignore: !hasRTLlang && ['**/*.rtl.css'], + ignore: hasRTLlang ? undefined : ['**/*.rtl.css'], }); copyFiles(ASSETS_FOLDER, outputBundlePath, bundleAssetFilePath); } -function processAssetsMdRun({args, tmpOutputFolder}) { +function processAssetsMdRun({args, tmpOutputFolder}: Pick) { const {input: inputFolderPath, allowCustomResources, resources} = ArgvService.getConfig(); + // FIXME: The way we merge parameters from two Argv sources breaks type safety here + // @ts-expect-error const pathToConfig = args.config || join(args.input, YFM_CONFIG_FILENAME); const pathToRedirects = join(args.input, REDIRECTS_FILENAME); const pathToLintConfig = join(args.input, LINT_CONFIG_FILENAME); @@ -95,14 +97,9 @@ function processAssetsMdRun({args, tmpOutputFolder}) { copyFiles(args.input, tmpOutputFolder, resourcePaths); } - const tocYamlFiles = TocService.getNavigationPaths().reduce((acc, file) => { - if (file.endsWith('.yaml')) { - const resolvedPathToFile = resolve(inputFolderPath, file); - - acc.push(resolvedPathToFile); - } - return acc; - }, []); + const tocYamlFiles = TocService.getNavigationPaths() + .filter((file) => file.endsWith('.yaml')) + .map((file) => resolve(inputFolderPath, file)); tocYamlFiles.forEach((yamlFile) => { const content = load(readFileSync(yamlFile, 'utf8')); @@ -111,30 +108,27 @@ function processAssetsMdRun({args, tmpOutputFolder}) { return; } - const contentLinks = findAllValuesByKeys(content, LINK_KEYS); - const localMediaLinks = contentLinks.reduce( - (acc, link) => { + // FIXME: Better type cast than `object` would be appreciated + const contentLinks = findAllValuesByKeys(content as object, LINK_KEYS); + const localMediaLinks = contentLinks + .filter((link) => { const linkHasMediaExt = new RegExp( /^\S.*\.(svg|png|gif|jpg|jpeg|bmp|webp|ico)$/gm, ).test(link); - if (linkHasMediaExt && isLocalUrl(link) && checkPathExists(link, yamlFile)) { - const linkAbsolutePath = resolveRelativePath(yamlFile, link); - const linkRootPath = linkAbsolutePath.replace(`${inputFolderPath}${sep}`, ''); + return linkHasMediaExt && isLocalUrl(link) && checkPathExists(link, yamlFile); + }) + .map((link) => { + const linkAbsolutePath = resolveRelativePath(yamlFile, link); - acc.push(linkRootPath); - } - return acc; - }, - - [], - ); + return linkAbsolutePath.replace(`${inputFolderPath}${sep}`, ''); + }); copyFiles(args.input, tmpOutputFolder, localMediaLinks); }); } -function hasIntersection(array1, array2) { +function hasIntersection(array1: unknown[], array2: unknown[]) { const set1 = new Set(array1); return array2.some((element) => set1.has(element)); } diff --git a/src/steps/processPages.ts b/src/steps/processPages.ts index a6ba56d9..29a62612 100644 --- a/src/steps/processPages.ts +++ b/src/steps/processPages.ts @@ -175,6 +175,8 @@ async function saveSinglePages() { const singlePageFn = join(tocDir, SINGLE_PAGE_FILENAME); const singlePageDataFn = join(tocDir, SINGLE_PAGE_DATA_FILENAME); const singlePageContent = generateStaticMarkup( + // FIXME: Whatever we do here with page props is pretty unsound + // @ts-expect-error pageData, toc?.root?.deepBase || toc?.deepBase || 0, ); diff --git a/src/utils/markup.ts b/src/utils/markup.ts index 6b854faf..0b21f64b 100644 --- a/src/utils/markup.ts +++ b/src/utils/markup.ts @@ -174,7 +174,7 @@ export function replaceDoubleToSingleQuotes(str: string): string { return str.replace(/"/g, "'"); } -export function findAllValuesByKeys(obj, keysToFind: string[]) { +export function findAllValuesByKeys(obj: object, keysToFind: string[]): string[] { return flatMapDeep(obj, (value: string | string[], key: string) => { if ( keysToFind?.includes(key) && @@ -192,14 +192,16 @@ export function findAllValuesByKeys(obj, keysToFind: string[]) { } export function modifyValuesByKeys( - originalObj, + originalObj: object, keysToFind: string[], modifyFn: (value: string) => string, ) { - function customizer(value, key) { - if (keysToFind?.includes(key) && isString(value)) { + function customizer(value: unknown, key: number | string | undefined) { + if (typeof key === 'string' && keysToFind?.includes(key) && isString(value)) { return modifyFn(value); } + + return undefined; } // Clone the object deeply with a customizer function that modifies matching keys diff --git a/src/validator.ts b/src/validator.ts index f0289244..40fb2c81 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,7 +1,7 @@ import {Arguments} from 'yargs'; import {join, resolve} from 'path'; import {readFileSync} from 'fs'; -import {load} from 'js-yaml'; +import {YAMLException, load} from 'js-yaml'; import merge from 'lodash/merge'; import log from '@diplodoc/transform/lib/log'; import {LINT_CONFIG_FILENAME, REDIRECTS_FILENAME, YFM_CONFIG_FILENAME} from './constants'; @@ -95,7 +95,7 @@ export function argvValidator(argv: Arguments): Boolean { const content = readFileSync(resolve(pathToConfig), 'utf8'); Object.assign(argv, load(content) || {}); } catch (error) { - if (error.name === 'YAMLException') { + if (error instanceof YAMLException) { log.error(`Error to parse ${YFM_CONFIG_FILENAME}: ${error.message}`); } } @@ -107,7 +107,7 @@ export function argvValidator(argv: Arguments): Boolean { lintConfig = load(content) || {}; } catch (error) { - if (error.name === 'YAMLException') { + if (error instanceof YAMLException) { log.error(`Error to parse ${LINT_CONFIG_FILENAME}: ${error.message}`); } } finally { @@ -127,11 +127,11 @@ export function argvValidator(argv: Arguments): Boolean { validateRedirects(redirects as RedirectsConfig, pathToRedirects); } catch (error) { - if (error.name === 'YAMLException') { + if (error instanceof YAMLException) { log.error(`Error to parse ${REDIRECTS_FILENAME}: ${error.message}`); } - if (error.code !== 'ENOENT') { + if (error instanceof Error && error.code !== 'ENOENT') { throw error; } } diff --git a/tests/tsconfig.json b/tests/tsconfig.json index ac232ae6..0bad774c 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "es2019", "moduleResolution": "NodeNext", + "module": "NodeNext", "esModuleInterop": true, "paths": { "*": [ diff --git a/tsconfig.json b/tsconfig.json index 221a48de..61e9c69b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "lib": ["ES2019"], "target": "es6", "outDir": "build", - "module": "CommonJS", + "module": "NodeNext", + "moduleResolution": "NodeNext", "paths": { "~/*": ["./src/*"] }