SPFX 2019 Libraries with PnP JS
How to create a solution with a library for SharePoint 2019 with some PnP SP inside.
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.
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
"bundles": {
"repository-library": {
"components": [
{
"entrypoint": "./lib/libraries/repository/RepositoryLibrary.js",
"manifest": "./src/libraries/repository/RepositoryLibrary.manifest.json"
}
]
}
RepositoryLibrary.manifest.json
{
"id": "27ce84c6-8b9a-470f-9468-adb991bbb2e9",
"alias": "RepositoryLibrary",
"componentType": "Library",
"version": "*",
"manifestVersion": 2
}
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
{
"name": "@custom/spfx-2019-lib",
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"package": "gulp clean && tsc -p tsconfig.json && webpack --config webpack.config.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@microsoft/sp-application-base": "1.4.1",
"@microsoft/sp-core-library": "1.4.1",
"@microsoft/sp-webpart-base": "1.4.1",
"@pnp/sp": "2.0.13"
},
"devDependencies": {
"@types/es6-promise": "0.0.33",
"@types/microsoft-ajax": "0.0.33",
"@types/sharepoint": "2016.1.2",
"@types/webpack-env": "1.14.1",
"del": "5.1.0",
"gulp": "^3.9.1",
"webpack": "4.42.0",
"webpack-cli": "^3.3.11",
"fork-ts-checker-webpack-plugin": "4.1.0",
"typescript": "3.6.4"
}
}
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)
const path = require('path');
const del = require('del');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const webpack = require('webpack');
var PACKAGE = require('./package.json');
var version = PACKAGE.version.replace(/\./g, '_');
...
module.exports = ['source-map'].map((devtool) => ({
mode: 'development',
entry: {
"spfx2019lib": './lib/index.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
modules: ['node_modules']
},
context: path.resolve(__dirname),
output: {
path: path.resolve(__dirname, 'dist'),
chunkFilename: '[id].[name]_[chunkhash].js',
filename: '[name].js',
library: '[name]_' + version,
libraryTarget: 'var',
umdNamedDefine: true,
devtoolModuleFilenameTemplate: 'webpack:///../[resource-path]',
devtoolFallbackModuleFilenameTemplate: 'webpack:///../[resource-path]?[hash]'
},
devtool,
optimization: {
runtimeChunk: false
},
performance: { hints: false },
externals: [
'@microsoft/decorators',
'@microsoft/sp-lodash-subset',
'@microsoft/sp-core-library',
'@microsoft/office-ui-fabric-react-bundle',
'@microsoft/sp-polyfills',
'@microsoft/sp-loader',
'@microsoft/sp-http',
'@microsoft/sp-page-context',
'@microsoft/sp-component-base',
'@microsoft/sp-extension-base',
'@microsoft/sp-application-base',
'@microsoft/sp-webpart-base',
'@microsoft/sp-dialog',
'@microsoft/sp-office-ui-fabric-core',
'@microsoft/sp-client-preview',
'@microsoft/sp-webpart-workbench'
],
module: {
...
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
}),
new ForkTsCheckerWebpackPlugin({
tslint: false
}),
new ClearCssModuleDefinitionsPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(true),
DEBUG: JSON.stringify(true)
})
]
}));
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
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/site-groups";
import { ISiteGroupInfo } from "../interfaces/models/ISiteGroupInfo";
import { IPnPService } from "../interfaces/IPnPService";
export class PnPService implements IPnPService {
constructor(absoluteWebUrl:string){
sp.setup({
sp: {
baseUrl: absoluteWebUrl
}
});
}
/**
* Get all site groups
*/
public async getAllSiteGroups(): Promise<ISiteGroupInfo[]> {
return await sp.web.siteGroups.get() as ISiteGroupInfo[];
}
}
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
export * from './interfaces';
export * 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
[17:02:31] Using gulpfile C:\daten\git\spfx-2019-solution\spfx-2019-lib\gulpfile.js
[17:02:31] Starting 'clean'...
[17:02:31] Finished 'clean' after 76 ms
Starting type checking service...
Hash: abe77e4dd0d7cd747042
Version: webpack 4.42.0
Child
Hash: abe77e4dd0d7cd747042
Time: 2930ms
Built at: 2021-05-07 5:02:42 PM
Asset Size Chunks Chunk Names
spfx2019lib.js 334 KiB spfx2019lib [emitted] [big] spfx2019lib
spfx2019lib.js.map 286 KiB spfx2019lib [emitted] [dev] spfx2019lib
Entrypoint spfx2019lib [big] = spfx2019lib.js spfx2019lib.js.map
[./lib/index.js] 62 bytes {spfx2019lib} [built]
[./lib/services/PnPService.js] 3.27 KiB {spfx2019lib} [built]
[./lib/services/index.js] 64 bytes {spfx2019lib} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {spfx2019lib} [built]
+ 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
"dependencies": {
"@custom/spfx-2019-lib": "file:../spfx-2019-lib",
...
},
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
import { ISiteGroupInfo, PnPService } from "@custom/spfx-2019-lib";
...
public async componentDidMount(): Promise<void> {
const service = new PnPService(this.props.absoluteWebUrl);
let groups = await service.getAllSiteGroups();
this.setState({ groups: groups });
}
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
const typeScriptConfig = require('@microsoft/gulp-core-build-typescript/lib/TypeScriptConfiguration');
typeScriptConfig.TypeScriptConfiguration.setTypescriptCompiler(require('typescript'));
const buildtypescript = require('@microsoft/gulp-core-build-typescript');
buildtypescript.tslint.enabled = false;
PnP js has also a problem with source map typings, we disable this in our build configuration.
function includeRuleForSourceMapLoader(rules) {
for (const rule of rules) {
if (rule.use && typeof rule.use === 'string' && rule.use.indexOf('source-map-loader') !== -1) {
rule.include = [path.resolve(__dirname, 'lib'), path.resolve(__dirname, 'node_modules', 'spfx-2019-lib')];
}
}
}
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
//we dont like to include all source maps
includeRuleForSourceMapLoader(generatedConfiguration.module.rules);
return generatedConfiguration;
}
});
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
"loaderConfig": {
"entryModuleId": "hello-world-web-part",
"internalModuleBaseUrls": [
"https://localhost:4321/"
],
"scriptResources": {
"hello-world-web-part": {
"type": "path",
"path": "dist/hello-world-web-part.js"
},
"@custom/spfx-2019-lib": {
"type": "path",
"path": "node_modules/@custom/spfx-2019-lib/dist/spfx2019lib.js",
"globalName": "spfx2019lib_1_0_0"
},
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.
Finally we get to run our smart webpart with PnP.
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:
- The code can shared between multiple webparts inside a solution
- You have full support of typings
- You can use other libraries inside your library (like PnP JS)
- You don’t have to care about versioning, it is still bundled inside your package
- You can share your library with other projects
Bad:
- You can not externalize React this way! Another story.
- Don’t externalize Office-Fabric-UI this way, similar to React.
- You cannot use other microsoft libraries inside your library (example @microsoft/sp-page-context)! The problem is, that the module loader can not load related sub libraries.