diff --git a/src/commands/build/__tests__/index.ts b/src/commands/build/__tests__/index.ts index 70d9841a..de093631 100644 --- a/src/commands/build/__tests__/index.ts +++ b/src/commands/build/__tests__/index.ts @@ -1,7 +1,9 @@ import type {Run} from '../run'; import type {BuildConfig, BuildRawConfig} from '..'; +import {join} from 'node:path'; import {Mock, describe, expect, it, vi} from 'vitest'; +import {when} from 'vitest-when'; import {Build} from '..'; import {handler as originalHandler} from '../handler'; import {withConfigUtils} from '~/config'; @@ -30,15 +32,53 @@ vi.mock('~/config', async (importOriginal) => { }; }); -export async function runBuild(args: string) { +type BuildState = { + globs?: Hash; + files?: Hash; +}; +export function setupBuild(state: BuildState = {}): Build & {run: Run} { const build = new Build(); build.apply(); build.hooks.BeforeAnyRun.tap('Tests', (run) => { + (build as Build & {run: Run}).run = run; + + // @ts-ignore + run.glob = vi.fn(() => []); run.copy = vi.fn(); run.write = vi.fn(); + run.fs.writeFile = vi.fn(); + // @ts-ignore + run.fs.readFile = vi.fn(); + // @ts-ignore + run.logger.proc = vi.fn(); + // @ts-ignore + run.logger.info = vi.fn(); + // @ts-ignore + run.logger.warn = vi.fn(); + // @ts-ignore + run.logger.error = vi.fn(); + + if (state.globs) { + for (const [pattern, files] of Object.entries(state.globs)) { + when(run.glob).calledWith(pattern, expect.anything()).thenResolve(files); + } + } + + if (state.files) { + for (const [file, content] of Object.entries(state.files)) { + when(run.fs.readFile) + .calledWith(join(run.input, file), expect.anything()) + .thenResolve(content); + } + } }); + return build as Build & {run: Run}; +} + +export async function runBuild(args: string, build?: Build) { + build = build || setupBuild(); await build.parse(['node', 'index'].concat(args.split(' '))); } diff --git a/src/commands/build/core/vars/VarsService.ts b/src/commands/build/core/vars/VarsService.ts new file mode 100644 index 00000000..4fee897d --- /dev/null +++ b/src/commands/build/core/vars/VarsService.ts @@ -0,0 +1,105 @@ +import type {Preset, Presets} from './types'; + +import {dirname, join} from 'node:path'; +import {merge} from 'lodash'; +import {dump, load} from 'js-yaml'; + +import {Run} from '~/commands/build'; +import {freeze, own} from '~/utils'; +import {AsyncParallelHook, AsyncSeriesWaterfallHook} from 'tapable'; + +export type VarsServiceConfig = { + varsPreset: string; + vars: Hash; +}; + +type VarsServiceHooks = { + /** + * Async waterfall hook. + * Called after any presets.yaml was loaded. + */ + PresetsLoaded: AsyncSeriesWaterfallHook<[Presets, RelativePath]>; + /** + * Async parallel hook. + * Called after vars was resolved on any level. + * Vars data is sealed here. + */ + Resolved: AsyncParallelHook<[Preset, RelativePath]>; +}; + +export class VarsService { + hooks: VarsServiceHooks; + + private run: Run; + + private fs: Run['fs']; + + private logger: Run['logger']; + + private config: VarsServiceConfig; + + private cache: Record = {}; + + constructor(run: Run) { + this.run = run; + this.fs = run.fs; + this.logger = run.logger; + this.config = run.config; + this.hooks = { + PresetsLoaded: new AsyncSeriesWaterfallHook(['presets', 'path']), + Resolved: new AsyncParallelHook(['vars', 'path']), + }; + } + + async load(path: RelativePath) { + const varsPreset = this.config.varsPreset || 'default'; + const file = join(dirname(path), 'presets.yaml'); + + if (this.cache[file]) { + return this.cache[file]; + } + + this.logger.proc(path); + + const scopes = []; + + if (dirname(path) !== '.') { + scopes.push(await this.load(dirname(path))); + } + + try { + const presets = await this.hooks.PresetsLoaded.promise( + load(await this.fs.readFile(join(this.run.input, file), 'utf8')) as Presets, + file, + ); + + scopes.push(presets['default']); + + if (varsPreset && varsPreset !== 'default') { + scopes.push(presets[varsPreset] || {}); + } + } catch (error) { + if (!own(error, 'code') || error.code !== 'ENOENT') { + throw error; + } + } + + scopes.push(this.config.vars); + + this.cache[file] = freeze(merge({}, ...scopes)); + + await this.hooks.Resolved.promise(this.cache[file], file); + + return this.cache[file]; + } + + dump(presets: Hash): string { + return dump(presets, { + lineWidth: 120, + }); + } + + entries() { + return Object.entries(this.cache); + } +} diff --git a/src/commands/build/core/vars/__snapshots__/index.spec.ts.snap b/src/commands/build/core/vars/__snapshots__/index.spec.ts.snap new file mode 100644 index 00000000..115d72ec --- /dev/null +++ b/src/commands/build/core/vars/__snapshots__/index.spec.ts.snap @@ -0,0 +1,57 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`vars > service > load > should allow content extending in PresetsLoaded hook 1`] = ` +"field1: value1 +field2: value2 +" +`; + +exports[`vars > service > load > should allow content updating in PresetsLoaded hook 1`] = ` +"field1: value2 +" +`; + +exports[`vars > service > load > should load presets file default scope 1`] = ` +"field1: value1 +field2: value2 +" +`; + +exports[`vars > service > load > should load presets file target scope 1`] = ` +"field1: value3 +field2: value2 +" +`; + +exports[`vars > service > load > should load super layers 1`] = ` +"field1: value1 +override1: value1 +override2: value1 +override3: value1 +override4: value1 +field2: value1 +sub1: value1 +sub2: value1 +override5: value1 +override6: value1 +subsub1: value1 +subsub2: value1 +" +`; + +exports[`vars > service > load > should override default presets with vars 1`] = ` +"field1: value6 +field2: value2 +" +`; + +exports[`vars > service > load > should override target presets with vars 1`] = ` +"field1: value6 +field2: value2 +" +`; + +exports[`vars > service > load > should use vars if presets not found 1`] = ` +"field1: value6 +" +`; diff --git a/src/commands/build/core/vars/index.spec.ts b/src/commands/build/core/vars/index.spec.ts new file mode 100644 index 00000000..c12a68c2 --- /dev/null +++ b/src/commands/build/core/vars/index.spec.ts @@ -0,0 +1,294 @@ +import type {Run} from '~/commands/build'; +import type {VarsServiceConfig} from './VarsService'; + +import {join} from 'node:path'; +import {describe, expect, it, vi} from 'vitest'; +import {when} from 'vitest-when'; +import {dedent} from 'ts-dedent'; +import {YAMLException} from 'js-yaml'; + +import {VarsService} from './VarsService'; + +const ENOENT = Object.assign(new Error('ENOENT: no such file or directory'), { + code: 'ENOENT', +}); + +type Options = Partial; + +function prepare(content: string | Hash | Error, options: Options = {}) { + const input = '/dev/null/input' as AbsolutePath; + const output = '/dev/null/output' as AbsolutePath; + const run = { + input, + output, + config: { + varsPreset: options.varsPreset, + vars: options.vars || {}, + }, + logger: { + proc: vi.fn(), + }, + fs: { + readFile: vi.fn(), + }, + } as unknown as Run; + const service = new VarsService(run); + + if (content instanceof Error) { + when(run.fs.readFile) + .calledWith(join(input, './presets.yaml'), expect.anything()) + .thenReject(content); + } else { + if (typeof content === 'string') { + content = {'./presets.yaml': content}; + } + + for (const [file, data] of Object.entries(content)) { + when(run.fs.readFile) + .calledWith(join(input, file), expect.anything()) + .thenResolve(data); + } + } + + return service; +} + +async function call(content: string | Error, options: Options = {}) { + const service = prepare(content, options); + const result = await service.load('./presets.yaml' as RelativePath); + + expect(service.dump(result)).toMatchSnapshot(); +} + +function test(name: string, content: string | Error, options: Options = {}) { + it(name, async () => call(content, options)); +} + +describe('vars', () => { + describe('service', () => { + describe('load', () => { + test( + 'should load presets file default scope', + dedent` + default: + field1: value1 + field2: value2 + internal: + field1: value3 + external: + field1: value4 + `, + ); + + test( + 'should load presets file target scope', + dedent` + default: + field1: value1 + field2: value2 + internal: + field1: value3 + external: + field1: value4 + `, + {varsPreset: 'internal'}, + ); + + test( + 'should override default presets with vars', + dedent` + default: + field1: value1 + field2: value2 + internal: + field1: value3 + external: + field1: value4 + `, + {vars: {field1: 'value6'}}, + ); + + test( + 'should override target presets with vars', + dedent` + default: + field1: value1 + field2: value2 + internal: + field1: value3 + external: + field1: value4 + `, + {varsPreset: 'internal', vars: {field1: 'value6'}}, + ); + + test('should use vars if presets not found', ENOENT, {vars: {field1: 'value6'}}); + + it('should throw parse error', async () => { + await expect(() => call('!@#', {vars: {field1: 'value6'}})).rejects.toThrow( + YAMLException, + ); + }); + + it('should load super layers', async () => { + const service = prepare( + { + './presets.yaml': dedent` + default: + field1: value1 + override1: value2 + override2: value2 + override3: value2 + override4: value2 + internal: + field2: value1 + override1: value1 + `, + './subfolder/presets.yaml': dedent` + default: + sub1: value1 + sub2: value2 + override2: value1 + override5: value2 + internal: + sub2: value1 + override3: value1 + override6: value2 + `, + './subfolder/subfolder/subfolder/presets.yaml': dedent` + default: + subsub1: value2 + override4: value2 + override5: value1 + internal: + subsub1: value1 + subsub2: value1 + override4: value1 + override6: value1 + `, + }, + {varsPreset: 'internal'}, + ); + + const result = await service.load( + './subfolder/subfolder/subfolder/presets.yaml' as RelativePath, + ); + + expect(service.dump(result)).toMatchSnapshot(); + }); + + it('should call PresetsLoaded hook', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + const spy = vi.fn(); + + service.hooks.PresetsLoaded.tap('Test', spy); + + await service.load('./presets.yaml' as RelativePath); + + expect(spy).toHaveBeenCalledWith({default: {field1: 'value1'}}, 'presets.yaml'); + }); + + it('should call Resolved hook', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + const spy = vi.fn(); + + service.hooks.Resolved.tap('Test', spy); + + await service.load('./presets.yaml' as RelativePath); + + expect(spy).toHaveBeenCalledWith({field1: 'value1'}, 'presets.yaml'); + }); + + it('should allow content updating in PresetsLoaded hook', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + service.hooks.PresetsLoaded.tap('Test', (presets) => { + presets.default.field1 = 'value2'; + + return presets; + }); + + const result = await service.load('./presets.yaml' as RelativePath); + + expect(service.dump(result)).toMatchSnapshot(); + }); + + it('should allow content extending in PresetsLoaded hook', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + service.hooks.PresetsLoaded.tap('Test', (presets) => { + presets.default.field2 = 'value2'; + + return presets; + }); + + const result = await service.load('./presets.yaml' as RelativePath); + + expect(service.dump(result)).toMatchSnapshot(); + }); + + it('should reject content updating in Resolved hook', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + service.hooks.Resolved.tap('Test', (vars) => { + vars.field1 = 'value2'; + }); + + await expect(() => + service.load('./presets.yaml' as RelativePath), + ).rejects.toThrow(); + }); + + it('should reject content extending in Resolved hook', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + service.hooks.Resolved.tap('Test', (vars) => { + vars.field2 = 'value2'; + }); + + await expect(() => + service.load('./presets.yaml' as RelativePath), + ).rejects.toThrow(); + }); + + it('should load content only once', async () => { + const service = prepare(dedent` + default: + field1: value1 + `); + + const spy1 = vi.fn(); + const spy2 = vi.fn(); + + service.hooks.PresetsLoaded.tap('Test', spy1); + service.hooks.Resolved.tap('Test', spy2); + + await service.load('./presets.yaml' as RelativePath); + await service.load('./presets.yaml' as RelativePath); + + expect(spy1).toHaveBeenCalledOnce(); + expect(spy2).toHaveBeenCalledOnce(); + }); + }); + }); +}); diff --git a/src/commands/build/core/vars/index.ts b/src/commands/build/core/vars/index.ts new file mode 100644 index 00000000..7f666f0d --- /dev/null +++ b/src/commands/build/core/vars/index.ts @@ -0,0 +1,3 @@ +export type {Preset, Presets} from './types'; + +export {VarsService} from './VarsService'; diff --git a/src/commands/build/core/vars/types.ts b/src/commands/build/core/vars/types.ts new file mode 100644 index 00000000..2dbb23d8 --- /dev/null +++ b/src/commands/build/core/vars/types.ts @@ -0,0 +1,10 @@ +export type Presets = { + default: Preset; +} & { + [prop: string]: Preset; +}; + +export type Preset = { + __system?: Hash; + __metadata?: Hash; +} & Hash; diff --git a/src/commands/build/features/templating/index.spec.ts b/src/commands/build/features/templating/index.spec.ts index 3c1befc2..ff220f81 100644 --- a/src/commands/build/features/templating/index.spec.ts +++ b/src/commands/build/features/templating/index.spec.ts @@ -1,5 +1,7 @@ -import {describe} from 'vitest'; -import {testConfig as test} from '../../__tests__'; +import {describe, expect, it} from 'vitest'; +import {runBuild, setupBuild, testConfig as test} from '../../__tests__'; +import {resolve} from 'node:path'; +import {dedent} from 'ts-dedent'; describe('Build template feature', () => { describe('config', () => { @@ -51,6 +53,10 @@ describe('Build template feature', () => { text: false, code: false, }, + features: { + conditions: false, + substitutions: false, + }, }, }); @@ -203,4 +209,76 @@ describe('Build template feature', () => { ); }); }); + + describe('run', () => { + const args = (...args: string[]) => + '-i /dev/null/input -o /dev/null/output ' + args.join(' '); + + it('should not save presets.yaml for html build', async () => { + const build = setupBuild({ + globs: { + '**/presets.yaml': ['./presets.yaml'], + }, + files: { + './presets.yaml': dedent` + default: + field: value + `, + }, + }); + + await runBuild(args('-f', 'html', '--no-template'), build); + + expect(build.run.write).not.toHaveBeenCalledWith( + resolve('/dev/null/output/.tmp_output/presets.yaml'), + `default:\n field: value\n`, + ); + }); + + it('should save presets.yaml for md build with disabled templating', async () => { + const build = setupBuild({ + globs: { + '**/presets.yaml': ['./presets.yaml'], + }, + files: { + './presets.yaml': dedent` + default: + field: value + `, + }, + }); + + await runBuild(args('-f', 'md', '--no-template'), build); + + expect(build.run.write).toHaveBeenCalledWith( + resolve('/dev/null/output/.tmp_output/presets.yaml'), + `default:\n field: value\n`, + ); + }); + + it('should filter presets.yaml for md build with disabled templating', async () => { + const build = setupBuild({ + globs: { + '**/presets.yaml': ['./presets.yaml'], + }, + files: { + './presets.yaml': dedent` + default: + field: value + internal: + field: value + external: + field: value + `, + }, + }); + + await runBuild(args('-f', 'md', '--no-template', '--vars-preset', 'internal'), build); + + expect(build.run.write).toHaveBeenCalledWith( + resolve('/dev/null/output/.tmp_output/presets.yaml'), + `default:\n field: value\ninternal:\n field: value\n`, + ); + }); + }); }); diff --git a/src/commands/build/features/templating/index.ts b/src/commands/build/features/templating/index.ts index 6fab837f..e159a726 100644 --- a/src/commands/build/features/templating/index.ts +++ b/src/commands/build/features/templating/index.ts @@ -1,21 +1,13 @@ import type {Build} from '~/commands'; import type {Command} from '~/config'; -import {defined, valuable} from '~/config'; -import {options} from './config'; +import type {Preset} from '~/commands/build/core/vars'; -const merge = (acc: Hash, ...sources: Hash[]) => { - for (const source of sources) { - for (const [key, value] of Object.entries(source)) { - if (!acc[key] || !value) { - acc[key] = value; - } else if (typeof value === 'object') { - acc[key] = merge({}, acc[key], value); - } - } - } +import {join} from 'node:path'; +import {dump} from 'js-yaml'; +import {merge} from 'lodash'; - return acc; -}; +import {defined, valuable} from '~/config'; +import {options} from './config'; export type TemplatingArgs = { template?: boolean | 'all' | 'text' | 'code'; @@ -88,7 +80,38 @@ export class Templating { config.template.features.conditions = templateConditions; } + if (!config.template.enabled) { + config.template.features.substitutions = false; + config.template.features.conditions = false; + } + return config; }); + + program.hooks.BeforeRun.for('md').tap('Build', (run) => { + const {varsPreset, template} = run.config; + const {substitutions, conditions} = template.features; + + // For case when we need to copy project from private to public repo and filter private presets. + if (!substitutions || !conditions) { + run.vars.hooks.PresetsLoaded.tapPromise('Build', async (presets, path) => { + const scopes = [ + {default: presets.default}, + varsPreset !== 'default' && + presets[varsPreset] && {[varsPreset]: presets[varsPreset]}, + ].filter(Boolean) as Preset[]; + const result = merge({}, ...scopes); + + await run.write( + join(run.output, path), + dump(result, { + lineWidth: 120, + }), + ); + + return presets; + }); + } + }); } } diff --git a/src/commands/build/handler.ts b/src/commands/build/handler.ts index e37859fa..abe17161 100644 --- a/src/commands/build/handler.ts +++ b/src/commands/build/handler.ts @@ -4,16 +4,16 @@ import 'threads/register'; import OpenapiIncluder from '@diplodoc/openapi-extension/includer'; -import {ArgvService, Includers, SearchService} from '~/services'; +import {ArgvService, Includers, PresetService, SearchService} from '~/services'; import { initLinterWorkers, + preparingTocFiles, processAssets, processChangelogs, processExcludedFiles, processLinter, processLogs, processPages, - processServiceFiles, } from '~/steps'; import {prepareMapFile} from '~/steps/processMapFile'; @@ -27,7 +27,8 @@ export async function handler(run: Run) { const {lintDisabled, buildDisabled, addMapFile} = ArgvService.getConfig(); - await processServiceFiles(); + PresetService.init(run.vars); + await preparingTocFiles(run); processExcludedFiles(); if (addMapFile) { diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index daa26ba5..acfe3472 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -2,7 +2,6 @@ import type {IProgram, ProgramArgs, ProgramConfig} from '~/program'; import type {DocAnalytics} from '@diplodoc/client'; import {ok} from 'node:assert'; -import {join} from 'node:path'; import {pick} from 'lodash'; import {AsyncParallelHook, AsyncSeriesHook, HookMap} from 'tapable'; @@ -286,6 +285,14 @@ export class Build await run.copy(run.originalInput, run.input, ['node_modules/**', '*/node_modules/**']); + const presets = (await run.glob('**/presets.yaml', { + cwd: run.input, + ignore: run.config.ignore, + })) as RelativePath[]; + for (const preset of presets) { + await run.vars.load(preset); + } + await Promise.all([handler(run), this.hooks.Run.promise(run)]); await this.hooks.AfterRun.for(this.config.outputFormat).promise(run); diff --git a/src/commands/build/run.ts b/src/commands/build/run.ts index 7359aff9..f563df66 100644 --- a/src/commands/build/run.ts +++ b/src/commands/build/run.ts @@ -1,4 +1,5 @@ import type {YfmArgv} from '~/models'; +import type {GlobOptions} from 'glob'; // import {ok} from 'node:assert'; import {dirname, join, resolve} from 'node:path'; @@ -13,9 +14,10 @@ import { TMP_OUTPUT_FOLDER, YFM_CONFIG_FILENAME, } from '~/constants'; -import {Logger} from '~/logger'; +import {LogLevel, Logger} from '~/logger'; import {BuildConfig} from '.'; // import {InsecureAccessError} from './errors'; +import {VarsService} from './core/vars'; type FileSystem = { access: typeof access; @@ -27,6 +29,10 @@ type FileSystem = { writeFile: typeof writeFile; }; +class RunLogger extends Logger { + proc = this.topic(LogLevel.INFO, 'PROC'); +} + /** * This is transferable context for build command. * Use this context to communicate with lower data processing levels. @@ -42,12 +48,14 @@ export class Run { readonly legacyConfig: YfmArgv; - readonly logger: Logger; + readonly logger: RunLogger; readonly config: BuildConfig; readonly fs: FileSystem = {access, stat, link, unlink, mkdir, readFile, writeFile}; + readonly vars: VarsService; + get bundlePath() { return join(this.output, BUNDLE_FOLDER); } @@ -72,6 +80,11 @@ export class Run { this.input = resolve(config.output, TMP_INPUT_FOLDER); this.output = resolve(config.output, TMP_OUTPUT_FOLDER); + this.logger = new RunLogger(config, [ + (_level, message) => message.replace(new RegExp(this.input, 'ig'), ''), + ]); + + this.vars = new VarsService(this); this.legacyConfig = { rootInput: this.originalInput, input: this.input, @@ -119,10 +132,6 @@ export class Run { included: config.mergeIncludes, }; - - this.logger = new Logger(config, [ - (_level, message) => message.replace(new RegExp(this.input, 'ig'), ''), - ]); } write = async (path: AbsolutePath, content: string | Buffer) => { @@ -131,6 +140,15 @@ export class Run { await this.fs.writeFile(path, content, 'utf8'); }; + glob = async (pattern: string | string[], options: GlobOptions) => { + return glob(pattern, { + dot: true, + nodir: true, + follow: true, + ...options, + }); + }; + copy = async (from: AbsolutePath, to: AbsolutePath, ignore?: string[]) => { const isFile = (await this.fs.stat(from)).isFile(); const hardlink = async (from: AbsolutePath, to: AbsolutePath) => { @@ -154,12 +172,8 @@ export class Run { } const dirs = new Set(); - // TODO: check dotfiles copy - const files = (await glob('**', { + const files = (await this.glob('**', { cwd: from, - dot: true, - nodir: true, - follow: true, ignore, })) as RelativePath[]; diff --git a/src/services/preset.ts b/src/services/preset.ts index c24533ba..13694c85 100644 --- a/src/services/preset.ts +++ b/src/services/preset.ts @@ -1,43 +1,30 @@ import {dirname, normalize} from 'path'; -import {DocPreset, YfmPreset} from '../models'; +import {YfmPreset} from '../models'; +import {VarsService} from '~/commands/build/core/vars'; export type PresetStorage = Map; let presetStorage: PresetStorage = new Map(); -function add(parsedPreset: DocPreset, path: string, varsPreset: string) { - const combinedValues = { - ...(parsedPreset.default || {}), - ...(parsedPreset[varsPreset] || {}), - __metadata: parsedPreset.__metadata, - } as YfmPreset; - - const key = dirname(normalize(path)); - presetStorage.set(key, combinedValues); +function init(vars: VarsService) { + for (const [path, values] of vars.entries()) { + presetStorage.set(dirname(path), values); + } } function get(path: string): YfmPreset { - let combinedValues: YfmPreset = {}; - let localPath = normalize(path); - - while (localPath !== '.') { - const presetValues: YfmPreset = presetStorage.get(localPath) || {}; - localPath = dirname(localPath); - - combinedValues = { - ...presetValues, - ...combinedValues, - }; + let vars = presetStorage.get(normalize(path)); + while (!vars) { + path = dirname(path); + vars = presetStorage.get(normalize(path)); + + if (path === '.') { + break; + } } - // Add root' presets - combinedValues = { - ...presetStorage.get('.'), - ...combinedValues, - }; - - return combinedValues; + return vars || {}; } function getPresetStorage(): Map { @@ -49,7 +36,7 @@ function setPresetStorage(preset: Map): void { } export default { - add, + init, get, getPresetStorage, setPresetStorage, diff --git a/src/steps/processServiceFiles.ts b/src/steps/processServiceFiles.ts index 7c315264..9c1fafe1 100644 --- a/src/steps/processServiceFiles.ts +++ b/src/steps/processServiceFiles.ts @@ -1,13 +1,7 @@ -import {dirname, resolve} from 'path'; import walkSync from 'walk-sync'; -import {readFileSync, writeFileSync} from 'fs'; -import {dump, load} from 'js-yaml'; import log from '@diplodoc/transform/lib/log'; -import {ArgvService, PresetService, TocService} from '../services'; -import {logger} from '../utils'; -import {DocPreset} from '../models'; -import shell from 'shelljs'; +import {ArgvService, TocService} from '../services'; const getFilePathsByGlobals = (globs: string[]): string[] => { const {input, ignore = []} = ArgvService.getConfig(); @@ -20,64 +14,7 @@ const getFilePathsByGlobals = (globs: string[]): string[] => { }); }; -export async function processServiceFiles(): Promise { - await preparingPresetFiles(); - await preparingTocFiles(); -} - -async function preparingPresetFiles() { - const { - input: inputFolderPath, - varsPreset = '', - outputFormat, - applyPresets, - resolveConditions, - } = ArgvService.getConfig(); - - try { - const presetsFilePaths = getFilePathsByGlobals(['**/presets.yaml']); - - for (const path of presetsFilePaths) { - logger.proc(path); - - const pathToPresetFile = resolve(inputFolderPath, path); - const content = readFileSync(pathToPresetFile, 'utf8'); - const parsedPreset = load(content) as DocPreset; - - PresetService.add(parsedPreset, path, varsPreset); - - if (outputFormat === 'md' && (!applyPresets || !resolveConditions)) { - // Should save filtered presets.yaml only when --apply-presets=false or --resolve-conditions=false - saveFilteredPresets(path, parsedPreset); - } - } - } catch (error) { - log.error(`Preparing presets.yaml files failed. Error: ${error}`); - throw error; - } -} - -function saveFilteredPresets(path: string, parsedPreset: DocPreset): void { - const {output: outputFolderPath, varsPreset = ''} = ArgvService.getConfig(); - - const outputPath = resolve(outputFolderPath, path); - const filteredPreset: Record = { - default: parsedPreset.default, - }; - - if (parsedPreset[varsPreset]) { - filteredPreset[varsPreset] = parsedPreset[varsPreset]; - } - - const outputPreset = dump(filteredPreset, { - lineWidth: 120, - }); - - shell.mkdir('-p', dirname(outputPath)); - writeFileSync(outputPath, outputPreset); -} - -async function preparingTocFiles(): Promise { +export async function preparingTocFiles(): Promise { try { const tocFilePaths = getFilePathsByGlobals(['**/toc.yaml']); await TocService.init(tocFilePaths); diff --git a/src/utils/common.ts b/src/utils/common.ts index 569226ce..bae1fff7 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -50,3 +50,28 @@ export function checkPathExists(path: string, parentFilePath: string) { return isFileExists(includePath); } + +export function own(box: unknown, field: T): box is {[p in T]: unknown} { + return ( + Boolean(box && typeof box === 'object') && Object.prototype.hasOwnProperty.call(box, field) + ); +} + +export function freeze(target: T, visited = new Set()): T { + if (!visited.has(target)) { + visited.add(target); + + if (Array.isArray(target)) { + target.forEach((item) => freeze(item, visited)); + } + + if (isObject(target) && !Object.isSealed(target)) { + Object.freeze(target); + Object.keys(target).forEach((key) => + freeze(target[key as keyof typeof target], visited), + ); + } + } + + return target; +} diff --git a/tests/mocks/metadata/md2html-with-metadata/input/presets.yaml b/tests/mocks/metadata/md2html-with-metadata/input/presets.yaml index a5dec198..d02a09a9 100644 --- a/tests/mocks/metadata/md2html-with-metadata/input/presets.yaml +++ b/tests/mocks/metadata/md2html-with-metadata/input/presets.yaml @@ -1,5 +1,6 @@ -__metadata: - - name: test-yfm - content: inline test - - name: yfm-config - content: config test +default: + __metadata: + - name: test-yfm + content: inline test + - name: yfm-config + content: config test diff --git a/tests/mocks/metadata/md2md-with-metadata/input/presets.yaml b/tests/mocks/metadata/md2md-with-metadata/input/presets.yaml index a5dec198..d02a09a9 100644 --- a/tests/mocks/metadata/md2md-with-metadata/input/presets.yaml +++ b/tests/mocks/metadata/md2md-with-metadata/input/presets.yaml @@ -1,5 +1,6 @@ -__metadata: - - name: test-yfm - content: inline test - - name: yfm-config - content: config test +default: + __metadata: + - name: test-yfm + content: inline test + - name: yfm-config + content: config test