Skip to content

Commit

Permalink
feat: Extend Build.Run fs API
Browse files Browse the repository at this point in the history
  • Loading branch information
3y3 committed Dec 6, 2024
1 parent c239dd0 commit bcdb96b
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 133 deletions.
66 changes: 47 additions & 19 deletions src/commands/build/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {Run} from '../run';
import type {BuildConfig, BuildRawConfig} from '..';
import type {Mock, MockInstance} from 'vitest';

import {join} from 'node:path';
import {Mock, describe, expect, it, vi} from 'vitest';
import {describe, expect, it, vi} from 'vitest';
import {when} from 'vitest-when';
import {Build} from '..';
import {Run} from '../run';
import {handler as originalHandler} from '../handler';
import {withConfigUtils} from '~/config';

Expand All @@ -14,6 +15,7 @@ export const handler = originalHandler as Mock;
var resolveConfig: Mock;

vi.mock('shelljs');
vi.mock('../legacy-config');
vi.mock('../handler');
vi.mock('../run', async (importOriginal) => {
return {
Expand All @@ -32,6 +34,38 @@ vi.mock('~/config', async (importOriginal) => {
};
});

const Mocked = Symbol('Mocked');

type RunSpy = Run & {
glob: MockInstance<Parameters<Run['glob']>, ReturnType<Run['glob']>>;
copy: MockInstance<Parameters<Run['copy']>, ReturnType<Run['copy']>>;
read: MockInstance<Parameters<Run['read']>, ReturnType<Run['read']>>;
write: MockInstance<Parameters<Run['write']>, ReturnType<Run['write']>>;
[Mocked]: boolean;
};

export function setupRun(config: DeepPartial<BuildConfig>, run?: Run): RunSpy {
run = run || new Run(config);

const impl = (method: string) => (...args: any[]) => {
throw new Error(`Method ${method} with args\n${args.join('\n')} not implemented.`);
};

for (const method of ['glob', 'copy', 'read', 'write'] as string[]) {
// @ts-ignore
vi.spyOn(run, method).mockImplementation(impl(method));
}

for (const method of ['proc', 'info', 'warn', 'error'] as string[]) {
// @ts-ignore
vi.spyOn(run.logger, method).mockImplementation(() => {});
}

(run as RunSpy)[Mocked] = true;

return run as RunSpy;
}

type BuildState = {
globs?: Hash<string[]>;
files?: Hash<string>;
Expand All @@ -43,21 +77,15 @@ export function setupBuild(state: BuildState = {}): Build & {run: Run} {
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 (!(run as RunSpy)[Mocked]) {
setupRun({}, run);
}

when(run.copy).calledWith(expect.anything(), expect.anything()).thenResolve();
when(run.copy).calledWith(expect.anything(), expect.anything(), expect.anything()).thenResolve();
when(run.write).calledWith(expect.anything(), expect.anything()).thenResolve();
when(run.glob).calledWith('**/toc.yaml', expect.anything()).thenResolve([]);
when(run.glob).calledWith('**/presets.yaml', expect.anything()).thenResolve([]);

if (state.globs) {
for (const [pattern, files] of Object.entries(state.globs)) {
Expand All @@ -67,8 +95,8 @@ export function setupBuild(state: BuildState = {}): Build & {run: Run} {

if (state.files) {
for (const [file, content] of Object.entries(state.files)) {
when(run.fs.readFile)
.calledWith(join(run.input, file), expect.anything())
when(run.read)
.calledWith(join(run.input, file))
.thenResolve(content);
}
}
Expand Down
5 changes: 1 addition & 4 deletions src/commands/build/core/vars/VarsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ export class VarsService {

private run: Run;

private fs: Run['fs'];

private logger: Run['logger'];

private config: VarsServiceConfig;
Expand All @@ -42,7 +40,6 @@ export class VarsService {

constructor(run: Run) {
this.run = run;
this.fs = run.fs;
this.logger = run.logger;
this.config = run.config;
this.hooks = {
Expand All @@ -69,7 +66,7 @@ export class VarsService {

try {
const presets = await this.hooks.PresetsLoaded.promise(
load(await this.fs.readFile(join(this.run.input, file), 'utf8')) as Presets,
load(await this.run.read(join(this.run.input, file))) as Presets,
file,
);

Expand Down
105 changes: 50 additions & 55 deletions src/commands/build/core/vars/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type {Run} from '~/commands/build';
import type {VarsServiceConfig} from './VarsService';

import {join} from 'node:path';
Expand All @@ -7,6 +6,8 @@ import {when} from 'vitest-when';
import {dedent} from 'ts-dedent';
import {YAMLException} from 'js-yaml';

import {setupRun} from '~/commands/build/__tests__';

import {VarsService} from './VarsService';

const ENOENT = Object.assign(new Error('ENOENT: no such file or directory'), {
Expand All @@ -15,37 +16,30 @@ const ENOENT = Object.assign(new Error('ENOENT: no such file or directory'), {

type Options = Partial<VarsServiceConfig>;

function prepare(content: string | Hash<string> | Error, options: Options = {}) {
function prepare(content: string | Error | Hash<string | Error>, options: Options = {}) {
const input = '/dev/null/input' as AbsolutePath;
const output = '/dev/null/output' as AbsolutePath;
const run = {
const run = setupRun({
input,
output,
config: {
varsPreset: options.varsPreset,
vars: options.vars || {},
},
logger: {
proc: vi.fn(),
},
fs: {
readFile: vi.fn(),
},
} as unknown as Run;
varsPreset: options.varsPreset,
vars: options.vars || {},
});

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};
}
if (typeof content === 'string' || content instanceof Error) {
content = {'./presets.yaml': content};
}

for (const [file, data] of Object.entries(content)) {
when(run.fs.readFile)
.calledWith(join(input, file), expect.anything())
for (const [file, data] of Object.entries(content)) {
if (data instanceof Error) {
when(run.read)
.calledWith(join(run.input, file))
.thenReject(data);
} else {
when(run.read)
.calledWith(join(run.input, file))
.thenResolve(data);
}
}
Expand Down Expand Up @@ -134,38 +128,39 @@ describe('vars', () => {
const service = prepare(
{
'./presets.yaml': dedent`
default:
field1: value1
override1: value2
override2: value2
override3: value2
override4: value2
internal:
field2: value1
override1: value1
`,
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
`,
default:
sub1: value1
sub2: value2
override2: value1
override5: value2
internal:
sub2: value1
override3: value1
override6: value2
`,
'./subfolder/subfolder/presets.yaml': ENOENT,
'./subfolder/subfolder/subfolder/presets.yaml': dedent`
default:
subsub1: value2
override4: value2
override5: value1
internal:
subsub1: value1
subsub2: value1
override4: value1
override6: value1
`,
default:
subsub1: value2
override4: value2
override5: value1
internal:
subsub1: value1
subsub2: value1
override4: value1
override6: value1
`,
},
{varsPreset: 'internal'},
);
Expand Down
19 changes: 19 additions & 0 deletions src/commands/build/errors/InsecureAccessError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export class InsecureAccessError extends Error {
readonly realpath: AbsolutePath;

readonly realstack: AbsolutePath[];

constructor(file: AbsolutePath, stack?: AbsolutePath[]) {
const message = [
`Requested file '${file}' is out of project scope.`,
stack && 'File resolution stack:\n\t' + stack.join('\n\t'),
]
.filter(Boolean)
.join('\n');

super(message);

this.realpath = file;
this.realstack = stack || [];
}
}
1 change: 1 addition & 0 deletions src/commands/build/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {InsecureAccessError} from './InsecureAccessError';
54 changes: 54 additions & 0 deletions src/commands/build/legacy-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {Run} from '.';
import type {YfmArgv} from '~/models';

export function legacyConfig(run: Run): YfmArgv {
const {config} = run;

return {
rootInput: run.originalInput,
input: run.input,
output: run.output,
quiet: config.quiet,
addSystemMeta: config.addSystemMeta,
addMapFile: config.addMapFile,
staticContent: config.staticContent,
strict: config.strict,
langs: config.langs,
lang: config.lang,
ignoreStage: config.ignoreStage,
singlePage: config.singlePage,
removeHiddenTocItems: config.removeHiddenTocItems,
allowCustomResources: config.allowCustomResources,
resources: config.resources,
analytics: config.analytics,
varsPreset: config.varsPreset,
vars: config.vars,
outputFormat: config.outputFormat,
allowHTML: config.allowHtml,
needToSanitizeHtml: config.sanitizeHtml,
useLegacyConditions: config.useLegacyConditions,

ignore: config.ignore,

applyPresets: config.template.features.substitutions,
resolveConditions: config.template.features.conditions,
conditionsInCode: config.template.scopes.code,
disableLiquid: !config.template.enabled,

buildDisabled: config.buildDisabled,

lintDisabled: !config.lint.enabled,
// @ts-ignore
lintConfig: config.lint.config,

vcs: config.vcs,
connector: config.vcs.connector,
contributors: config.contributors,
ignoreAuthorPatterns: config.ignoreAuthorPatterns,

changelogs: config.changelogs,
search: config.search,

included: config.mergeIncludes,
};
}
Loading

0 comments on commit bcdb96b

Please sign in to comment.