Detour – Overview of developing a webpack plugin

We now want to return to the first error we encountered when bundling our app which was:

  • ERROR in Unexpected value SlimSliderDirective in /path/to/TNSStudio/app/modules/player/directives/slider.directive.d.ts declared by the module PlayerModule in /path/to/TNSStudio/app/modules/player/player.module.ts. Please add a @Pipe/@Directive/@Component annotation.

A solution for this error did not exist at the time of writing this book, so we created the nativescript-webpack-import-replace (https://github.com/NathanWalker/nativescript-webpack-import-replace) plugin to solve the problem.

Developing a webpack plugin in detail is out of the scope of this book, but we wanted to give you some highlights to the process in case you end up needing to create one to solve a particular case for your app.

We started by creating a separate project with a package.json file so we could install our webpack plugin like any other npm plugin:

{
"name": "nativescript-webpack-import-replace",
"version": "1.0.0",
"description": "Replace imports with .ios or .android suffix for target mobile platforms.",
"files": [
"index.js",
"lib"
],
"engines": {
"node": ">= 4.3 < 5.0.0 || >= 5.10"
},
"author": {
"name": "Nathan Walker",
"url": "http://github.com/NathanWalker"
},
"keywords": [
"webpack",
"nativescript",
"angular"
],
"nativescript": {
"platforms": {
"android": "3.0.0",
"ios": "3.0.0"
},
"plugin": {
"nan": "false",
"pan": "false",
"core3": "true",
"webpack": "true",
"category": "Developer"
}
},
"homepage": "https://github.com/NathanWalker/nativescript-webpack-import-replace",
"repository": "NathanWalker/nativescript-webpack-import-replace",
"license": "MIT"
}

The nativescript key actually helps categorize this plugin on the various NativeScript plugin listing sites. 

We then created lib/ImportReplacePlugin.js to represent the actual plugin class we would be able to import and use in our webpack config. We created this file inside a lib folder for good measure in case we need to add extra supporting files to aid our plugin for a nice clean separation of concerns with our plugin organization. In this file, we set up an export by defining a closure containing a constructor for our plugin:

exports.ImportReplacePlugin = (function () {
function ImportReplacePlugin(options) {
if (!options || !options.platform) {
throw new Error(`Target platform must be specified!`);
}

this.platform = options.platform;
this.files = options.files;
if (!this.files) {
throw new Error(`An array of files containing just the filenames to replace with platform specific names must be specified.`);
}
}

return ImportReplacePlugin;
})();

This will take the target platform defined in our webpack config and pass it through as options along with a files collection, which will contain all the filenames of the imports we need to replace.

We then want to wire into webpack's make lifecycle hook to grab hold of the source files being processed in order to parse them:

ImportReplacePlugin.prototype.apply = function (compiler) {
compiler.plugin("make", (compilation, callback) => {
const aotPlugin = getAotPlugin(compilation);
aotPlugin._program.getSourceFiles()
.forEach(sf => {
this.usePlatformUrl(sf)
});

callback();
})
};

function getAotPlugin(compilation) {
let maybeAotPlugin = compilation._ngToolsWebpackPluginInstance;
if (!maybeAotPlugin) {
throw new Error(`This plugin must be used with the AotPlugin!`);
}
return maybeAotPlugin;
}

This grabs hold of all the AoT source files. Then we setup a loop to process them one by one and add processing methods for what we need:

ImportReplacePlugin.prototype.usePlatformUrl = function (sourceFile) {
this.setCurrentDirectory(sourceFile);
forEachChild(sourceFile, node => this.replaceImport(node));
}

ImportReplacePlugin.prototype.setCurrentDirectory = function (sourceFile) {
this.currentDirectory = resolve(sourceFile.path, "..");
}

ImportReplacePlugin.prototype.replaceImport = function (node) {
if (node.moduleSpecifier) {
var sourceFile = this.getSourceFileOfNode(node);
const sourceFileText = sourceFile.text;
const result = this.checkMatch(sourceFileText);
if (result.index > -1) {
var platformSuffix = "." + this.platform;
var additionLength = platformSuffix.length;
var escapeAndEnding = 2; // usually "";" or "';"
var remainingStartIndex = result.index + (result.match.length - 1) + (platformSuffix.length - 1) - escapeAndEnding;

sourceFile.text =
sourceFileText.substring(0, result.index) +
result.match +
platformSuffix +
sourceFileText.substring(remainingStartIndex);

node.moduleSpecifier.end += additionLength;
}
}
}

ImportReplacePlugin.prototype.getSourceFileOfNode = function (node) {
while (node && node.kind !== SyntaxKind.SourceFile) {
node = node.parent;
}
return node;
}

ImportReplacePlugin.prototype.checkMatch = function (text) {
let match = '';
let index = -1;
this.files.forEach(name => {
const matchIndex = text.indexOf(name);
if (matchIndex > -1) {
match = name;
index = matchIndex;
}
});
return { match, index };
}

An interesting part to building webpack plugins (and arguably the most challenging) is working with Abstract Syntax Trees (ASTs) of your source code. A critical aspect of our plugin is getting the "source file" node from the AST as follows:

ImportReplacePlugin.prototype.getSourceFileOfNode = function (node) {
while (node && node.kind !== SyntaxKind.SourceFile) {
node = node.parent;
}
return node;
}

This effectively weeds out any other nodes that are not source files since that is all our plugin needs to deal with.

Lastly, we create an index.js file in the root to simply export the plugin file for use:

module.exports = require("./lib/ImportReplacePlugin").ImportReplacePlugin;

With the aid of this webpack plugin, we are able to completely solve all the webpack bundling errors we encountered in our app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.105.227