evont-software.com

Email: info@evont-software.com

SPFX 2019 Libraries with PnP JS

Category:SPFX
Date:
Author: Mathias Osterkamp

What we create

Ok. Here a small preview, what we like to create in this guide. A sample webpart loading your code from a library, here together with PnP JS to get all site groups.

webpart-sample.png


You find the complete source code for the solution here.

What are SPFX libraries?

If we come to the library topic most people start with the default description from microsoft. (source)

The library component type in the SharePoint Framework (SPFx) enables you to have independently versioned and deployed code served automatically for the SharePoint Framework components with a deployment through an app catalog. Library components provide you an alternative option to create shared code, which can be then used and referenced cross all the components in the tenant.

Yes i agree all these advantages - I like to start with SharePoint 2019 and use these. What we don't really learn is, nope - you won't get this! Ok after some deep dive i learned a lot and share you some about that. - And of course created a solution.

Library is not library

In SharePoint SPFX context the wording library is used a lot, but there are different use cases.

Code share

First is, we like to put our code into another solution and reuse it between multiple projects. On a code perspective this is relative easy, you can basically create a npm package and share this code by default packaging mechanism with npm. You can find a short description about that here. But now is our code included inside our main project, so yes it is reused but not shared. We like to build real libraries and also exclude code from our main SPFX files. So we have to continue.

Externals

To exclude code, we can use externals on our SPFX project. We can find some details from Microsoft here

Your client-side solution might include multiple web parts. These web parts might need to import or share the same library. In such cases, instead of bundling the library, you should include it in a separate JavaScript file to improve performance and caching. This is especially true of larger libraries.

This will help us to get our code outside. You will see two methods AMD and non-AMD modules. You find a good description about that on the same article, but it does not help you so much for our specific problem. How we tell sharepoint to build our library code as external?

Library component

The Library component brings a technical solution. You define a special manifest json with a library component type. Also you define in your config a special bundle with this type.

config.json

1 "bundles": {
2 "repository-library": {
3 "components": [
4 {
5 "entrypoint": "./lib/libraries/repository/RepositoryLibrary.js",
6 "manifest": "./src/libraries/repository/RepositoryLibrary.manifest.json"
7 }
8 ]
9 }


RepositoryLibrary.manifest.json


1{
2 "id": "27ce84c6-8b9a-470f-9468-adb991bbb2e9",
3 "alias": "RepositoryLibrary",
4 "componentType": "Library",
5 "version": "*",
6 "manifestVersion": 2
7}



Taken from sp-dev-fx-library-components.

Now the concept is, to deploy your component within a SPFX solution and load this inside your actual webpart.

If we like to use this library component inside SharePoint 2019 OnPremise we will see, yes it builds a solution and yes you could create a correct SPFX solution. But, it will not work and nothing happens correct. So i started to look for the reason and the short answer is, it is just supported from SPFX version 1.8.1 and not available for version 1.4.1.

You can build your solution, because your webpack compiler understands the type of bundle. Inside webpack is also a configuration called library, what creates a correct bundle. After deploy the solution to SharePoint 2019 AppCatalog it will fail. SharePoint cannot interpret this type correct and there is also missing inside SharePoint 2019 the background technology to load these libraries inside a site page. Technical SharePoint creates a list for all libraries inside the app catalog and loads these libraries if requested. Like it is working with other default Microsoft components.

Custom libraries

So after this fail with library components, i went back to focus on the external topic and decided to create a separate package loaded by the default externals mechanism. This is working also in older versions of SPFX. The challenge is to allow bundling and get a sweet typescript support.

How to create a custom library?

We start with separate npm project for our library.

package.json


1{
2 "name": "@custom/spfx-2019-lib",
3 "version": "1.0.0",
4 "description": "",
5 "main": "lib/index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1",
8 "package": "gulp clean && tsc -p tsconfig.json && webpack --config webpack.config.js"
9 },
10 "author": "",
11 "license": "ISC",
12 "dependencies": {
13 "@microsoft/sp-application-base": "1.4.1",
14 "@microsoft/sp-core-library": "1.4.1",
15 "@microsoft/sp-webpart-base": "1.4.1",
16 "@pnp/sp": "2.0.13"
17 },
18 "devDependencies": {
19 "@types/es6-promise": "0.0.33",
20 "@types/microsoft-ajax": "0.0.33",
21 "@types/sharepoint": "2016.1.2",
22 "@types/webpack-env": "1.14.1",
23 "del": "5.1.0",
24 "gulp": "^3.9.1",
25 "webpack": "4.42.0",
26 "webpack-cli": "^3.3.11",
27 "fork-ts-checker-webpack-plugin": "4.1.0",
28 "typescript": "3.6.4"
29 }
30}



We define a name @custom/spfx-2019-lib and add our dependencies. Furthermore we create a custom webpack to create a smart bundle.

webpack.config.json (not full version)


1const path = require('path');
2const del = require('del');
3const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
4const webpack = require('webpack');
5
6var PACKAGE = require('./package.json');
7var version = PACKAGE.version.replace(/\./g, '_');
8
9...
10
11module.exports = ['source-map'].map((devtool) => ({
12 mode: 'development',
13 entry: {
14 "spfx2019lib": './lib/index.js'
15 },
16 resolve: {
17 extensions: ['.ts', '.tsx', '.js'],
18 modules: ['node_modules']
19 },
20 context: path.resolve(__dirname),
21 output: {
22 path: path.resolve(__dirname, 'dist'),
23 chunkFilename: '[id].[name]_[chunkhash].js',
24 filename: '[name].js',
25 library: '[name]_' + version,
26 libraryTarget: 'var',
27 umdNamedDefine: true,
28 devtoolModuleFilenameTemplate: 'webpack:///../[resource-path]',
29 devtoolFallbackModuleFilenameTemplate: 'webpack:///../[resource-path]?[hash]'
30 },
31 devtool,
32 optimization: {
33 runtimeChunk: false
34 },
35 performance: { hints: false },
36 externals: [
37 '@microsoft/decorators',
38 '@microsoft/sp-lodash-subset',
39 '@microsoft/sp-core-library',
40 '@microsoft/office-ui-fabric-react-bundle',
41 '@microsoft/sp-polyfills',
42 '@microsoft/sp-loader',
43 '@microsoft/sp-http',
44 '@microsoft/sp-page-context',
45 '@microsoft/sp-component-base',
46 '@microsoft/sp-extension-base',
47 '@microsoft/sp-application-base',
48 '@microsoft/sp-webpart-base',
49 '@microsoft/sp-dialog',
50 '@microsoft/sp-office-ui-fabric-core',
51 '@microsoft/sp-client-preview',
52 '@microsoft/sp-webpart-workbench'
53 ],
54 module: {
55 ...
56 },
57 plugins: [
58 new webpack.optimize.LimitChunkCountPlugin({
59 maxChunks: 1
60 }),
61 new ForkTsCheckerWebpackPlugin({
62 tslint: false
63 }),
64 new ClearCssModuleDefinitionsPlugin(),
65 new webpack.DefinePlugin({
66 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
67 'process.env.DEBUG': JSON.stringify(true),
68 DEBUG: JSON.stringify(true)
69 })
70 ]
71}));



The most important thing is the filename:

"spfx2019lib": './lib/index.js'

And our library target, it is an old UMD module. So we package everything into a variable.

libraryTarget: 'var',
umdNamedDefine: true,

I tried to use newer AMD module definition, but again SharePoint 2019 cannot handle this correct. It was a problem with loading the modules inside application extensions, here it was breaking. For old UMD modules, everything works correct.

I implemented also a small service inside the solution, working with latest PnP version.

PnPService.ts

1import { sp } from "@pnp/sp";
2import "@pnp/sp/webs";
3import "@pnp/sp/site-groups";
4import { ISiteGroupInfo } from "../interfaces/models/ISiteGroupInfo";
5import { IPnPService } from "../interfaces/IPnPService";
6
7export class PnPService implements IPnPService {
8
9 constructor(absoluteWebUrl:string){
10 sp.setup({
11 sp: {
12 baseUrl: absoluteWebUrl
13 }
14 });
15 }
16 /**
17 * Get all site groups
18 */
19 public async getAllSiteGroups(): Promise<ISiteGroupInfo[]> {
20 return await sp.web.siteGroups.get() as ISiteGroupInfo[];
21 }
22}


One important thing is, to use own interfaces and also export everything on the index.ts. Your package also links to this file "main": "lib/index.js".

index.ts

1export * from './interfaces';
2export * from './services';


Now you will get a correct library

npm run package

> @custom/spfx-2019-lib@1.0.0 package C:\daten\git\spfx-2019-solution\spfx-2019-lib
> gulp clean && tsc -p tsconfig.json && webpack --config webpack.config.js

1[17:02:31] Using gulpfile C:\daten\git\spfx-2019-solution\spfx-2019-lib\gulpfile.js
2[17:02:31] Starting 'clean'...
3[17:02:31] Finished 'clean' after 76 ms
4Starting type checking service...
5Hash: abe77e4dd0d7cd747042
6Version: webpack 4.42.0
7Child
8 Hash: abe77e4dd0d7cd747042
9 Time: 2930ms
10 Built at: 2021-05-07 5:02:42 PM
11 Asset Size Chunks Chunk Names
12 spfx2019lib.js 334 KiB spfx2019lib [emitted] [big] spfx2019lib
13 spfx2019lib.js.map 286 KiB spfx2019lib [emitted] [dev] spfx2019lib
14 Entrypoint spfx2019lib [big] = spfx2019lib.js spfx2019lib.js.map
15 [./lib/index.js] 62 bytes {spfx2019lib} [built]
16 [./lib/services/PnPService.js] 3.27 KiB {spfx2019lib} [built]
17 [./lib/services/index.js] 64 bytes {spfx2019lib} [built]
18 [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {spfx2019lib} [built]
19 + 53 hidden modules


Include this into your project

General

Now we reuse or library inside our other project, we create a symlink by just adding this to your package configuration.

package.json

1 "dependencies": {
2 "@custom/spfx-2019-lib": "file:../spfx-2019-lib",
3 ...
4 },


Inside our config we declare to use this project as external. The global name is defined inside your library project. The bundle mechanism will exclude all code into a separate file.

"externals": {
"@custom/spfx-2019-lib": {
"path": "./node_modules/@custom/spfx-2019-lib/dist/spfx2019lib.js",
"globalName": "spfx2019lib_1_0_0"
}

Now we can use this library inside your webpart. It is important just use the package name "@custom/spfx-2019-lib" and NO subpath (lib/...). Only code with exact package name will excluded into separate file.

HelloWorld.tsx

1import { ISiteGroupInfo, PnPService } from "@custom/spfx-2019-lib";
2...
3public async componentDidMount(): Promise<void> {
4 const service = new PnPService(this.props.absoluteWebUrl);
5 let groups = await service.getAllSiteGroups();
6 this.setState({ groups: groups });
7}


PnP Specific

For our PnP stuff we have to tell our compiler to use latest typescript. This is done in our gulp file.

gulpfile.js

1const typeScriptConfig = require('@microsoft/gulp-core-build-typescript/lib/TypeScriptConfiguration');
2typeScriptConfig.TypeScriptConfiguration.setTypescriptCompiler(require('typescript'));
3
4const buildtypescript = require('@microsoft/gulp-core-build-typescript');
5buildtypescript.tslint.enabled = false;
6PnP js has also a problem with source map typings, we disable this in our build configuration.
7function includeRuleForSourceMapLoader(rules) {
8 for (const rule of rules) {
9 if (rule.use && typeof rule.use === 'string' && rule.use.indexOf('source-map-loader') !== -1) {
10 rule.include = [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'node_modules', 'spfx-2019-lib')];
11 }
12 }
13}
14build.configureWebpack.mergeConfig({
15 additionalConfiguration: (generatedConfiguration) => {
16 //we dont like to include all source maps
17 includeRuleForSourceMapLoader(generatedConfiguration.module.rules);
18 return generatedConfiguration;
19 }
20});


Run it

You can run it and build your spfx solution.

PS C:\daten\git\spfx-2019-solution\spfx-2019> npm run package-ship

> spfx-2019@0.0.1 package-ship C:\daten\git\spfx-2019-solution\spfx-2019
> gulp clean && gulp build && gulp bundle --ship && gulp package-solution --ship
...
[17:22:57] Finished subtask 'package-solution' after 1.09 s
[17:22:57] Finished 'package-solution' after 1.14 s
[17:22:57] ==================[ Finished ]==================
[17:22:58] Project spfx-2019 version: 0.0.1
[17:22:58] Build tools version: 3.2.7
[17:22:58] Node version: v10.22.0
[17:22:58] Total duration: 8.09 s

You can see our library inside the manifests.json (located in the temp folder).

manifests.json

1"loaderConfig": {
2 "entryModuleId": "hello-world-web-part",
3 "internalModuleBaseUrls": [
4 "https://localhost:4321/"
5 ],
6 "scriptResources": {
7 "hello-world-web-part": {
8 "type": "path",
9 "path": "dist/hello-world-web-part.js"
10 },
11 "@custom/spfx-2019-lib": {
12 "type": "path",
13 "path": "node_modules/@custom/spfx-2019-lib/dist/spfx2019lib.js",
14 "globalName": "spfx2019lib_1_0_0"
15 },


Now we deploy our solution and if we check our webpart we have excluded our code inside a single file @custom-spfx-2019-lib_f61c3320dcfac71474b84527be597369.js. It will also work with multiple webparts.

loaded-resources.png


Finally we get to run our smart webpart with PnP. 

webpart-sample.png


Conclusion

I hope this approach helps you to create better SPFX 2013 solutions and externalize your code in your own libraries. Here some small hints.

Good:

Bad:

SPFX 2019 Libraries with PnP JS | Evont Software GmbH | Evont Software GmbH