Skip to content

Commit beea2ea

Browse files
authored
feat: Throw if there are conflicting dependency names (#1155)
Adds validation to check that all dependencies have a unique name. This follows on from [a discussion] with @zhibek about why we need to turn off webpack's minification optimisation. TL;DR two dependency classes can be minified to the same one-letter name, and since dependencies are keyed by class name, this causes nasty runtime errors. Lambda Wrapper will now be able to detect this situation and suggest turning off minification, linking to the [readme note] of how to do this for webpack. As part of this, dependency classes will be deduplicated before being instantiated. If a dependency is specified several times (unlikely but possible) it will now be instantiated only once. Noting for completeness in case we discover unexpected side effects down the line. Jira: [ENG-2728] [a discussion]: comicrelief/serverless-payments#614 (comment) [readme note]: https://github.com/comicrelief/lambda-wrapper/tree/beta#notes
1 parent fcdff3e commit beea2ea

4 files changed

Lines changed: 103 additions & 8 deletions

File tree

src/core/DependencyInjection.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,38 @@ export default class DependencyInjection<TConfig extends LambdaWrapperConfig = a
2828
readonly event: any,
2929
readonly context: Context,
3030
) {
31-
const classes = Object.values(config.dependencies);
31+
// get unique dependency classes -- a class may be included several times,
32+
// but should be instantiated only once
33+
const classes = Array.from(new Set(Object.values(config.dependencies)));
34+
35+
// guard against duplicate keys
36+
const countByName = classes
37+
.map((Constructor) => Constructor.name)
38+
.reduce(
39+
(counts, name) => ({ ...counts, [name]: (counts[name] || 0) + 1 }),
40+
{} as Record<string, number>,
41+
);
42+
if (Object.values(countByName).some((count) => count > 1)) {
43+
const duplicateNames = Object.entries(countByName)
44+
.filter(([, count]) => count > 1)
45+
.map(([name]) => name);
46+
47+
// if all class names are single-letter, they're probably minified -- in
48+
// this case, give a hint about how to fix it
49+
const action = duplicateNames.every((it) => it.length === 1)
50+
? "If you don't recognise the single-letter names listed above, your "
51+
+ "bundler may be minifying your code. You'll need to disable this "
52+
+ 'for Lambda Wrapper to work correctly. Please refer to the Notes '
53+
+ 'section of the Lambda Wrapper readme:\n\n'
54+
+ ' https://github.com/comicrelief/lambda-wrapper/tree/beta#notes'
55+
: 'Please ensure that all dependency classes have a unique name.';
56+
57+
throw new Error(
58+
`Dependency names are not unique: ${duplicateNames.join(', ')}\n\n${action}`,
59+
);
60+
}
61+
62+
// instantiate all dependencies
3263
this.dependencies = Object.fromEntries(
3364
classes.map((Constructor) => [Constructor.name, new Constructor(this)]),
3465
);

tests/mocks/dependencies.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { DependencyAwareClass } from '@/src';
2+
3+
export class A extends DependencyAwareClass {}
4+
5+
export class B extends DependencyAwareClass {}
6+
7+
export class C extends DependencyAwareClass {}
8+
9+
export class ServiceA extends DependencyAwareClass {}

tests/mocks/dependencies2.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Some more dependency classes, with the same name as those in dependencies.ts
2+
// so that we can test name conflicts.
3+
4+
import { DependencyAwareClass } from '@/src';
5+
6+
export class A extends DependencyAwareClass {}
7+
8+
export class ServiceA extends DependencyAwareClass {}

tests/unit/core/DependencyInjection.spec.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { DependencyAwareClass, DependencyInjection } from '@/src';
1+
import { DependencyInjection } from '@/src';
22
import { mockContext, mockEvent } from '@/tests/mocks/aws';
3-
4-
class A extends DependencyAwareClass {}
5-
6-
class B extends DependencyAwareClass {}
7-
8-
class C extends DependencyAwareClass {}
3+
import {
4+
A,
5+
B,
6+
C,
7+
ServiceA,
8+
} from '@/tests/mocks/dependencies';
9+
import {
10+
A as AClash,
11+
ServiceA as ServiceAClash,
12+
} from '@/tests/mocks/dependencies2';
913

1014
describe('unit.core.DependencyInjection', () => {
1115
const mockConfig = {
@@ -17,6 +21,49 @@ describe('unit.core.DependencyInjection', () => {
1721

1822
const di = new DependencyInjection(mockConfig, mockEvent, mockContext);
1923

24+
describe('constructor', () => {
25+
describe('dependency conflicts', () => {
26+
it('should throw if dependencies have conflicting names', () => {
27+
const clashConfig = {
28+
dependencies: {
29+
ServiceA,
30+
ServiceAClash,
31+
},
32+
};
33+
34+
expect(
35+
() => new DependencyInjection(clashConfig, mockEvent, mockContext),
36+
).toThrowError('ensure that all dependency classes have a unique name');
37+
});
38+
39+
it('should suggest turning off minification if names are single-letter', () => {
40+
const clashConfig = {
41+
dependencies: {
42+
A,
43+
AClash,
44+
},
45+
};
46+
47+
expect(
48+
() => new DependencyInjection(clashConfig, mockEvent, mockContext),
49+
).toThrowError('your bundler may be minifying your code');
50+
});
51+
52+
it('should not throw if the same dependency is included twice', () => {
53+
const okayConfig = {
54+
dependencies: {
55+
A,
56+
again: A,
57+
},
58+
};
59+
60+
expect(
61+
() => new DependencyInjection(okayConfig, mockEvent, mockContext),
62+
).not.toThrow();
63+
});
64+
});
65+
});
66+
2067
describe('event', () => {
2168
it('should expose the event', () => {
2269
expect(di.event).toBe(mockEvent);

0 commit comments

Comments
 (0)