Skip to content

Commit 7f4e113

Browse files
authored
Support phases and optional dependencies (#7)
* Add phase related logic * Compose plugins with respecting phase specific configs * Write tests for the compose functionality * Create an optional helper * Make it possible to load or require a plugin depending on the current phase * Allow objects to be passed as plugins * Make exports compatible with nodejs
1 parent ef9361e commit 7f4e113

10 files changed

+783
-14
lines changed

Diff for: .eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"jest/globals": true
88
},
99
"rules": {
10+
"import/no-extraneous-dependencies": "off",
1011
"jest/no-disabled-tests": "error",
1112
"jest/no-focused-tests": "error",
1213
"jest/no-identical-title": "error"

Diff for: CONTRIBUTING.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Contributing
22

33
All contributions to this repository are more than welcome and appreciated a lot 🎉
4-
Don't hesitate to [create a new issue](https://github.com/cyrilwanner/next-compose-plugins/issues/new) if you have any question.
4+
Don't hesitate to [create a new issue](https://github.com/cyrilwanner/next-compose-plugins/issues/new) if you have any question.
55

66
## Setup instructions
77

@@ -18,6 +18,7 @@ We write tests for all major functionality of this plugin to ensure a good code
1818
Please update and/or write new tests when contributing to this repository.
1919

2020
You can run the tests locally with `npm test`.
21+
To watch for code changes and automatically run tests on changes, use `npm run test:watch` (babel needs to be started in watching mode too for this in a separate process with `npm run watch`).
2122
Please note that test will get executed against the built sources, so make sure you are either watching the files or build them once (`npm run build`) before running the tests.
2223

2324
## Coding style

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"lint": "eslint src",
1313
"lint:fix": "eslint --fix src",
1414
"test": "jest --coverage lib",
15+
"test:watch": "jest --watch lib",
1516
"prepack": "rimraf lib/**/__tests__"
1617
},
1718
"repository": {

Diff for: src/__tests__/compose.test.js

+358
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
import 'jest';
2+
import { parsePluginConfig, composePlugins } from '../compose';
3+
import { markOptional } from '../optional';
4+
5+
const testPlugin = { test: 'plugin' };
6+
7+
const PHASE_DEVELOPMENT_SERVER = 'phase-development-server';
8+
const PHASE_PRODUCTION_SERVER = 'phase-production-server';
9+
const PHASE_PRODUCTION_BUILD = 'phase-production-build';
10+
11+
describe('next-compose-plugins/compose', () => {
12+
/**
13+
* parsePluginConfig
14+
*
15+
* ----------------------------------------------------
16+
*/
17+
it('parses the plugin without a configuration', () => {
18+
const withoutConfig1 = parsePluginConfig(testPlugin);
19+
expect(withoutConfig1).toEqual({
20+
pluginFunction: { test: 'plugin' },
21+
pluginConfig: {},
22+
phases: null,
23+
});
24+
expect(withoutConfig1.pluginFunction).toBe(testPlugin); // test same reference
25+
26+
const withoutConfig2 = parsePluginConfig([testPlugin]);
27+
expect(withoutConfig2).toEqual({
28+
pluginFunction: { test: 'plugin' },
29+
pluginConfig: {},
30+
phases: null,
31+
});
32+
expect(withoutConfig2.pluginFunction).toBe(testPlugin);
33+
});
34+
35+
it('parses the plugin with a configuration', () => {
36+
const withConfig = parsePluginConfig([testPlugin, { my: 'conf', nested: { foo: 'bar' } }]);
37+
38+
expect(withConfig).toEqual({
39+
pluginFunction: { test: 'plugin' },
40+
pluginConfig: { my: 'conf', nested: { foo: 'bar' } },
41+
phases: null,
42+
});
43+
44+
expect(withConfig.pluginFunction).toBe(testPlugin);
45+
});
46+
47+
it('parses the plugin with a phase restriction', () => {
48+
const withPhaseRestriction = parsePluginConfig([testPlugin, [
49+
PHASE_DEVELOPMENT_SERVER,
50+
PHASE_PRODUCTION_BUILD,
51+
PHASE_PRODUCTION_SERVER,
52+
]]);
53+
54+
expect(withPhaseRestriction).toEqual({
55+
pluginFunction: { test: 'plugin' },
56+
pluginConfig: {},
57+
phases: [PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER],
58+
});
59+
60+
expect(withPhaseRestriction.pluginFunction).toBe(testPlugin);
61+
});
62+
63+
/**
64+
* composePlugins
65+
*
66+
* ----------------------------------------------------
67+
*/
68+
it('passed down the initial configuration', () => {
69+
const plugin = jest.fn((nextConfig) => {
70+
expect(nextConfig).toEqual({ initial: 'config' });
71+
72+
return nextConfig;
73+
});
74+
75+
const result = composePlugins(PHASE_DEVELOPMENT_SERVER, [plugin], { initial: 'config' });
76+
77+
expect(result).toEqual({ initial: 'config' });
78+
expect(plugin).toHaveBeenCalledTimes(1);
79+
});
80+
81+
it('does not execute a plugin if it is not in the correct phase', () => {
82+
const plugin1 = jest.fn(nextConfig => ({ ...nextConfig, plugin1: true }));
83+
const plugin2 = jest.fn(nextConfig => ({ ...nextConfig, plugin2: true }));
84+
const plugin3 = jest.fn(nextConfig => ({ ...nextConfig, plugin3: true }));
85+
86+
const result = composePlugins(PHASE_DEVELOPMENT_SERVER, [
87+
[plugin1, [PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD]],
88+
[plugin2, [PHASE_PRODUCTION_BUILD]],
89+
[plugin3, ['!', PHASE_PRODUCTION_SERVER]],
90+
], { initial: 'config' });
91+
92+
expect(result).toEqual({
93+
initial: 'config',
94+
plugin1: true,
95+
plugin3: true,
96+
});
97+
98+
expect(plugin1).toHaveBeenCalledTimes(1);
99+
expect(plugin2).toHaveBeenCalledTimes(0);
100+
expect(plugin3).toHaveBeenCalledTimes(1);
101+
});
102+
103+
it('merges the plugin configuration', () => {
104+
const plugin1 = jest.fn((nextConfig) => {
105+
expect(nextConfig.plugin1Config).toEqual('bar');
106+
107+
return nextConfig;
108+
});
109+
110+
const plugin2 = jest.fn((nextConfig) => {
111+
expect(nextConfig.plugin2Config).toEqual({ hello: 'world' });
112+
113+
return nextConfig;
114+
});
115+
116+
const plugin3 = jest.fn((nextConfig) => {
117+
expect(nextConfig.plugin3Config).toEqual(false);
118+
119+
return nextConfig;
120+
});
121+
122+
composePlugins(PHASE_DEVELOPMENT_SERVER, [
123+
[plugin1, {
124+
plugin1Config: 'bar',
125+
[PHASE_PRODUCTION_SERVER]: {
126+
plugin1Config: 'foo',
127+
},
128+
}],
129+
[plugin2, {
130+
plugin2Config: { hey: 'you' },
131+
[PHASE_DEVELOPMENT_SERVER]: {
132+
plugin2Config: { hello: 'world' },
133+
},
134+
}],
135+
[plugin3, {
136+
plugin3Config: true,
137+
[PHASE_PRODUCTION_BUILD + PHASE_DEVELOPMENT_SERVER]: {
138+
plugin3Config: false,
139+
},
140+
}],
141+
], {});
142+
143+
expect(plugin1).toHaveBeenCalledTimes(1);
144+
expect(plugin2).toHaveBeenCalledTimes(1);
145+
expect(plugin3).toHaveBeenCalledTimes(1);
146+
});
147+
148+
it('provides next-compose-plugin infos for plugins', () => {
149+
const plugin = jest.fn((nextConfig, info) => {
150+
expect(info).toEqual({
151+
nextComposePlugins: true,
152+
phase: PHASE_DEVELOPMENT_SERVER,
153+
});
154+
155+
return nextConfig;
156+
});
157+
158+
composePlugins(PHASE_DEVELOPMENT_SERVER, [plugin], {});
159+
160+
expect(plugin).toHaveBeenCalledTimes(1);
161+
});
162+
163+
it('does not pass down the updated configuration if it is in the wrong phase', () => {
164+
const plugin1 = jest.fn(nextConfig => ({
165+
...nextConfig,
166+
plugin1Config: 'foo',
167+
phases: [PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD],
168+
}));
169+
170+
const plugin2 = jest.fn(nextConfig => ({
171+
...nextConfig,
172+
plugin2Config: 'bar',
173+
webpack: () => `changed ${nextConfig.webpack()}`,
174+
phases: [PHASE_DEVELOPMENT_SERVER],
175+
}));
176+
177+
const plugin3 = jest.fn(nextConfig => ({
178+
...nextConfig,
179+
plugin3Config: 'world',
180+
}));
181+
182+
const webpackConfig = () => 'initial webpack config';
183+
184+
const result = composePlugins(PHASE_PRODUCTION_BUILD, [plugin1, plugin2, plugin3], {
185+
initial: 'config',
186+
webpack: webpackConfig,
187+
});
188+
189+
expect(result).toEqual({
190+
initial: 'config',
191+
plugin1Config: 'foo',
192+
plugin3Config: 'world',
193+
webpack: webpackConfig,
194+
});
195+
196+
expect(result.webpack()).toEqual('initial webpack config');
197+
198+
expect(plugin1).toHaveBeenCalledTimes(1);
199+
expect(plugin2).toHaveBeenCalledTimes(1);
200+
expect(plugin3).toHaveBeenCalledTimes(1);
201+
});
202+
203+
it('lets the user overwrite the plugins phase', () => {
204+
const plugin1 = jest.fn(nextConfig => ({
205+
...nextConfig,
206+
plugin1Config: 'foo',
207+
phases: [PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD],
208+
}));
209+
210+
const plugin2 = jest.fn(nextConfig => ({
211+
...nextConfig,
212+
plugin2Config: 'bar',
213+
phases: [PHASE_DEVELOPMENT_SERVER],
214+
}));
215+
216+
const result = composePlugins(PHASE_PRODUCTION_BUILD, [plugin1, [plugin2, [PHASE_PRODUCTION_BUILD]]], { initial: 'config' });
217+
218+
expect(result).toEqual({
219+
initial: 'config',
220+
plugin1Config: 'foo',
221+
plugin2Config: 'bar',
222+
});
223+
224+
expect(plugin1).toHaveBeenCalledTimes(1);
225+
expect(plugin2).toHaveBeenCalledTimes(1);
226+
});
227+
228+
it('does not pass down the phase configuration of the previous plugin', () => {
229+
const plugin1 = jest.fn(({ plugin1Config, ...nextConfig }) => {
230+
expect(nextConfig).toEqual({ initial: 'config' });
231+
expect(plugin1Config).toEqual('foo');
232+
233+
return nextConfig;
234+
});
235+
236+
const plugin2 = jest.fn(({ plugin2Config, ...nextConfig }) => {
237+
expect(nextConfig).toEqual({ initial: 'config' });
238+
expect(plugin2Config).toEqual('bar');
239+
240+
return nextConfig;
241+
});
242+
243+
const result = composePlugins(PHASE_DEVELOPMENT_SERVER, [
244+
[plugin1, { plugin1Config: 'foo' }],
245+
[plugin2, { plugin2Config: 'bar' }],
246+
], { initial: 'config' });
247+
248+
expect(result).toEqual({ initial: 'config' });
249+
250+
expect(plugin1).toHaveBeenCalledTimes(1);
251+
expect(plugin2).toHaveBeenCalledTimes(1);
252+
});
253+
254+
it('does not change a reference but always creates new objects', () => {
255+
const plugin1 = jest.fn(nextConfig => ({
256+
...nextConfig,
257+
plugin1Config: 'foo',
258+
phases: [PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD],
259+
}));
260+
261+
const plugin2 = jest.fn((nextConfig) => {
262+
nextConfig.illegallyUpdated = true; // eslint-disable-line no-param-reassign
263+
264+
return {
265+
...nextConfig,
266+
plugin2Config: 'bar',
267+
phases: [PHASE_DEVELOPMENT_SERVER],
268+
};
269+
});
270+
271+
const plugin3 = jest.fn(nextConfig => ({
272+
...nextConfig,
273+
plugin3Config: 'world',
274+
}));
275+
276+
const result = composePlugins(PHASE_PRODUCTION_BUILD, [plugin1, plugin2, plugin3], { initial: 'config' });
277+
278+
expect(result).toEqual({
279+
initial: 'config',
280+
plugin1Config: 'foo',
281+
plugin3Config: 'world',
282+
});
283+
284+
expect(plugin1).toHaveBeenCalledTimes(1);
285+
expect(plugin2).toHaveBeenCalledTimes(1);
286+
expect(plugin3).toHaveBeenCalledTimes(1);
287+
});
288+
289+
it('loads an optional plugin in the correct phase', () => {
290+
const plugin1 = jest.fn(nextConfig => ({
291+
...nextConfig,
292+
plugin1Config: 'foo',
293+
}));
294+
295+
const plugin2 = jest.fn(nextConfig => ({
296+
...nextConfig,
297+
plugin2Config: 'bar',
298+
}));
299+
300+
const result = composePlugins(PHASE_DEVELOPMENT_SERVER, [
301+
[plugin1, [PHASE_DEVELOPMENT_SERVER]],
302+
[markOptional(() => plugin2), [PHASE_DEVELOPMENT_SERVER]],
303+
], { initial: 'config' });
304+
305+
expect(result).toEqual({
306+
initial: 'config',
307+
plugin1Config: 'foo',
308+
plugin2Config: 'bar',
309+
});
310+
311+
expect(plugin1).toHaveBeenCalledTimes(1);
312+
expect(plugin2).toHaveBeenCalledTimes(1);
313+
});
314+
315+
it('does not load an optional plugin in the wrong phase', () => {
316+
const plugin1 = jest.fn(nextConfig => ({
317+
...nextConfig,
318+
plugin1Config: 'foo',
319+
}));
320+
321+
const plugin2 = jest.fn(nextConfig => ({
322+
...nextConfig,
323+
plugin2Config: 'bar',
324+
}));
325+
326+
const result = composePlugins(PHASE_DEVELOPMENT_SERVER, [
327+
[plugin1, [PHASE_DEVELOPMENT_SERVER]],
328+
[markOptional(() => plugin2), [PHASE_PRODUCTION_SERVER]],
329+
], { initial: 'config' });
330+
331+
expect(result).toEqual({
332+
initial: 'config',
333+
plugin1Config: 'foo',
334+
});
335+
336+
expect(plugin1).toHaveBeenCalledTimes(1);
337+
expect(plugin2).toHaveBeenCalledTimes(0);
338+
});
339+
340+
it('handles objects as plugins', () => {
341+
const plugin = {
342+
plugin1Config: 'foo',
343+
};
344+
345+
const result = composePlugins(PHASE_DEVELOPMENT_SERVER, [plugin], { initial: 'config' });
346+
347+
expect(result).toEqual({
348+
initial: 'config',
349+
plugin1Config: 'foo',
350+
});
351+
});
352+
353+
it('throws an error for incompatible plugins', () => {
354+
const plugin = ['something', 'weird'];
355+
356+
expect(() => composePlugins(PHASE_DEVELOPMENT_SERVER, [plugin], {})).toThrowError('Incompatible plugin');
357+
});
358+
});

0 commit comments

Comments
 (0)