Skip to content

Commit 11ca695

Browse files
committed
Update logging and add tests
1 parent 59f78de commit 11ca695

File tree

14 files changed

+334
-430
lines changed

14 files changed

+334
-430
lines changed

packages/snaps-cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@
129129
"@types/jest": "^27.5.1",
130130
"@types/node": "18.14.2",
131131
"@types/serve-handler": "^6.1.0",
132-
"@types/webpack-bundle-analyzer": "^4.7.0",
133132
"@types/yargs": "^17.0.24",
134133
"@typescript-eslint/eslint-plugin": "^5.42.1",
135134
"@typescript-eslint/parser": "^6.21.0",

packages/snaps-cli/src/commands/build/build.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DEFAULT_SNAP_BUNDLE } from '@metamask/snaps-utils/test-utils';
22
import fs from 'fs';
3+
import type { Compiler } from 'webpack';
4+
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
35

46
import { getMockConfig } from '../../test-utils';
57
import { evaluate } from '../eval';
@@ -10,6 +12,10 @@ jest.mock('fs');
1012
jest.mock('../eval');
1113
jest.mock('./implementation');
1214

15+
jest.mock('webpack-bundle-analyzer', () => ({
16+
BundleAnalyzerPlugin: jest.fn(),
17+
}));
18+
1319
describe('buildHandler', () => {
1420
it('builds a snap', async () => {
1521
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);
@@ -37,7 +43,54 @@ describe('buildHandler', () => {
3743
);
3844
});
3945

40-
it('does note evaluate if the evaluate option is set to false', async () => {
46+
it('analyzes a snap bundle', async () => {
47+
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);
48+
49+
jest.spyOn(console, 'log').mockImplementation();
50+
const config = getMockConfig('webpack', {
51+
input: '/input.js',
52+
output: {
53+
path: '/foo',
54+
filename: 'output.js',
55+
},
56+
});
57+
58+
const compiler: Compiler = {
59+
// @ts-expect-error: Mock `Compiler` object.
60+
options: {
61+
plugins: [new BundleAnalyzerPlugin()],
62+
},
63+
};
64+
65+
const plugin = jest.mocked(BundleAnalyzerPlugin);
66+
const instance = plugin.mock.instances[0];
67+
68+
// @ts-expect-error: Partial `server` mock.
69+
instance.server = Promise.resolve({
70+
http: {
71+
address: () => 'http://localhost:8888',
72+
},
73+
});
74+
75+
jest.mocked(build).mockResolvedValueOnce(compiler);
76+
77+
await buildHandler(config, true);
78+
79+
expect(process.exitCode).not.toBe(1);
80+
expect(build).toHaveBeenCalledWith(config, {
81+
analyze: true,
82+
evaluate: false,
83+
spinner: expect.any(Object),
84+
});
85+
86+
expect(console.log).toHaveBeenCalledWith(
87+
expect.stringContaining(
88+
'Bundle analyzer running at http://localhost:8888.',
89+
),
90+
);
91+
});
92+
93+
it('does not evaluate if the evaluate option is set to false', async () => {
4194
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);
4295

4396
jest.spyOn(console, 'log').mockImplementation();

packages/snaps-cli/src/commands/build/build.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { isFile } from '@metamask/snaps-utils/node';
2+
import { assert } from '@metamask/utils';
23
import { resolve as pathResolve } from 'path';
34

45
import type { ProcessedConfig, ProcessedWebpackConfig } from '../../config';
56
import { CommandError } from '../../errors';
67
import type { Steps } from '../../utils';
7-
import { executeSteps, info } from '../../utils';
8+
import { success, executeSteps, info } from '../../utils';
89
import { evaluate } from '../eval';
910
import { build } from './implementation';
11+
import { getBundleAnalyzerPort } from './utils';
1012

1113
type BuildContext = {
1214
analyze: boolean;
1315
config: ProcessedWebpackConfig;
16+
port?: number;
1417
};
1518

1619
const steps: Steps<BuildContext> = [
@@ -31,7 +34,22 @@ const steps: Steps<BuildContext> = [
3134
task: async ({ analyze, config, spinner }) => {
3235
// We don't evaluate the bundle here, because it's done in a separate
3336
// step.
34-
return await build(config, { analyze, evaluate: false, spinner });
37+
const compiler = await build(config, {
38+
analyze,
39+
evaluate: false,
40+
spinner,
41+
});
42+
43+
if (analyze) {
44+
return {
45+
analyze,
46+
config,
47+
spinner,
48+
port: await getBundleAnalyzerPort(compiler),
49+
};
50+
}
51+
52+
return undefined;
3553
},
3654
},
3755
{
@@ -49,6 +67,16 @@ const steps: Steps<BuildContext> = [
4967
info(`Snap bundle evaluated successfully.`, spinner);
5068
},
5169
},
70+
{
71+
name: 'Running analyser.',
72+
condition: ({ analyze }) => analyze,
73+
task: async ({ spinner, port }) => {
74+
assert(port, 'Port is not defined.');
75+
success(`Bundle analyzer running at http://localhost:${port}.`, spinner);
76+
77+
spinner.stop();
78+
},
79+
},
5280
] as const;
5381

5482
/**

packages/snaps-cli/src/commands/build/implementation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Compiler } from 'webpack';
2+
13
import type { ProcessedWebpackConfig } from '../../config';
24
import type { WebpackOptions } from '../../webpack';
35
import { getCompiler } from '../../webpack';
@@ -14,7 +16,7 @@ export async function build(
1416
options?: WebpackOptions,
1517
) {
1618
const compiler = await getCompiler(config, options);
17-
return await new Promise<void>((resolve, reject) => {
19+
return await new Promise<Compiler>((resolve, reject) => {
1820
compiler.run((runError) => {
1921
if (runError) {
2022
reject(runError);
@@ -27,7 +29,7 @@ export async function build(
2729
return;
2830
}
2931

30-
resolve();
32+
resolve(compiler);
3133
});
3234
});
3335
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { Compiler } from 'webpack';
2+
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
3+
4+
import { getBundleAnalyzerPort } from './utils';
5+
6+
jest.mock('webpack-bundle-analyzer', () => ({
7+
BundleAnalyzerPlugin: jest.fn(),
8+
}));
9+
10+
describe('getBundleAnalyzerPort', () => {
11+
it('returns the port of the bundle analyzer server', async () => {
12+
const compiler: Compiler = {
13+
// @ts-expect-error: Mock `Compiler` object.
14+
options: {
15+
plugins: [new BundleAnalyzerPlugin()],
16+
},
17+
};
18+
19+
const plugin = jest.mocked(BundleAnalyzerPlugin);
20+
const instance = plugin.mock.instances[0];
21+
22+
// @ts-expect-error: Partial `server` mock.
23+
instance.server = Promise.resolve({
24+
http: {
25+
address: () => 'http://localhost:8888',
26+
},
27+
});
28+
29+
const port = await getBundleAnalyzerPort(compiler);
30+
expect(port).toBe(8888);
31+
});
32+
33+
it('returns the port of the bundle analyzer server that returns an object', async () => {
34+
const compiler: Compiler = {
35+
// @ts-expect-error: Mock `Compiler` object.
36+
options: {
37+
plugins: [new BundleAnalyzerPlugin()],
38+
},
39+
};
40+
41+
const plugin = jest.mocked(BundleAnalyzerPlugin);
42+
const instance = plugin.mock.instances[0];
43+
44+
// @ts-expect-error: Partial `server` mock.
45+
instance.server = Promise.resolve({
46+
http: {
47+
address: () => {
48+
return {
49+
port: 8888,
50+
};
51+
},
52+
},
53+
});
54+
55+
const port = await getBundleAnalyzerPort(compiler);
56+
expect(port).toBe(8888);
57+
});
58+
59+
it('returns undefined if the bundle analyzer server is not available', async () => {
60+
const compiler: Compiler = {
61+
// @ts-expect-error: Mock `Compiler` object.
62+
options: {
63+
plugins: [new BundleAnalyzerPlugin()],
64+
},
65+
};
66+
67+
const port = await getBundleAnalyzerPort(compiler);
68+
expect(port).toBeUndefined();
69+
});
70+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Compiler } from 'webpack';
2+
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
3+
4+
/**
5+
* Get the port of the bundle analyzer server.
6+
*
7+
* @param compiler - The Webpack compiler.
8+
* @returns The port of the bundle analyzer server.
9+
*/
10+
export async function getBundleAnalyzerPort(compiler: Compiler) {
11+
const analyzerPlugin = compiler.options.plugins.find(
12+
(plugin): plugin is BundleAnalyzerPlugin =>
13+
plugin instanceof BundleAnalyzerPlugin,
14+
);
15+
16+
if (analyzerPlugin?.server) {
17+
const { http } = await analyzerPlugin.server;
18+
19+
const address = http.address();
20+
if (typeof address === 'string') {
21+
const { port } = new URL(address);
22+
return parseInt(port, 10);
23+
}
24+
25+
return address?.port;
26+
}
27+
28+
return undefined;
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
declare module 'webpack-bundle-analyzer' {
2+
import type { Server } from 'http';
3+
import type { Compiler, WebpackPluginInstance } from 'webpack';
4+
5+
export type BundleAnalyzerPluginOptions = {
6+
analyzerPort?: number | undefined;
7+
logLevel?: 'info' | 'warn' | 'error' | 'silent' | undefined;
8+
openAnalyzer?: boolean | undefined;
9+
};
10+
11+
export class BundleAnalyzerPlugin implements WebpackPluginInstance {
12+
readonly opts: BundleAnalyzerPluginOptions;
13+
14+
server?: Promise<{
15+
http: Server;
16+
}>;
17+
18+
constructor(options?: BundleAnalyzerPluginOptions);
19+
20+
apply(compiler: Compiler): void;
21+
}
22+
}

packages/snaps-cli/src/utils/logging.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
1-
import { blue, dim, red, yellow } from 'chalk';
1+
import { blue, dim, green, red, yellow } from 'chalk';
22
import type { Ora } from 'ora';
33

4-
import { error, info, warn } from './logging';
4+
import { error, info, success, warn } from './logging';
5+
6+
describe('success', () => {
7+
it('logs a success message', () => {
8+
const log = jest.spyOn(console, 'log').mockImplementation();
9+
10+
success('foo');
11+
expect(log).toHaveBeenCalledWith(`${green('✔')} foo`);
12+
});
13+
14+
it('clears a spinner if provided', () => {
15+
jest.spyOn(console, 'warn').mockImplementation();
16+
17+
const spinner = { clear: jest.fn(), frame: jest.fn() } as unknown as Ora;
18+
success('foo', spinner);
19+
20+
expect(spinner.clear).toHaveBeenCalled();
21+
expect(spinner.frame).toHaveBeenCalled();
22+
});
23+
});
524

625
describe('warn', () => {
726
it('logs a warning message', () => {

packages/snaps-cli/src/utils/logging.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import { logError, logInfo, logWarning } from '@metamask/snaps-utils';
2-
import { blue, dim, red, yellow } from 'chalk';
2+
import { blue, dim, green, red, yellow } from 'chalk';
33
import type { Ora } from 'ora';
44

55
/**
6-
* Log a warning message. The message is prefixed with "Warning:".
6+
* Log a success message. The message is prefixed with a green checkmark.
7+
*
8+
* @param message - The message to log.
9+
* @param spinner - The spinner to clear.
10+
*/
11+
export function success(message: string, spinner?: Ora) {
12+
if (spinner) {
13+
spinner.clear();
14+
spinner.frame();
15+
}
16+
17+
logInfo(`${green('✔')} ${message}`);
18+
}
19+
20+
/**
21+
* Log a warning message. The message is prefixed with a yellow warning sign.
722
*
823
* @param message - The message to log.
924
* @param spinner - The spinner to clear.

packages/snaps-cli/src/utils/steps.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ describe('executeSteps', () => {
6363
});
6464
});
6565

66+
it('updates the context if a step returns an object', async () => {
67+
const steps = [
68+
{
69+
name: 'Step 1',
70+
task: jest.fn(),
71+
},
72+
{
73+
name: 'Step 2',
74+
task: jest.fn().mockResolvedValue({ foo: 'baz' }),
75+
},
76+
{
77+
name: 'Step 3',
78+
task: jest.fn(),
79+
},
80+
];
81+
82+
const context = {
83+
foo: 'bar',
84+
};
85+
86+
await executeSteps(steps, context);
87+
88+
expect(steps[0].task).toHaveBeenCalledWith({
89+
...context,
90+
spinner: expect.any(Object),
91+
});
92+
93+
expect(steps[0].task).toHaveBeenCalledWith({
94+
...context,
95+
spinner: expect.any(Object),
96+
});
97+
98+
expect(steps[2].task).toHaveBeenCalledWith({
99+
foo: 'baz',
100+
spinner: expect.any(Object),
101+
});
102+
});
103+
66104
it('sets the exit code to 1 if a step throws an error', async () => {
67105
const log = jest.spyOn(console, 'error').mockImplementation();
68106

0 commit comments

Comments
 (0)