Skip to content

Add API Extractor for Firestore + API Pruning #3790

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

Closed
wants to merge 24 commits into from

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Sep 16, 2020

This adds support for API extractor to Firestore Exp and Firestore Lite and a script to prune its output to match our previous manual API layer.

I manually verified that the new and the old type files are equivalent (which was quite painful, but very necessary).

There are some things we could still improve:

  • Remove unused imports
  • Go back to export instead of export declare
  • Whitespace between lines.

@changeset-bot
Copy link

changeset-bot bot commented Sep 16, 2020

⚠️ No Changeset found

Latest commit: 9f9cc75

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@schmidt-sebastian schmidt-sebastian changed the title Add API Extractor for Firestor + API Pruning Add API Extractor for Firestore + API Pruning Sep 16, 2020
@google-oss-bot
Copy link
Contributor

google-oss-bot commented Sep 16, 2020

Size Analysis Report

Affected Products

No changes between base commit (b6925be) and head commit (dc8d06a).

Test Logs

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Sep 16, 2020

Binary Size Report

Affected SDKs

  • @firebase/firestore

    Type Base (b6925be) Head (dc8d06a) Diff
    browser 249 kB 249 kB +96 B (+0.0%)
    esm2017 197 kB 197 kB +96 B (+0.0%)
    main 484 kB 484 kB -9 B (-0.0%)
    module 247 kB 247 kB +96 B (+0.0%)
    react-native 197 kB 197 kB +96 B (+0.0%)
  • @firebase/firestore-exp

    Type Base (b6925be) Head (dc8d06a) Diff
    browser ? 189 kB ? (?)
    main ? 477 kB ? (?)
    module ? 189 kB ? (?)
    react-native ? 189 kB ? (?)
  • @firebase/firestore-lite

    Type Base (b6925be) Head (dc8d06a) Diff
    browser ? 63.5 kB ? (?)
    main ? 140 kB ? (?)
    module ? 63.5 kB ? (?)
    react-native ? 63.7 kB ? (?)
  • @firebase/firestore/memory

    Type Base (b6925be) Head (dc8d06a) Diff
    browser 187 kB 187 kB +96 B (+0.1%)
    esm2017 147 kB 147 kB +96 B (+0.1%)
    main 357 kB 357 kB -9 B (-0.0%)
    module 185 kB 185 kB +96 B (+0.1%)
    react-native 147 kB 148 kB +96 B (+0.1%)
  • firebase

    Type Base (b6925be) Head (dc8d06a) Diff
    firebase-firestore.js 287 kB 287 kB +94 B (+0.0%)
    firebase-firestore.memory.js 227 kB 227 kB +94 B (+0.0%)
    firebase.js 831 kB 831 kB +94 B (+0.0%)

Test Logs

@@ -1,544 +1,557 @@
/**
Copy link
Member

Choose a reason for hiding this comment

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

Should this file be not committed since it's generated now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer not deleting it yet as it makes the changes to this tool and the API in general much more visible. A next step is to use the new d.ts file as input to API Explorer. I think we can remove it once that is done.

},
{
// Copy into generated source files to support API Extractor
src: 'src/protos/firestore_proto_api.d.ts',
Copy link
Member

Choose a reason for hiding this comment

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

You can change firestore_proto_api.d.ts to a ts file even though it only has types, so TS will automatically generate the d.ts file for you in the dist folder.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is L5 level genius.

input: {
type: 'string',
demandOption: true,
desc: 'The location of the index.ts file'
Copy link
Member

Choose a reason for hiding this comment

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

s/index.ts/index.d.ts

* - Constructors are made private or protected if marked with
* `@hideconstructor`/`@hideconstructor protected`.
*
* This function is meant to operate on DTS files generated by API explorer
Copy link
Member

Choose a reason for hiding this comment

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

s/explorer/extractor

* export class PublicFoo {}
* export function doFoo(foo: PublicFoo);
*/
function extractPublicName(
Copy link
Member

Choose a reason for hiding this comment

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

Why would we use a private type in a public API? This feels like an issue that should be fixed in the source code. In case of multiple public classes extending PrivateFoo, we use the first one which seems arbitrary and is probably wrong - considering PublicFoo1 and PublicFoo2, both of which extends PrivateFoo. doFoo should accept either of them, but after this change, it will only accept PublicFoo1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is definitely an issue in our source code, but one that saves us 1000+ lines of code. The Lite SDK and the Exp SDK use the same Collection/Document/Query types and APIs, even though the actual instance of FirebaseFirestore/FirebaseDataConverter is different. If I don't perform this hack, then two versions of FirebaseDataConverter exist and all APIs that seemingly depend on the Lite type leak types that are invisible.

Copy link
Member

Choose a reason for hiding this comment

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

Can you show me an example in a snippet? Don't lite and exp have separate d.ts files, why would there be 2 versions of FirebaseDataConverter? and what do you mean by leaking invisible types?

exportedTypes.push(
ts.updateExpressionWithTypeArguments(
type,
type.typeArguments,
Copy link
Member

Choose a reason for hiding this comment

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

What if the public class/interface takes different numbers of type arguments? Will it create broken typings for this example?

class A<T> {
   data: T;
}

class B extends A<string> {
}

ts.updateExpressionWithTypeArguments(
type,
type.typeArguments,
ts.createIdentifier(publicName)
Copy link
Member

Choose a reason for hiding this comment

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

IIUC, this can potentially make the class/interface has more members than it really has. Considering this:

class A {
   data: string;
}

export class C extends A {
   additionalData: number;
}

export class B  extends A {
}

will become

export class C {
  data: string;
  additionalData: number;
}

export class B  extends C {
}

Now B has additionalData it didn't have before.

I think the safest strategy is always copy the members from the private symbols instead of replacing it with a public one that extends it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This becomes:

export declare class B2 {
    data?: string;
}
export declare class C2 extends B2 {
    additionalData?: number;
}

That seems correct to me.

Copy link
Member

Choose a reason for hiding this comment

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

I'm confused. Is B2 B and C2 C from my example? Or are you giving a different example?

@Feiyang1
Copy link
Member

Nit: I saw some usage of == and != which should be replaced by === and !== instead.

Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

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

Here it is again.

Sorry, the commit history is a bit messy, but I wanted to keep it in case I need to go back to an earlier revision.

I addressed your main two points as follows:

  • If a private API has different generic arguments from a public API, I will no longer simply substitute their names. Instead, I merge the declarations of the private API into the public type.
  • To merge the declarations, I now use TypeScript's codefix package. At first, I tried to just port the code that we need, but it quickly got out of hand. So for now, this script relies on the same private API that VS Code uses to auto-fill missing imports. If this private API ever breaks, we can move the required parts into our repository (a hacky attempt is here: ffc5ba1#diff-6db0153ee9f6f29fa6fc10782448b200R204, but it still has some issues)

@@ -1,544 +1,557 @@
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer not deleting it yet as it makes the changes to this tool and the API in general much more visible. A next step is to use the new d.ts file as input to API Explorer. I think we can remove it once that is done.

},
{
// Copy into generated source files to support API Extractor
src: 'src/protos/firestore_proto_api.d.ts',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is L5 level genius.

* export class PublicFoo {}
* export function doFoo(foo: PublicFoo);
*/
function extractPublicName(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is definitely an issue in our source code, but one that saves us 1000+ lines of code. The Lite SDK and the Exp SDK use the same Collection/Document/Query types and APIs, even though the actual instance of FirebaseFirestore/FirebaseDataConverter is different. If I don't perform this hack, then two versions of FirebaseDataConverter exist and all APIs that seemingly depend on the Lite type leak types that are invisible.

ts.updateExpressionWithTypeArguments(
type,
type.typeArguments,
ts.createIdentifier(publicName)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This becomes:

export declare class B2 {
    data?: string;
}
export declare class C2 extends B2 {
    additionalData?: number;
}

That seems correct to me.

if (
publicSymbol &&
publicSymbol.name !== currentName &&
verifyMatchingTypeArguments(
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure it's the sufficient condition for a safe replacement. Considering:

declare class A<T> {
   aData: T;
}

export declare class B<T> extends A<string> {
    bData: T; 
}

export declare class C<T> extends A<T> {
}

Will it become?

export declare class B<T> {
    aData: string;
    bData: T; 
}
export declare class C<T> extends B<T> { // it is incorrect, because the type of `aData` becomes `string` which was `T` in the original definition.
}

// can explore adding a slimmed down version of this package to our repository
// if this dependency should ever break.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ts as any).codefix.createMissingMemberNodes(
Copy link
Member

Choose a reason for hiding this comment

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

Does it handle generics with different names? For example:

declare class A<T> {
  aData: T;
}

export declare class B<K> extends A<K> {
  bData:K;
}

Does it become:

export declare class B<K> {
  aData: K; // instead of aData: T
  bData: K;
}

* export class PublicFoo {}
* export function doFoo(foo: PublicFoo);
*/
function extractPublicName(
Copy link
Member

Choose a reason for hiding this comment

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

Can you show me an example in a snippet? Don't lite and exp have separate d.ts files, why would there be 2 versions of FirebaseDataConverter? and what do you mean by leaking invisible types?

@schmidt-sebastian
Copy link
Contributor Author

@Feiyang1 - Can you take another look at this?

@Feiyang1
Copy link
Member

Feiyang1 commented Oct 2, 2020

@schmidt-sebastian I don't see any new changes since my last review. Did you mean to push something, but didn't?

@schmidt-sebastian schmidt-sebastian deleted the mrschmidt/pruneapi branch November 25, 2020 00:02
@firebase firebase locked and limited conversation to collaborators Dec 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants