In a recent blog series I shared how to go about creating your own Umbraco v14 extensions, but the final piece of the puzzle that I left out was how to deploy your extension type definitions such that others implementing your extensions would have nice type checking.
Well, in this article we are going to look at doing exactly that.
The Challenge
The challenge for Umbraco Package developers that provide their own extension points is that we actually need to split out deployments into two.
- For the actual code that makes up your package, this is likely going to be packaged up in your NuGet package.
- For your extension points, we need to distribute the types for these as an NPM package.
I won’t bother covering the first part as Kevin Jump has already done a great job of outlining how to setup a project for an Umbraco package.
Here then I’ll focus on the second point and how to extend your project to distribute your types.
To set a baseline however, I’ll assume you’ve used Kevin’s early adopters template for your setup.
Step 1: Consolidate Your Types
We don’t want to export all types in our package, rather we just we to export our extension types, such as our custom manifest types. To make this explicit, I decided to add a root level exports.js
file that exports all the types I needed to make public.
export type { ManifestQuickAction, MetaQuickAction
} from './quick-action/types.js'
For brevity here I’m just exporting the types directly in the root level
exports.js
file, but in actuality, I have a number ofexports.js
files located in my project and my root levelexports.js
file re-exports all the other export files likeexport * from './feature/exports.js
Step 2: Generate Your Type Definitions
Now we have all our types exported in one place, we next need to use the TypeScript CLI tool to generate our type definitions.
In your package.json
add a new script
entry as follows
{ "script": { "build:api": "tsc -p tsconfig.api.json", }
}
Next create a tsconfig.api.json
with the following contents
{ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./types", "rootDir": "./src", "declaration": true, "declarationMap": true, "emitDeclarationOnly" : true }
}
Essentially this is telling TypeScript to use our main tsconfig.json
configuration, but to only generate type definitions.
Now if you run this script
npm run build:api
We should get a new types
folder with all our generate .d.ts
files.
One glaring issue here though is that we’ve generated type definitions for our whole package and not just the ones we want to export. At best this is a whole lot of bloat, but at worse, it’s now exposing all of our packages types, not just the ones we want to make public.
Unfortunately this is just how the TypeScript CLI works, but we can do some further processing of these files to get where we want to get to.
Step 3: Rollup Your Type Definitions
What we ideally want then is a single .d.ts
file that contains all of our exported types and nothing else.
Thankfully we can achieve this using third party tool, API Extractor.
API Extractor is capable of a lot of cool features, but for the purposes of this article, we’ll focus on it’s rollup feature.
First then, install API Extractor as a dev dependency.
npm install @microsoft/api-extractor --save-dev
Next, we need to create a config for API Extractor which we can do by running the command
npx api-extractor init
This will generate a new api-extractor.json
file in our root folder. By default this file contains a lot of comments to help document all the features, but for now, we can just replace it’s contents with the following
{ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", "projectFolder": ".", "mainEntryPointFilePath": "<projectFolder>/types/exports.d.ts", "compiler": { "tsconfigFilePath": "<projectFolder>/tsconfig.api.json" }, "apiReport": { "enabled": false }, "docModel": { "enabled": false }, "dtsRollup": { "enabled": true, "publicTrimmedFilePath": "<projectFolder>/types/my-package.d.ts" }
}
Essentially here we are telling API Extractor where to find our types entry point, which in our case is our consolidated exports.d.ts
file along with the path to our tsconfig
file.
We then disable the features we don’t need, but enable the dtsRollup
feature and give it a publicTrimmedFilePath
of where we want to export our rolled up types to.
With this in place, we can now update the script in our package.json
as follows
{ "script": { "build:api": "tsc -p tsconfig.api.json && api-extractor run --verbose", }
}
Now when you execute the script, you’ll find there is an extra my-package.d.ts
file added to our types
output folder
If we take a look inside this file, you should see something similar to the following
import { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
import { LitElement } from '@umbraco-cms/backoffice/external/lit';
import { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
import { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
import { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbElement } from '@umbraco-cms/backoffice/element-api';
import { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api'; export declare type DefaultManifestQuickActionKind = ManifestKind<ManifestQuickAction> & ManifestBase; export declare interface QuickActionApi extends UmbApi { manifest: ManifestQuickAction; execute(): Promise<void>;
} export declare interface QuickActionElement extends UmbControllerHostElement { manifest: ManifestQuickAction; api?: QuickActionApi;
} export declare interface ManifestQuickAction extends ManifestElementAndApi<QuickActionElement, QuickActionApi> { type: 'quickAction'; meta: MetaQuickAction;
} export declare interface MetaQuickAction { entityType: string; label: string; look?: 'primary' | 'secondary';
}
Step 4: Package Your Type Definitions
Now that we have all our public type in a single file, we can now move on to packaging things up.
First, update your package.json
adding the following types
, files
and peerDependencies
values.
{ "name": "my-package", "version": "14.0.0", "type": "module", "types": "./types/my-package.d.ts", "files": [ "types/my-package.d.ts", ], "peerDependencies": { "@umbraco-cms/backoffice": "^14.0.0", }
}
Next, add an additional script as follows
{ "script": { "pack:api": "npm pack", }
}
And finally, execute this script in your terminal
npm run pack:api
Now in the root of your project, you should see a my-package-14.0.0.tgz
file, which if we look into should contain the following files/structure.
package
├─ types
│ └─ my-package.d.ts
└─ package.json
And with that, we now have an NPM package we can publish using npm publish
.
Step 5: Define an Import Map
There is one final step we need to implement, and that is to define an import map for our package.
This is required in order that when anyone used your NPM package and imports a type like import { ManifestQuickAction } from 'my-package'
, when this runs in the browser, we need to tell it where my-package
actually resolves to.
To setup an import map, update your umbraco-package.json
and add the following
{ ... "importmap": { "imports": { "my-plugin": "/App_Plugins/MyPlugin/my-plugin.js" } }
}
And that should be all we need to implement.
Gotchas
There is one major gotcha I hit with using API Extractor and that is that it doesn’t support using custom TypeScript path shortcuts as I recently blogged about. Unfortunately API Extractor assumes all TypeScript path declarations refer to external libraries and so failures ensue.
Ultimately this resulted in me needing to strip out those shortcuts and update all paths back to being relative. That was a fun couple of hours.
Summary
I’ve tried to boil down the essence here of the steps that are required in order to publish your public API as NPM modules, but there is a lot more you can do with API Extractor as well as other useful scripts you might want to implement (I have some that dynamically updates the package version based on the build server output amongst other things).
But I hope this gives at least the important steps that you might want to implement to do just the same.
Discover more from Coursity
Subscribe to get the latest posts sent to your email.