Skip to content

Commit 7d40022

Browse files
committed
Add support for peer dependencies
1 parent e2e0fc5 commit 7d40022

File tree

11 files changed

+135
-5
lines changed

11 files changed

+135
-5
lines changed

integration-tests/bundler/package-types.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,36 @@ test('includes all required local files and references correct node modules but
4747
]
4848
});
4949
});
50+
51+
test('includes peer dependencies correctly', async (t) => {
52+
const fixture = path.join(process.cwd(), 'integration-tests/fixtures/with-peer-dependencies');
53+
const result = await bundler.build({
54+
name: 'the-package-name',
55+
version: '42.0.0',
56+
sourcesFolder: path.join(fixture, 'src'),
57+
entryPoints: [{ js: path.join(fixture, 'src/entry.js') }],
58+
mainPackageJson: await loadPackageJson(fixture)
59+
});
60+
61+
t.deepEqual(result, {
62+
packageJson: {
63+
dependencies: {},
64+
peerDependencies: { 'example-module': '1.2.3' },
65+
main: 'entry.js',
66+
name: 'the-package-name',
67+
version: '42.0.0'
68+
},
69+
contents: [
70+
{
71+
kind: 'source',
72+
source: '{\n "dependencies": {},\n "main": "entry.js",\n "name": "the-package-name",\n "peerDependencies": {\n "example-module": "1.2.3"\n },\n "version": "42.0.0"\n}',
73+
targetFilePath: 'package.json'
74+
},
75+
{
76+
kind: 'reference',
77+
sourceFilePath: path.join(fixture, 'src/entry.js'),
78+
targetFilePath: 'entry.js'
79+
}
80+
]
81+
});
82+
});

integration-tests/fixtures/with-peer-dependencies/node_modules/example-module/index.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/fixtures/with-peer-dependencies/node_modules/example-module/package.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "test-fixture",
3+
"version": "0.0.0-dev",
4+
"peerDependencies": {
5+
"example-module": "1.2.3"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { example } from 'example-module';
2+
export const foo = example;

source/bundler/bundler.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,40 @@ test('builds a bundle for a single file with the correct package.json', async (t
177177
});
178178
});
179179

180+
test('builds a bundle for a single file with the correct peerDependencies in package.json', async (t) => {
181+
const graph = buildDependencyGraph({
182+
entries: [{ filePath: '/foo/bar.js', content: 'true', topLevelDependencies: new Map([['pkg', '42']]) }]
183+
});
184+
const scan = fake.resolves(graph);
185+
const bundler = bundlerFactory({ scan });
186+
187+
const bundle = await bundler.build({
188+
sourcesFolder: '/foo',
189+
entryPoints: [{ js: '/foo/bar.js' }],
190+
name: 'the-name',
191+
version: 'the-version',
192+
mainPackageJson: { peerDependencies: { pkg: '21' } }
193+
});
194+
195+
t.deepEqual(bundle, {
196+
contents: [
197+
{
198+
kind: 'source',
199+
source: '{\n "dependencies": {},\n "main": "bar.js",\n "name": "the-name",\n "peerDependencies": {\n "pkg": "42"\n },\n "version": "the-version"\n}',
200+
targetFilePath: 'package.json'
201+
},
202+
{ kind: 'reference', sourceFilePath: '/foo/bar.js', targetFilePath: 'bar.js' }
203+
],
204+
packageJson: {
205+
name: 'the-name',
206+
version: 'the-version',
207+
dependencies: {},
208+
peerDependencies: { pkg: '42' },
209+
main: 'bar.js'
210+
}
211+
});
212+
});
213+
180214
test('builds a bundle for a single file with the given additional package.json fields, sorted alphabetically', async (t) => {
181215
const graph = buildDependencyGraph({
182216
entries: [{ filePath: '/foo/bar.js', content: 'true' }]

source/bundler/bundler.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,19 @@ type GroupedDependencies = {
3232

3333
function distributeDependencies(
3434
packageDependencies: Record<string, string>,
35-
bundlePeerDependencies: readonly BundleDescription[]
35+
bundlePeerDependencies: readonly BundleDescription[],
36+
mainPackageJson: MainPackageJson
3637
): Readonly<GroupedDependencies> {
3738
const dependencies: Record<string, string> = {};
3839
const peerDependencies: Record<string, string> = {};
3940

4041
for (const [dependencyName, dependencyVersion] of Object.entries(packageDependencies)) {
4142
if (containsBundleWithPackageName(bundlePeerDependencies, dependencyName)) {
4243
peerDependencies[dependencyName] = dependencyVersion;
43-
} else {
44+
} else if (mainPackageJson.peerDependencies?.[dependencyName] === undefined) {
4445
dependencies[dependencyName] = dependencyVersion;
46+
} else {
47+
peerDependencies[dependencyName] = dependencyVersion;
4548
}
4649
}
4750

@@ -62,7 +65,11 @@ function buildPackageJson(
6265
bundlePeerDependencies = []
6366
} = options;
6467

65-
const distributedDependencies = distributeDependencies(packageDependencies, bundlePeerDependencies);
68+
const distributedDependencies = distributeDependencies(
69+
packageDependencies,
70+
bundlePeerDependencies,
71+
mainPackageJson
72+
);
6673
const types =
6774
firstEntryPoint.declarationFile === undefined
6875
? undefined

source/config/package-json.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ test('main package.json: validation fails when dependencies contains non-string
5151
expectedMessages: ['At dependencies.foo: expected string; but got number']
5252
});
5353

54+
test('main package.json: validation fails when peerDependencies is not an object', checkValidationFailure, {
55+
schema: mainPackageJsonSchema,
56+
data: { peerDependencies: true },
57+
expectedMessages: ['At peerDependencies: expected object; but got boolean']
58+
});
59+
60+
test('main package.json: validation fails when peerDependencies contains non-string values', checkValidationFailure, {
61+
schema: mainPackageJsonSchema,
62+
data: { peerDependencies: { foo: 123 } },
63+
expectedMessages: ['At peerDependencies.foo: expected string; but got number']
64+
});
65+
5466
test('main package.json: validation fails when devDependencies is not an object', checkValidationFailure, {
5567
schema: mainPackageJsonSchema,
5668
data: { devDependencies: true },
@@ -91,6 +103,12 @@ test('additional attributes: validation fails when peerDependencies key is given
91103
expectedMessages: ['At peerDependencies: unexpected extra key or index']
92104
});
93105

106+
test('additional attributes: validation fails when devDependencies key is given', checkValidationFailure, {
107+
schema: additionalPackageJsonAttributesSchema,
108+
data: { devDependencies: {} },
109+
expectedMessages: ['At devDependencies: unexpected extra key or index']
110+
});
111+
94112
test('additional attributes: validation fails when main key is given', checkValidationFailure, {
95113
schema: additionalPackageJsonAttributesSchema,
96114
data: { main: 'foo' },

source/config/package-json.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ const optionalStringRecordSchema = optional(stringRecordSchema, { exact: true })
2424
const $mainPackageJsonSchema = struct({
2525
type: optional(literal('module'), { exact: true }),
2626
dependencies: optionalStringRecordSchema,
27-
devDependencies: optionalStringRecordSchema
27+
devDependencies: optionalStringRecordSchema,
28+
peerDependencies: optionalStringRecordSchema
2829
}).pipe(extend(record(string, unknown)));
2930
export type MainPackageJson = NoExpand<Schema.To<typeof $mainPackageJsonSchema>>;
3031
export const mainPackageJsonSchema: Schema<MainPackageJson> = $mainPackageJsonSchema;
@@ -50,6 +51,7 @@ const attributeValueSchema: Schema<JsonValue> = union(
5051
const forbiddenAttributeNames = new Set([
5152
'dependencies',
5253
'peerDependencies',
54+
'devDependencies',
5355
'main',
5456
'name',
5557
'types',

source/dependency-scanner/scanner.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,19 @@ test('returns all detected node_modules dependencies with its corresponding vers
263263
t.deepEqual(result.topLevelDependencies, { 'any-module': 'the-version' });
264264
});
265265

266+
test('returns the detected node_modules dependencies when they are defined as a peer dependency', async (t) => {
267+
const getReferencedSourceFilePaths = fake.returns(['/dir/node_modules/any-module/foo.js']);
268+
const analyzeProject = createFakeAnalyzeProject({ getReferencedSourceFilePaths });
269+
const dependencyScanner = dependencyScannerFactory({ analyzeProject });
270+
271+
const graph = await dependencyScanner.scan('/dir/entry.js', '/dir', {
272+
mainPackageJson: { peerDependencies: { 'any-module': 'the-version' } }
273+
});
274+
const result = graph.flatten('/dir/entry.js');
275+
276+
t.deepEqual(result.topLevelDependencies, { 'any-module': 'the-version' });
277+
});
278+
266279
test('uses the version from devDependencies when includeDevDependencies is true', async (t) => {
267280
const getReferencedSourceFilePaths = fake.returns(['/dir/node_modules/any-module/foo.js']);
268281
const analyzeProject = createFakeAnalyzeProject({ getReferencedSourceFilePaths });

source/dependency-scanner/scanner.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ function extractModuleName(nodeModulePath: string): string {
4747
function isKnownNodeModule(moduleName: string, options: ScanOptions): boolean {
4848
const { includeDevDependencies, mainPackageJson } = options;
4949

50-
if (mainPackageJson.dependencies?.[moduleName] !== undefined) {
50+
if (
51+
mainPackageJson.dependencies?.[moduleName] !== undefined ||
52+
mainPackageJson.peerDependencies?.[moduleName] !== undefined
53+
) {
5154
return true;
5255
}
5356

@@ -70,6 +73,8 @@ function getVersionFromDependencies(
7073

7174
return undefined;
7275
}
76+
77+
// eslint-disable-next-line max-statements -- this will be removed soon when the dependency scanner doesn’t have to take care about versions anymore
7378
function determineVersionNumber(moduleName: string, options: ScanOptions): string {
7479
const { mainPackageJson } = options;
7580
const version = getVersionFromDependencies(moduleName, mainPackageJson.dependencies);
@@ -78,13 +83,20 @@ function determineVersionNumber(moduleName: string, options: ScanOptions): strin
7883
return version;
7984
}
8085

86+
const peerDependencyVersion = getVersionFromDependencies(moduleName, mainPackageJson.peerDependencies);
87+
88+
if (peerDependencyVersion !== undefined) {
89+
return peerDependencyVersion;
90+
}
91+
8192
const devDependencyVersion = getVersionFromDependencies(moduleName, mainPackageJson.devDependencies);
8293
if (devDependencyVersion !== undefined) {
8394
return devDependencyVersion;
8495
}
8596

8697
throw new Error(`Couldn’t determine version number of ${moduleName}`);
8798
}
99+
88100
function addVersionNumbersToModules(moduleNames: readonly string[], options: ScanOptions): ReadonlyMap<string, string> {
89101
return new Map<string, string>(
90102
moduleNames.map((moduleName) => {

0 commit comments

Comments
 (0)