Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsii): Enforce use of peerDependencies #421

Merged
merged 3 commits into from
Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/jsii-spec/lib/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,18 @@ export interface PackageVersion {
dependencies?: { [assembly: string]: PackageVersion };

/**
* Indicates if this dependency is a peer dependency or a normal dependency.
* Indicates if this dependency is a direct (peer) dependency or a
* transitive dependency.
*
* Peer dependencies are expected to be explicitly defined by the user of
* this library instead of brought in as transitive dependencies.
*
* jsii enforces that if this module exports a type from a dependency, this
* dependency must be defined as a peer and not as a normal dependency.
* Otherwise, it would be impossible to safely use two versions of this
* dependency in a closure.
* jsii enforces that any direct dependency on another jsii module is also
* defined as a peerDependency. Otherwise, it would be impossible to safely
* use two versions of this dependency in a closure.
*
* @see https://github.com/awslabs/aws-cdk/issues/979
* @see https://github.com/awslabs/jsii/issues/361
* @see https://nodejs.org/en/blog/npm/peer-dependencies/
*/
peer?: boolean;
Expand Down
3 changes: 0 additions & 3 deletions packages/jsii/bin/jsii-fix-peers

This file was deleted.

95 changes: 0 additions & 95 deletions packages/jsii/bin/jsii-fix-peers.ts

This file was deleted.

14 changes: 11 additions & 3 deletions packages/jsii/bin/jsii.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@ import { VERSION } from '../lib/version';
.env('JSII')
.option('watch', { alias: 'w', type: 'boolean', desc: 'Watch for file changes and recompile automatically' })
.option('verbose', { alias: 'v', type: 'count', desc: 'Increase the verbosity of output', global: true })
// tslint:disable-next-line:max-line-length
.option('project-references', { alias: 'r', type: 'boolean', desc: 'Generate TypeScript project references (also [package.json].jsii.projectReferences)' })
.option('project-references', {
alias: 'r',
type: 'boolean',
desc: 'Generate TypeScript project references (also [package.json].jsii.projectReferences)'
})
.option('fix-peer-dependencies', {
type: 'boolean',
default: true,
desc: 'Automatically add missing entries in the peerDependencies section of package.json'
})
.help()
.version(VERSION)
.argv;
Expand All @@ -23,7 +31,7 @@ import { VERSION } from '../lib/version';
const projectRoot = path.normalize(path.resolve(process.cwd(), argv._[0] || '.'));

const compiler = new Compiler({
projectInfo: await loadProjectInfo(projectRoot),
projectInfo: await loadProjectInfo(projectRoot, argv['fix-peer-dependencies']),
watch: argv.watch,
projectReferences: argv['project-references']
});
Expand Down
2 changes: 1 addition & 1 deletion packages/jsii/lib/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class Assembler implements Emitter {
version: this.projectInfo.version,
description: this.projectInfo.description || this.projectInfo.name,
license: this.projectInfo.license,
homepage: this.projectInfo.homepage || this.projectInfo.repository.url,
homepage: this.projectInfo.homepage || this.projectInfo.repository.url,
author: this.projectInfo.author,
contributors: this.projectInfo.contributors && [...this.projectInfo.contributors],
repository: this.projectInfo.repository,
Expand Down
42 changes: 37 additions & 5 deletions packages/jsii/lib/project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,55 @@ export interface ProjectInfo {
readonly projectReferences?: boolean;
}

export async function loadProjectInfo(projectRoot: string): Promise<ProjectInfo> {
const pkg = require(path.join(projectRoot, 'package.json'));
export async function loadProjectInfo(projectRoot: string, fixPeerDependencies: boolean): Promise<ProjectInfo> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use keyword arguments for fixPeerDeps

const packageJsonPath = path.join(projectRoot, 'package.json');
const pkg = require(packageJsonPath);

const bundleDependencies: { [name: string]: string } = {};
(pkg.bundleDependencies || pkg.bundledDependencies || []).forEach((name: string) => {
const version = pkg.dependencies && pkg.dependencies[name];
if (!version) {
throw new Error(`The "package.json" has "${name}" in "bundleDependencies", but it is not declared in "dependencies"`);
throw new Error(`The "package.json" file has "${name}" in "bundleDependencies", but it is not declared in "dependencies"`);
}

if (pkg.peerDependencies && name in pkg.peerDependencies) {
throw new Error(`The "package.json" has "${name}" in "bundleDependencies", and also in "peerDependencies"`);
throw new Error(`The "package.json" file has "${name}" in "bundleDependencies", and also in "peerDependencies"`);
}

bundleDependencies[name] = version;
});

let addedPeerDependency = false;
Object.entries(pkg.dependencies || {}).forEach(([name, version]) => {
if (name in bundleDependencies) {
return;
}
pkg.peerDependencies = pkg.peerDependencies || {};
const peerVersion = pkg.peerDependencies[name];
if (peerVersion === version) {
return;
}
if (!fixPeerDependencies) {
if (peerVersion) {
// tslint:disable-next-line: max-line-length
throw new Error(`The "package.json" file has different version requirements for "${name}" in "dependencies" (${version}) versus "peerDependencies" (${peerVersion})`);
}
throw new Error(`The "package.json" file has "${name}" in "dependencies", but not in "peerDependencies"`);
}
if (peerVersion) {
LOG.warn(`Changing "peerDependency" on "${name}" from "${peerVersion}" to ${version}`);
} else {
LOG.warn(`Recording missing "peerDependency" on "${name}" at ${version}`);
}
pkg.peerDependencies[name] = version;
addedPeerDependency = true;
});
// Re-write "package.json" if we fixed up "peerDependencies" and were told to automatically fix.
// Yes, we should never have addedPeerDependencies if not fixPeerDependency, but I still check again.
if (addedPeerDependency && fixPeerDependencies) {
await fs.writeJson(packageJsonPath, pkg, { encoding: 'utf8', spaces: 2 });
}

const transitiveAssemblies: { [name: string]: spec.Assembly } = {};
const dependencies =
await _loadDependencies(pkg.dependencies, projectRoot, transitiveAssemblies, new Set<string>(Object.keys(bundleDependencies)));
Expand Down Expand Up @@ -107,7 +139,7 @@ function _guessRepositoryType(url: string): string {
throw new Error(`The "package.json" file must specify the "repository.type" attribute (could not guess from ${url})`);
}

async function _loadDependencies(dependencies: { [name: string]: string | spec.PackageVersion } | undefined,
async function _loadDependencies(dependencies: { [name: string]: string | spec.PackageVersion } | undefined,
searchPath: string,
transitiveAssemblies: { [name: string]: spec.Assembly },
bundled = new Set<string>()): Promise<spec.Assembly[]> {
Expand Down
82 changes: 71 additions & 11 deletions packages/jsii/test/test.project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ const BASE_PROJECT = {
jsii: {
targets: { foo: { bar: 'baz' } }
},
dependencies: { 'jsii-test-dep': '^1.2.3' }
dependencies: { 'jsii-test-dep': '^1.2.3' },
peerDependencies: { 'jsii-test-dep': '^1.2.3' }
};

export = nodeunit.testCase({
loadProjectInfo: {
async 'loads valid project'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
try {
const info = await loadProjectInfo(projectRoot);
const info = await loadProjectInfo(projectRoot, false);
test.equal(info.name, BASE_PROJECT.name);
test.equal(info.version, BASE_PROJECT.version);
test.equal(info.description, BASE_PROJECT.description);
Expand All @@ -51,7 +52,7 @@ export = nodeunit.testCase({
async 'loads valid project (UNLICENSED)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
try {
const info = await loadProjectInfo(projectRoot);
const info = await loadProjectInfo(projectRoot, false);
test.equal(info && info.license, 'UNLICENSED');
} catch (e) {
test.ifError(e);
Expand All @@ -66,7 +67,7 @@ export = nodeunit.testCase({
async 'loads valid project (using bundleDependencies)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
try {
const info = await loadProjectInfo(projectRoot);
const info = await loadProjectInfo(projectRoot, false);
test.deepEqual(info.bundleDependencies, { bundled: '^1.2.3' });
} catch (e) {
test.ifError(e);
Expand All @@ -82,7 +83,7 @@ export = nodeunit.testCase({
async 'loads valid project (using bundledDependencies)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
try {
const info = await loadProjectInfo(projectRoot);
const info = await loadProjectInfo(projectRoot, false);
test.deepEqual(info.bundleDependencies, { bundled: '^1.2.3' });
} catch (e) {
test.ifError(e);
Expand All @@ -99,7 +100,7 @@ export = nodeunit.testCase({
const contributors = [{ name: 'foo', email: 'nobody@amazon.com' }];
await _withTestProject(async projectRoot => {
try {
const info = await loadProjectInfo(projectRoot);
const info = await loadProjectInfo(projectRoot, false);
test.deepEqual(info && info.contributors && info.contributors.map(_stripUndefined),
contributors.map(c => ({ ...c, roles: ['contributor'] })));
} catch (e) {
Expand All @@ -114,7 +115,7 @@ export = nodeunit.testCase({
await _withTestProject(async projectRoot => {
let error: Error | undefined;
try {
await loadProjectInfo(projectRoot);
await loadProjectInfo(projectRoot, false);
} catch (e) {
error = e;
} finally {
Expand All @@ -131,7 +132,7 @@ export = nodeunit.testCase({
await _withTestProject(async projectRoot => {
let error: Error | undefined;
try {
await loadProjectInfo(projectRoot);
await loadProjectInfo(projectRoot, false);
} catch (e) {
error = e;
} finally {
Expand All @@ -148,7 +149,7 @@ export = nodeunit.testCase({
await _withTestProject(async projectRoot => {
let error: Error | undefined;
try {
await loadProjectInfo(projectRoot);
await loadProjectInfo(projectRoot, false);
} catch (e) {
error = e;
} finally {
Expand All @@ -158,8 +159,66 @@ export = nodeunit.testCase({
}
}, info => {
info.dependencies[TEST_DEP_ASSEMBLY.name] = '^1.2.5';
info.peerDependencies[TEST_DEP_ASSEMBLY.name] = '^1.2.5';
});
}
},

async 'fails to load with missing peerDependency (refusing to auto-fix)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
let error: Error | undefined;
try {
await loadProjectInfo(projectRoot, false);
} catch (e) {
error = e;
} finally {
test.throws(() => { if (error) { throw error; } },
`The "package.json" file has "${TEST_DEP_ASSEMBLY.name}" in "dependencies", but not in "peerDependencies"`);
test.done();
}
}, info => {
delete info.peerDependencies[TEST_DEP_ASSEMBLY.name];
});
},

async 'loads with missing peerDependency (when auto-fixing)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
await loadProjectInfo(projectRoot, true);
const info = require(path.join(projectRoot, 'package.json'));
test.equal(info.peerDependencies[TEST_DEP_ASSEMBLY.name], "^1.2.3");
test.done();
}, info => {
delete info.peerDependencies[TEST_DEP_ASSEMBLY.name];
});
},

async 'fails to load with inconsistent peerDependency (refusing to auto-fix)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
let error: Error | undefined;
try {
await loadProjectInfo(projectRoot, false);
} catch (e) {
error = e;
} finally {
test.throws(() => { if (error) { throw error; } },
// tslint:disable-next-line: max-line-length
`The "package.json" file has different version requirements for "${TEST_DEP_ASSEMBLY.name}" in "dependencies" (^1.2.3) versus "peerDependencies" (^42.1337.0)`);
test.done();
}
}, info => {
info.peerDependencies[TEST_DEP_ASSEMBLY.name] = '^42.1337.0';
});
},

async 'loads with inconsistent peerDependency (when auto-fixing)'(test: nodeunit.Test) {
await _withTestProject(async projectRoot => {
await loadProjectInfo(projectRoot, true);
const info = require(path.join(projectRoot, 'package.json'));
test.equal(info.peerDependencies[TEST_DEP_ASSEMBLY.name], "^1.2.3");
test.done();
}, info => {
info.peerDependencies[TEST_DEP_ASSEMBLY.name] = '^42.1337.0';
});
},
}
});

Expand All @@ -175,7 +234,8 @@ const TEST_DEP_ASSEMBLY: spec.Assembly = {
fingerprint: 'F1NG3RPR1N7',
dependencies: {
'jsii-test-dep-dep': {
version: '3.2.1'
version: '3.2.1',
peer: true,
}
}
};
Expand Down