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

[api-extractor] Problems with ember addon #1864

Closed
gossi opened this issue May 10, 2020 · 5 comments
Closed

[api-extractor] Problems with ember addon #1864

gossi opened this issue May 10, 2020 · 5 comments
Labels
repro confirmed The issue comments included repro instructions, and the maintainers reproduced the problem

Comments

@gossi
Copy link

gossi commented May 10, 2020

I'm trying to use api-extractor within an ember addon but the output from api-extractor is "empty".

Ember addons follow the conventions of the ember framework, which is important for their resolver to find the correct files. Most prominent part of the framework are components. These components can appear in template-only or with a backing up class. If there is a class, this is either a JS or TS file and can be documented as usual. Template-only components do not have a backing class, in order to document them, using a *.d.ts is one of the arising best practices within the ember community. So there are either *.ts or *.d.ts files which I plan to document. Due to embers nature, there is no entrance file, so I created an api.d.ts file and re-export everything I planned on documenting (I feel like I did a manual d.ts rollup ?!?).

I have a public monorepo in which I want to do it. For testing purposes I started with one package in this repo.

Here is the branch and the package: https://github.com/gossi/hokulea/tree/api-extractor/packages/buttons
The entry file: https://github.com/gossi/hokulea/blob/api-extractor/packages/buttons/addon/api.d.ts

Output

DocModel (at temp/buttons.api.json):

{
  "metadata": {
    "toolPackage": "@microsoft/api-extractor",
    "toolVersion": "7.8.0",
    "schemaVersion": 1003,
    "oldestForwardsCompatibleVersion": 1001
  },
  "kind": "Package",
  "canonicalReference": "@hokulea/buttons!",
  "docComment": "/**\n * Hokulea buttons package\n *\n * @packageDocumentation\n */\n",
  "name": "@hokulea/buttons",
  "members": [
    {
      "kind": "EntryPoint",
      "canonicalReference": "@hokulea/buttons!",
      "name": "",
      "members": []
    }
  ]
}

And d.ts rollup from api-extractor (at api/buttons.d.ts):

/**
 * Hokulea buttons package
 *
 * @packageDocumentation
 */

import { default as AccentButtonComponent } from '@hokulea/buttons/components/accent-button';
import { default as AdjacentButtonComponent } from '@hokulea/buttons/components/adjacent-button';
import { default as ButtonComponent } from '@hokulea/buttons/components/button';
import { default as DangerButtonComponent } from '@hokulea/buttons/components/danger-button';
import { default as GhostButtonComponent } from '@hokulea/buttons/components/ghost-button';
export { AccentButtonComponent }
export { AdjacentButtonComponent }
export { ButtonComponent }
export { DangerButtonComponent }
export { GhostButtonComponent }

export { }

I hope you can help me get the content in there. I'm around in gitter, happy to chat on this, too.

Thanks a lot

@octogonz octogonz added the repro confirmed The issue comments included repro instructions, and the maintainers reproduced the problem label May 10, 2020
@octogonz
Copy link
Collaborator

octogonz commented May 10, 2020

@gossi thanks for making this repro, very helpful!

API Extractor understands NPM packages and is designed to be able to analyze an SDK comprised of many packages. You would invoke API Extractor once for each package. (Generally we assume API Extractor's entry point is the default entry point for a package.json, although this is not strictly required.) Thus, from the perspective of api/buttons.d.ts, the analyzer sees your exported ButtonComponent declaration as NOT being a TypeScript class. Instead, it is an import alias for the TypeScript class that "belongs" to the @hokulea/buttons external package.

To understand why, suppose we are generating an API docs website. We would not want the entire ButtonComponent with all its members to appear under two different packages. Instead, the @hokulea/buttons will show the full signature, whereas this api/buttons.d.ts entry point should document the import alias as an stub entry. Today API Documenter doesn't actually show these stubs yet, which is why it's completely missing from the .api.json file. But when we fix that, it will appear there as a small stub, not as the full class signature. (The same consideration applies to .d.ts rollups -- we can't pull in an import alias because that will create a second declaration, which the TypeScript compiler may consider to be incompatible with the original.)

Sometimes people split their project into packages purely for internal organizational purposes, so from a customer's standpoint the API really comes from one master package. For this scenario, the API Extractor has a setting bundledPackages. For example, if we add this to your repro api-extractor.json file:

  /**
   * A list of NPM package names whose exports should be treated as part of this package.
   *
   * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
   * and another NPM package "library2" is embedded in this bundle.  Some types from library2 may become part
   * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
   * imports library2.  To avoid this, we can specify:
   *
   *   "bundledPackages": [ "library2" ],
   *
   * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
   * local files for library1.
   */
  "bundledPackages": [ "@hokulea/buttons" ],

...then magically all those components will appear in your .api.json file and also in the .d.ts rollup.

This might be a solution. However, taking a step back and looking at your project setup, the usage of imports is unconventional and could be improved:

  • Why are we using '@hokulea/buttons/components/adjacent-button' to refer to our own package? That notation is for external packages. Seems that you should be using './buttons/components/adjacent-button' instead. This might be a better fix.

  • The export default class form is deprecated, because it leads to naming accidents such as export { default as Panel } from '@hokulea/buttons/components/button';. Better to use export class.

So maybe you just need to straighten out your import/export usage, and then these problems will go away.

@gossi
Copy link
Author

gossi commented May 10, 2020

Thanks a lot, very insightful explanations.

One more question here:

As buttons was just a test, the idea is of course to document all packages in there. What's the best approach for this?

  1. One api-extractor.json config per package (though they will have a shared one to extend) and each package is creating its own <package>.api.json. And a second api-extractor.json for the whole library, the contains all bundled packages?
  2. Only one api-extractor.json in the root of the library, containing the config for everything?

Thing is, I may might want to have one api.d.ts per package in which I define the public interface for them.

@octogonz
Copy link
Collaborator

octogonz commented May 11, 2020

The typical model for API Extractor projects would be to break your SDK up into separate NPM packages. A consumer might import them like this:

import { Button, AdjacentButton } from "@hokulea/buttons";
import { Panel, ScrollPanel } from "@hokulea/panels";
import { Rectangle, HokuleaConstants } from "@hokulea/core";

Note that with this approach, Button and AdjacentButton are all exports of the default entry point for the package. For the above example, you would have a separate api-extractor.json file for each project (@hokulea/buttons, @hokulea/panels, @hokulea/core). And then if you want to generate an API documentation website, you copy all the .api.json files into a central folder, and then API Documenter processes them as a group to produce a single integrated website, showing all your packages together.

Typically there is not any "whole library" package. Consumers don't import from @hokulea/whole-library -- they need to install the individual packages. If they get published as a group, you can use something like Rush lockStepVersion to ensure they have a single changelog and they all get the exact same version.

Now, continuing my example above, suppose that GhostButton is also exported by @hokulea/buttons, but we are not importing it. This is typically not a performance concern, because Webpack tree-shaking will discard that code as an optimization when generating the bundle for the consumer's application.

Sometimes people want to micromanage the bundling, however. In this case, they use "path based imports", which requires consumers to import like this:

import { Button } from "@hokulea/buttons/components/button";
import { AdjacentButton } from "@hokulea/buttons/components/adjacent-button";
import { Panel } from "@hokulea/panels/components/panel";
import { Panel, ScrollPanel } from "@hokulea/panels/components/scroll-panel";
import { Rectangle, HokuleaConstants } from "@hokulea/core/utils";

or the all-in-one @hokulea/sdk package approach might look like this:

import { Button } from "@hokulea/sdk/components/button";
import { AdjacentButton } from "@hokulea/sdk/components/adjacent-button";
import { Panel } from "@hokulea/sdk/components/panel";
import { Panel, ScrollPanel } from "@hokulea/sdk/components/scroll-panel";
import { Rectangle, HokuleaConstants } from "@hokulea/sdk/utils";

This micromanagement ensures that Webpack will only process the specific entry points (paths) that the client is using. But it has downsides: Users now need to be aware of a complex tree of file paths. Imports are more verbose. It's difficult to know which file paths are intended to be "public" versus "internal." Today, API Extractor does not support path-based imports. We plan to support it eventually for API Extractor, but this feature hasn't been prioritized because it is not a best practice from an API contract standpoint.

Nonetheless there are sometimes valid reasons why people need to use path-based imports. For the API report feature and documentation feature, you can work around it by creating a fake index.ts file for API Extractor that reexports the individual paths. Unfortunately there isn't a workaround for using path-based imports with the .d.ts rollup feature.

@gossi
Copy link
Author

gossi commented May 11, 2020

Thanks again a thousand times, very insightful! I know how to approach this now.

This already makes it a really good Knowledge-Base candidate =) I close it once I'm done.

@gossi
Copy link
Author

gossi commented May 16, 2020

Note that with this approach, Button and AdjacentButton are all exports of the default entry point for the package. For the above example, you would have a separate api-extractor.json file for each project (@hokulea/buttons, @hokulea/panels, @hokulea/core). And then if you want to generate an API documentation website, you copy all the .api.json files into a central folder, and then API Documenter processes them as a group to produce a single integrated website, showing all your packages together.

I did exactly this! Very, very helpful - thank you so much. Here is the result:

Happily closing this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
repro confirmed The issue comments included repro instructions, and the maintainers reproduced the problem
Projects
None yet
Development

No branches or pull requests

2 participants