Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
39 changes: 39 additions & 0 deletions packages/jsii-diff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,45 @@ abstract members yet.
for subclassing, but treating them as such would limit the evolvability of
libraries too much.

## Accepting breaking changes

Sometimes you want to move forward with a change, even if it would technically
be a breaking change to your API. In order to do that, run `jsii-diff`
with the flag `--keys`. It will then print an identifier for every violation
between square brackets, for example:

```
$ jsii-diff <old> --keys
IFACE pkg.MyStruct: formerly required property 'prop' is optional: returned from pkg.IConsumer.someMethod [weakened:pkg.MyStruct]
```

To accept a breaking finding, put the key (in this example `weakened:pkg.MyStruct`)
into a text file, for example `allowed-breaking-changes.txt`, and pass it to
`jsii-diff` as an ignore file:

```
$ jsii-diff <old> --keys --ignore-file allowed-breaking-changes.txt
(no error)
```

### Moving/renaming API elements

If you've moved API elements around between versions of your library, you can
put a special ignore marker starting with `move:` into your `--ignore-file`. To
separate the old and new class names, you can use `:`, `,` or whitespace.

For example:

```
move:package.OldClassName package.NewClassName
move:package.OldClassName:package.NewClassName
move:package.OldClassName, package.NewClassName
```

Moving API elements is always breaking, but using this feature you can confirm
that you at least didn't break anything in the API surface of the moved classes
themselves.

## Help! jsii-diff is marking my changes as breaking

See [BREAKING_CHANGES.md](./BREAKING_CHANGES.md) for more information.
Expand Down
47 changes: 45 additions & 2 deletions packages/jsii-diff/bin/jsii-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,15 @@ async function main(): Promise<number> {
);
}

const allowedBreakingChanges = await loadFilter(argv['ignore-file']);
const fqnRemapping: Record<string, string> = extractFqnRemappings(
allowedBreakingChanges,
);

LOG.info('Starting analysis');
const mismatches = compareAssemblies(original, updated, {
defaultExperimental: argv['default-stability'] === 'experimental',
fqnRemapping,
});

LOG.info(`Found ${mismatches.count} issues`);
Expand All @@ -135,7 +141,7 @@ async function main(): Promise<number> {
const diags = classifyDiagnostics(
mismatches,
treatAsError(argv['error-on'] as ErrorClass, argv['experimental-errors']),
await loadFilter(argv['ignore-file']),
allowedBreakingChanges,
);

process.stderr.write(
Expand All @@ -155,6 +161,43 @@ async function main(): Promise<number> {
return 0;
}

/**
* Extract all lines that start with `move:` from the given string set
*
* Interpret them as `move:OLDFQN <sep> NEWFQN`, mapping moved FQNs.
*
* Separator can be any of `:`, comma or whitespace.
*
* Modifies the input set in-place.
*/
function extractFqnRemappings(
allowedBreakingChanges: Set<string>,
): Record<string, string> {
const ret: Record<string, string> = {};

for (const line of Array.from(allowedBreakingChanges)) {
const prefix = 'move:';
if (!line.startsWith(prefix)) {
continue;
}

const parts = line
.slice(prefix.length)
.trim()
.split(/[:, \t]+/g);
if (parts.length !== 2) {
throw new Error(
`Invalid moved FQN declaration: ${line}. Expected format is 'move:old:new'`,
);
}
const [oldFqn, newFqn] = parts;
ret[oldFqn] = newFqn;
allowedBreakingChanges.delete(line);
}

return ret;
}

// Allow both npm:<package> (legacy) and npm://<package> (looks better)
const NPM_REGEX = /^npm:(\/\/)?/;

Expand Down Expand Up @@ -311,7 +354,7 @@ async function loadFilter(filterFilename?: string): Promise<Set<string>> {
(await fs.readFile(filterFilename, { encoding: 'utf-8' }))
.split('\n')
.map((x) => x.trim())
.filter((x) => !x.startsWith('#')),
.filter((x) => x && !x.startsWith('#')),
);
} catch (e: any) {
if (e.code !== 'ENOENT') {
Expand Down
Loading
Loading