Skip to content

Commit

Permalink
feat(core): add esm plugin logic (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton authored Nov 15, 2023
1 parent 58b0aec commit 18d4e3a
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 96 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

.env

.nx

# compiled output
dist
tmp
Expand Down Expand Up @@ -42,4 +44,4 @@ testem.log
Thumbs.db

# generated Code PushUp reports
/.code-pushup
/.code-pushup
Empty file removed packages/cli/src/lib/model.ts
Empty file.
67 changes: 55 additions & 12 deletions packages/core/src/lib/implementation/execute-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest';
import {
AuditOutput,
AuditOutputs,
OnProgress,
PluginConfig,
RunnerConfig,
RunnerFunction,
auditOutputsSchema,
} from '@code-pushup/models';
import { auditReport, pluginConfig } from '@code-pushup/models/testing';
Expand Down Expand Up @@ -42,16 +44,57 @@ describe('executePlugin', () => {
);
});

it('should throw if invalid runnerOutput is produced with transform', async () => {
it('should work with valid runner config', async () => {
const runnerConfig = validPluginCfg.runner as RunnerConfig;
const pluginCfg: PluginConfig = {
...validPluginCfg,
runner: {
...validPluginCfg.runner,
outputTransform: (d: unknown) =>
Array.from(d as Record<string, unknown>[]).map((d, idx) => ({
...d,
slug: '-invalid-slug-' + idx,
})) as unknown as AuditOutputs,
...runnerConfig,
outputTransform: (audits: unknown) =>
Promise.resolve(audits as AuditOutputs),
},
};
const pluginResult = await executePlugin(pluginCfg);
expect(pluginResult.audits[0]?.slug).toBe('mock-audit-slug');
expect(() => auditOutputsSchema.parse(pluginResult.audits)).not.toThrow();
});

it('should work with valid runner function', async () => {
const runnerFunction = (onProgress?: OnProgress) => {
onProgress?.('update');
return Promise.resolve([
{ slug: 'mock-audit-slug', score: 0, value: 0 },
] satisfies AuditOutputs);
};

const pluginCfg: PluginConfig = {
...validPluginCfg,
runner: runnerFunction,
};
const pluginResult = await executePlugin(pluginCfg);
expect(pluginResult.audits[0]?.slug).toBe('mock-audit-slug');
expect(() => auditOutputsSchema.parse(pluginResult.audits)).not.toThrow();
});

it('should throw with invalid runner config', async () => {
const pluginCfg: PluginConfig = {
...validPluginCfg,
runner: '' as unknown as RunnerFunction,
};
await expect(executePlugin(pluginCfg)).rejects.toThrow(
'runner is not a function',
);
});

it('should throw if invalid runnerOutput', async () => {
const pluginCfg: PluginConfig = {
...validPluginCfg,
runner: (onProgress?: OnProgress) => {
onProgress?.('update');

return Promise.resolve([
{ slug: '-mock-audit-slug', score: 0, value: 0 },
] satisfies AuditOutputs);
},
};

Expand Down Expand Up @@ -87,21 +130,21 @@ describe('executePlugins', () => {
});

it('should use outputTransform if provided', async () => {
const processRunner = validPluginCfg.runner as RunnerConfig;
const plugins: PluginConfig[] = [
{
...validPluginCfg,
runner: {
...validPluginCfg.runner,
...processRunner,
outputTransform: (outputs: unknown): Promise<AuditOutputs> => {
const arr = Array.from(outputs as Record<string, unknown>[]);
return Promise.resolve(
arr.map(output => {
(outputs as AuditOutputs).map(output => {
return {
...output,
displayValue:
'transformed slug description - ' +
(output as { slug: string }).slug,
} as unknown as AuditOutput;
};
}),
);
},
Expand Down
53 changes: 16 additions & 37 deletions packages/core/src/lib/implementation/execute-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import chalk from 'chalk';
import { join } from 'path';
import {
Audit,
AuditOutput,
AuditOutputs,
AuditReport,
OnProgress,
PluginConfig,
PluginReport,
auditOutputsSchema,
} from '@code-pushup/models';
import {
ProcessObserver,
executeProcess,
getProgressBar,
readJsonFile,
} from '@code-pushup/utils';
import { getProgressBar } from '@code-pushup/utils';
import { executeRunnerConfig, executeRunnerFunction } from './runner';

/**
* Error thrown when plugin output is invalid.
Expand All @@ -30,7 +26,7 @@ export class PluginOutputMissingAuditError extends Error {
*
* @public
* @param pluginConfig - {@link ProcessConfig} object with runner and meta
* @param observer - process {@link ProcessObserver}
* @param onProgress - progress handler {@link OnProgress}
* @returns {Promise<AuditOutput[]>} - audit outputs from plugin runner
* @throws {PluginOutputMissingAuditError} - if plugin runner output is invalid
*
Expand All @@ -49,48 +45,30 @@ export class PluginOutputMissingAuditError extends Error {
*/
export async function executePlugin(
pluginConfig: PluginConfig,
observer?: ProcessObserver,
onProgress?: OnProgress,
): Promise<PluginReport> {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
runner: onlyUsedForRestingPluginMeta,
runner,
audits: pluginConfigAudits,
description,
docsUrl,
groups,
...pluginMeta
} = pluginConfig;
const { args, command } = pluginConfig.runner;

const { date, duration } = await executeProcess({
command,
args,
observer,
});
const executionMeta = { date, duration };

const processOutputPath = join(process.cwd(), pluginConfig.runner.outputFile);

// read process output from file system and parse it
let unknownAuditOutputs = await readJsonFile<Record<string, unknown>[]>(
processOutputPath,
);

// parse transform unknownAuditOutputs to auditOutputs
if (pluginConfig.runner?.outputTransform) {
unknownAuditOutputs = await pluginConfig.runner.outputTransform(
unknownAuditOutputs,
);
}

// validate audit outputs
const auditOutputs = auditOutputsSchema.parse(unknownAuditOutputs);
// execute plugin runner
const runnerResult =
typeof runner === 'object'
? await executeRunnerConfig(runner, onProgress)
: await executeRunnerFunction(runner, onProgress);
const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult;

// validate auditOutputs
const auditOutputs = auditOutputsSchema.parse(unvalidatedAuditOutputs);
auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits);

// enrich `AuditOutputs` to `AuditReport`
const audits: AuditReport[] = auditOutputs.map(
const auditReports: AuditReport[] = auditOutputs.map(
(auditOutput: AuditOutput) => ({
...auditOutput,
...(pluginConfigAudits.find(
Expand All @@ -99,10 +77,11 @@ export async function executePlugin(
}),
);

// create plugin report
return {
...pluginMeta,
...executionMeta,
audits,
audits: auditReports,
...(description && { description }),
...(docsUrl && { docsUrl }),
...(groups && { groups }),
Expand Down
87 changes: 87 additions & 0 deletions packages/core/src/lib/implementation/runner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import {
AuditOutputs,
OnProgress,
auditOutputsSchema,
} from '@code-pushup/models';
import { auditReport, echoRunnerConfig } from '@code-pushup/models/testing';
import {
RunnerResult,
executeRunnerConfig,
executeRunnerFunction,
} from './runner';

const validRunnerCfg = echoRunnerConfig([auditReport()], 'output.json');

describe('executeRunnerConfig', () => {
it('should work with valid plugins', async () => {
const runnerResult = await executeRunnerConfig(validRunnerCfg);

// data sanity
expect(runnerResult.date.endsWith('Z')).toBeTruthy();
expect(runnerResult.duration).toBeTruthy();
expect(runnerResult.audits[0]?.slug).toBe('mock-audit-slug');

// schema validation
expect(() => auditOutputsSchema.parse(runnerResult.audits)).not.toThrow();
});

it('should use transform if provided', async () => {
const runnerCfgWithTransform = {
...validRunnerCfg,
outputTransform: (audits: unknown) =>
(audits as AuditOutputs).map(a => ({
...a,
displayValue: `transformed - ${a.slug}`,
})),
};

const runnerResult = await executeRunnerConfig(runnerCfgWithTransform);

expect(runnerResult.audits[0]?.displayValue).toBe(
'transformed - mock-audit-slug',
);
});

it('should throw if transform throws', async () => {
const runnerCfgWithErrorTransform = {
...validRunnerCfg,
outputTransform: () => {
return Promise.reject(new Error('transform mock error'));
},
};

await expect(
executeRunnerConfig(runnerCfgWithErrorTransform),
).rejects.toThrow('transform mock error');
});
});

describe('executeRunnerFunction', () => {
it('should execute valid plugin config', async () => {
const nextSpy = vi.fn();
const runnerResult: RunnerResult = await executeRunnerFunction(
(observer?: OnProgress) => {
observer?.('update');

return Promise.resolve([
{ slug: 'mock-audit-slug', score: 0, value: 0 },
] satisfies AuditOutputs);
},
nextSpy,
);
expect(nextSpy).toHaveBeenCalledWith('update');
expect(runnerResult.audits[0]?.slug).toBe('mock-audit-slug');
});

it('should throw if plugin throws', async () => {
const nextSpy = vi.fn();
await expect(
executeRunnerFunction(
() => Promise.reject(new Error('plugin exec mock error')),
nextSpy,
),
).rejects.toThrow('plugin exec mock error');
expect(nextSpy).not.toHaveBeenCalled();
});
});
63 changes: 63 additions & 0 deletions packages/core/src/lib/implementation/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { join } from 'path';
import {
AuditOutputs,
OnProgress,
RunnerConfig,
RunnerFunction,
} from '@code-pushup/models';
import { calcDuration, executeProcess, readJsonFile } from '@code-pushup/utils';

export type RunnerResult = {
date: string;
duration: number;
audits: AuditOutputs;
};

export async function executeRunnerConfig(
cfg: RunnerConfig,
onProgress?: OnProgress,
): Promise<RunnerResult> {
const { args, command, outputFile, outputTransform } = cfg;

// execute process
const { duration, date } = await executeProcess({
command,
args,
observer: { onStdout: onProgress },
});

// read process output from file system and parse it
let audits = await readJsonFile<AuditOutputs>(
join(process.cwd(), outputFile),
);

// transform unknownAuditOutputs to auditOutputs
if (outputTransform) {
audits = await outputTransform(audits);
}

// create runner result
return {
duration,
date,
audits,
};
}

export async function executeRunnerFunction(
runner: RunnerFunction,
onProgress?: OnProgress,
): Promise<RunnerResult> {
const date = new Date().toISOString();
const start = performance.now();

// execute plugin runner
const audits = await runner(onProgress);

// create runner result
return {
date,
duration: calcDuration(start),
audits,
};
}
8 changes: 7 additions & 1 deletion packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export {
persistConfigSchema,
} from './lib/persist-config';
export { PluginConfig, pluginConfigSchema } from './lib/plugin-config';
export { RunnerConfig } from './lib/plugin-config-runner';
export {
auditSchema,
Audit,
Expand All @@ -45,3 +44,10 @@ export {
} from './lib/report';
export { UploadConfig, uploadConfigSchema } from './lib/upload-config';
export { materialIconSchema } from './lib/implementation/schemas';
export {
onProgressSchema,
OnProgress,
RunnerFunction,
runnerConfigSchema,
RunnerConfig,
} from './lib/plugin-config-runner';
Loading

0 comments on commit 18d4e3a

Please sign in to comment.