Skip to content

Commit

Permalink
Update logging and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Feb 7, 2025
1 parent 59f78de commit 11ca695
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 430 deletions.
1 change: 0 additions & 1 deletion packages/snaps-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@
"@types/jest": "^27.5.1",
"@types/node": "18.14.2",
"@types/serve-handler": "^6.1.0",
"@types/webpack-bundle-analyzer": "^4.7.0",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^6.21.0",
Expand Down
55 changes: 54 additions & 1 deletion packages/snaps-cli/src/commands/build/build.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DEFAULT_SNAP_BUNDLE } from '@metamask/snaps-utils/test-utils';
import fs from 'fs';
import type { Compiler } from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

import { getMockConfig } from '../../test-utils';
import { evaluate } from '../eval';
Expand All @@ -10,6 +12,10 @@ jest.mock('fs');
jest.mock('../eval');
jest.mock('./implementation');

jest.mock('webpack-bundle-analyzer', () => ({
BundleAnalyzerPlugin: jest.fn(),
}));

describe('buildHandler', () => {
it('builds a snap', async () => {
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);
Expand Down Expand Up @@ -37,7 +43,54 @@ describe('buildHandler', () => {
);
});

it('does note evaluate if the evaluate option is set to false', async () => {
it('analyzes a snap bundle', async () => {
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);

jest.spyOn(console, 'log').mockImplementation();
const config = getMockConfig('webpack', {
input: '/input.js',
output: {
path: '/foo',
filename: 'output.js',
},
});

const compiler: Compiler = {
// @ts-expect-error: Mock `Compiler` object.
options: {
plugins: [new BundleAnalyzerPlugin()],
},
};

const plugin = jest.mocked(BundleAnalyzerPlugin);
const instance = plugin.mock.instances[0];

// @ts-expect-error: Partial `server` mock.
instance.server = Promise.resolve({
http: {
address: () => 'http://localhost:8888',
},
});

jest.mocked(build).mockResolvedValueOnce(compiler);

await buildHandler(config, true);

expect(process.exitCode).not.toBe(1);
expect(build).toHaveBeenCalledWith(config, {
analyze: true,
evaluate: false,
spinner: expect.any(Object),
});

expect(console.log).toHaveBeenCalledWith(
expect.stringContaining(
'Bundle analyzer running at http://localhost:8888.',
),
);
});

it('does not evaluate if the evaluate option is set to false', async () => {
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);

jest.spyOn(console, 'log').mockImplementation();
Expand Down
32 changes: 30 additions & 2 deletions packages/snaps-cli/src/commands/build/build.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { isFile } from '@metamask/snaps-utils/node';
import { assert } from '@metamask/utils';
import { resolve as pathResolve } from 'path';

import type { ProcessedConfig, ProcessedWebpackConfig } from '../../config';
import { CommandError } from '../../errors';
import type { Steps } from '../../utils';
import { executeSteps, info } from '../../utils';
import { success, executeSteps, info } from '../../utils';
import { evaluate } from '../eval';
import { build } from './implementation';
import { getBundleAnalyzerPort } from './utils';

type BuildContext = {
analyze: boolean;
config: ProcessedWebpackConfig;
port?: number;
};

const steps: Steps<BuildContext> = [
Expand All @@ -31,7 +34,22 @@ const steps: Steps<BuildContext> = [
task: async ({ analyze, config, spinner }) => {
// We don't evaluate the bundle here, because it's done in a separate
// step.
return await build(config, { analyze, evaluate: false, spinner });
const compiler = await build(config, {
analyze,
evaluate: false,
spinner,
});

if (analyze) {
return {
analyze,
config,
spinner,
port: await getBundleAnalyzerPort(compiler),
};
}

return undefined;
},
},
{
Expand All @@ -49,6 +67,16 @@ const steps: Steps<BuildContext> = [
info(`Snap bundle evaluated successfully.`, spinner);
},
},
{
name: 'Running analyser.',
condition: ({ analyze }) => analyze,
task: async ({ spinner, port }) => {
assert(port, 'Port is not defined.');
success(`Bundle analyzer running at http://localhost:${port}.`, spinner);

spinner.stop();
},
},
] as const;

/**
Expand Down
6 changes: 4 additions & 2 deletions packages/snaps-cli/src/commands/build/implementation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Compiler } from 'webpack';

import type { ProcessedWebpackConfig } from '../../config';
import type { WebpackOptions } from '../../webpack';
import { getCompiler } from '../../webpack';
Expand All @@ -14,7 +16,7 @@ export async function build(
options?: WebpackOptions,
) {
const compiler = await getCompiler(config, options);
return await new Promise<void>((resolve, reject) => {
return await new Promise<Compiler>((resolve, reject) => {
compiler.run((runError) => {
if (runError) {
reject(runError);
Expand All @@ -27,7 +29,7 @@ export async function build(
return;
}

resolve();
resolve(compiler);
});
});
});
Expand Down
70 changes: 70 additions & 0 deletions packages/snaps-cli/src/commands/build/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Compiler } from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

import { getBundleAnalyzerPort } from './utils';

jest.mock('webpack-bundle-analyzer', () => ({
BundleAnalyzerPlugin: jest.fn(),
}));

describe('getBundleAnalyzerPort', () => {
it('returns the port of the bundle analyzer server', async () => {
const compiler: Compiler = {
// @ts-expect-error: Mock `Compiler` object.
options: {
plugins: [new BundleAnalyzerPlugin()],
},
};

const plugin = jest.mocked(BundleAnalyzerPlugin);
const instance = plugin.mock.instances[0];

// @ts-expect-error: Partial `server` mock.
instance.server = Promise.resolve({
http: {
address: () => 'http://localhost:8888',
},
});

const port = await getBundleAnalyzerPort(compiler);
expect(port).toBe(8888);
});

it('returns the port of the bundle analyzer server that returns an object', async () => {
const compiler: Compiler = {
// @ts-expect-error: Mock `Compiler` object.
options: {
plugins: [new BundleAnalyzerPlugin()],
},
};

const plugin = jest.mocked(BundleAnalyzerPlugin);
const instance = plugin.mock.instances[0];

// @ts-expect-error: Partial `server` mock.
instance.server = Promise.resolve({
http: {
address: () => {
return {
port: 8888,
};
},
},
});

const port = await getBundleAnalyzerPort(compiler);
expect(port).toBe(8888);
});

it('returns undefined if the bundle analyzer server is not available', async () => {
const compiler: Compiler = {
// @ts-expect-error: Mock `Compiler` object.
options: {
plugins: [new BundleAnalyzerPlugin()],
},
};

const port = await getBundleAnalyzerPort(compiler);
expect(port).toBeUndefined();
});
});
29 changes: 29 additions & 0 deletions packages/snaps-cli/src/commands/build/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Compiler } from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

/**
* Get the port of the bundle analyzer server.
*
* @param compiler - The Webpack compiler.
* @returns The port of the bundle analyzer server.
*/
export async function getBundleAnalyzerPort(compiler: Compiler) {
const analyzerPlugin = compiler.options.plugins.find(
(plugin): plugin is BundleAnalyzerPlugin =>
plugin instanceof BundleAnalyzerPlugin,
);

if (analyzerPlugin?.server) {
const { http } = await analyzerPlugin.server;

const address = http.address();
if (typeof address === 'string') {
const { port } = new URL(address);
return parseInt(port, 10);
}

return address?.port;
}

return undefined;
}
22 changes: 22 additions & 0 deletions packages/snaps-cli/src/types/webpack-bundle-analyzer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
declare module 'webpack-bundle-analyzer' {
import type { Server } from 'http';
import type { Compiler, WebpackPluginInstance } from 'webpack';

export type BundleAnalyzerPluginOptions = {
analyzerPort?: number | undefined;
logLevel?: 'info' | 'warn' | 'error' | 'silent' | undefined;
openAnalyzer?: boolean | undefined;
};

export class BundleAnalyzerPlugin implements WebpackPluginInstance {
readonly opts: BundleAnalyzerPluginOptions;

server?: Promise<{
http: Server;
}>;

constructor(options?: BundleAnalyzerPluginOptions);

apply(compiler: Compiler): void;
}
}
23 changes: 21 additions & 2 deletions packages/snaps-cli/src/utils/logging.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import { blue, dim, red, yellow } from 'chalk';
import { blue, dim, green, red, yellow } from 'chalk';
import type { Ora } from 'ora';

import { error, info, warn } from './logging';
import { error, info, success, warn } from './logging';

describe('success', () => {
it('logs a success message', () => {
const log = jest.spyOn(console, 'log').mockImplementation();

success('foo');
expect(log).toHaveBeenCalledWith(`${green('✔')} foo`);
});

it('clears a spinner if provided', () => {
jest.spyOn(console, 'warn').mockImplementation();

const spinner = { clear: jest.fn(), frame: jest.fn() } as unknown as Ora;
success('foo', spinner);

expect(spinner.clear).toHaveBeenCalled();
expect(spinner.frame).toHaveBeenCalled();
});
});

describe('warn', () => {
it('logs a warning message', () => {
Expand Down
19 changes: 17 additions & 2 deletions packages/snaps-cli/src/utils/logging.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { logError, logInfo, logWarning } from '@metamask/snaps-utils';
import { blue, dim, red, yellow } from 'chalk';
import { blue, dim, green, red, yellow } from 'chalk';
import type { Ora } from 'ora';

/**
* Log a warning message. The message is prefixed with "Warning:".
* Log a success message. The message is prefixed with a green checkmark.
*
* @param message - The message to log.
* @param spinner - The spinner to clear.
*/
export function success(message: string, spinner?: Ora) {
if (spinner) {
spinner.clear();
spinner.frame();
}

logInfo(`${green('✔')} ${message}`);
}

/**
* Log a warning message. The message is prefixed with a yellow warning sign.
*
* @param message - The message to log.
* @param spinner - The spinner to clear.
Expand Down
38 changes: 38 additions & 0 deletions packages/snaps-cli/src/utils/steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,44 @@ describe('executeSteps', () => {
});
});

it('updates the context if a step returns an object', async () => {
const steps = [
{
name: 'Step 1',
task: jest.fn(),
},
{
name: 'Step 2',
task: jest.fn().mockResolvedValue({ foo: 'baz' }),
},
{
name: 'Step 3',
task: jest.fn(),
},
];

const context = {
foo: 'bar',
};

await executeSteps(steps, context);

expect(steps[0].task).toHaveBeenCalledWith({
...context,
spinner: expect.any(Object),
});

expect(steps[0].task).toHaveBeenCalledWith({
...context,
spinner: expect.any(Object),
});

expect(steps[2].task).toHaveBeenCalledWith({
foo: 'baz',
spinner: expect.any(Object),
});
});

it('sets the exit code to 1 if a step throws an error', async () => {
const log = jest.spyOn(console, 'error').mockImplementation();

Expand Down
Loading

0 comments on commit 11ca695

Please sign in to comment.