Tuesday, July 26, 2016

Using a JavaScript Library in TypeScript Modules - the D.TS FIle

Update - After restructuring the JavaScript code to separate views from components, as per the great article by Brad Westfall, I found that the local custom declaration file no longer satisfied the compiler. The reason is that the views and container components are placed in separate folders, which breaks the relative module path references. For the sake of expediency, I have resorted to moving the custom declaration file into the SPScript node_module folder. Not great, I know, but it works and so will do in the meantime.

If you are needing to hand-craft your own d.ts files, have a look at Complex Typescript Definitions Made Easy by Steve Fenton. You'll be glad you did!
The JavaScript library SPScript simplifies REST calls to SharePoint from the client by wrapping the AJAX requests in higher level abstractions. Seemed a useful library for my trials with TypeScript, React and D3 in presenting SharePoint data, so I added it to my source code in Visual Studio Code, and imported the file into my module App.ts.

Issue - Cannot Find Module SPScript

Its not that simple in Typescript. First issue to correct is the red squiggly that appeared under SPScript in the following statement:
import SPScript from "SPScript";
This error notification from the editor environment indicates that TypeScript cannot locate a module that matches that import statement. Looking through the module resolution notes in the online TypeScript handbook shows the locations and files which the compiler will check for the sought module (see the section titled "How TypeScript resolves modules" on that page for the details). SPScript is provided as a plain JavaScript library, without .TS or .D.TS files. So none of the files expected by the node name resolution would be found.

To correct this, it was necessary to add a file at one of those locations. Rather than make any adjustments to the SPScript node module (risky!), I added the declaration file SPScript.d.ts in the same folder as the App.ts file, and added an export statement to that new file to set this file as a module.

A note on Visual Studio Code - I have noticed that VS Code is sometimes slow to indicate that errors are corrected. In this case, the red squiggly did not disappear, even with the new file in place. If you close and reopen the file with the issue (in my case, App.ts) then the error was gone when the file reopened!

Issue - What to Include in the Declaration File?

The call I wanted to make using SPScript is as follows:
    const dao = new SPScript.RestDao(_spPageContextInfo.webAbsoluteUrl);
For TypeScript to recognize the types in those statements, the declaration file SPScript.d.ts needs to define the various classes and parameters. I tried various combinations of statements in the declaration file, eventually using this:
export module SPScript {
    export class RestDao {
        constructor(webUrl: string);
    }
}
This approach allowed TypeScript to resolve the types, and the compiler successfully created a JS file.

So, all ready to go? Nope - looking at the code in the compiled JS file, that statement had become
    var SPScript_1 = (typeof window !== "undefined" ? window['SPScript'] : typeof global !== "undefined" ? global['SPScript'] : null);
    //... other statements
    var dao1 = new SPScript_1.SPScript.RestDao(_spPageContextInfo.webAbsoluteUrl);
That would fail in the browser, as the inclusion of the library SPScript.JS in the HTML page gives access to SPScript.RestDao not to SPScript.SPScript.RestDao.

Plenty of investigation, trial and error lead to the use of this in the declaration file:
export class RestDao {
    constructor(webUrl: string);
}
The import statement was adjusted to
import { RestDao } from "./SPScript";
And the successful outcome of the compile process gives this in the JavaScript file:
    var SPScript_1 = (typeof window !== "undefined" ? window['SPScript'] : typeof global !== "undefined" ? global['SPScript'] : null);
    //... other statements
    var dao = new SPScript_1.RestDao(_spPageContextInfo.webAbsoluteUrl);

A note on the Global Name and Browserify-Shim

The ES5 JavaScript file is being created from the TypeScript modules with the help of Browserify within a Gulp task
gulp.task('package', ['includeLibs', 'compile'], () => {
    let bundler =  browserify({
            entries: config.rootJS,
            debug: true //This provides sourcemapping
        })
        .external(['./SPScript']); 

    bundler.bundle() //start buindling
        .on('error', console.error.bind(console))
        .pipe(exorcist(config.distOutputPath + '/' + config.bundleFile + '.map')) //Extract the sourcemap to a separate file
        .pipe(source(config.bundleFile)) // Pass desired output file name to vinyl-source-stream
        .pipe(gulp.dest(config.distOutputPath)); // Destination for the bundle
})
Notice line 6 - this specifies that the SPScript is not to be included in any way in the output JavaScript. In addition, the global SPScript name needs to be made available within the bundled output modules. This is achieved by including the browserify-shim module in the source code, and adding the following to the package.json file:
  "browserify": {
    "transform": [
      "browserify-shim"
    ]
  },
  "browserify-shim": {
    "./SPScript": "global:SPScript"
  }
Note that the exact text used in the import statement - in this case it was ./SPScript - must be passed to browserify-shim, so that the correct reference occurs within the compiled JavaScript.

No comments: