<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><atom:link href="https://www.devextent.com/feed.rss" rel="self" type="application/rss+xml"/><title>Dev Extent</title><link>https://www.devextent.com/</link><description>JavaScript and Web Development Articles</description><language>en-US</language><item><title>Convert Markdown to HTML with Node.js</title><pubDate>Thu, 05 Aug 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/convert-markdown-to-html-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/convert-markdown-to-html-nodejs/</guid><description><![CDATA[<p>Unlike the name implies, <a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a> is a markup language that can be used to create rich text output while authoring content in a plain text editor without formatting. Like <a href="https://en.wikipedia.org/wiki/HTML">HTML</a>, Markdown includes a base syntax, however there is no formal specification for Markdown, like there is for HTML. As a result there are many <a href="https://www.iana.org/assignments/markdown-variants/markdown-variants.xhtml">Markdown variants</a>, with each providing their own syntax variations and specifications.</p><p>While there are some differences among the flavors of Markdown, one rather nice aspect of authoring content with Markdown is that it can be readily converted to HTML using one of the many markdown processing technologies that are available. One way that can facilitate the creation of a website's HTML files, while still authoring content in Markdown, is to use Node.js to convert Markdown content into an HTML file. The resulting HTML output can then be uploaded to Jamstack website hosting, using static HTML files.</p><p>In this post we'll use Node.js and CLI commands to read a Markdown file, convert that file to an HTML string and then write the HTML string to a new file. When we have the file created, we can start a local development server to test the file in a web browser. Before following the steps make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>Setup Node.js CLI Project</h3><p>To start, setup the package.json file used with Node.js by running the command <kbd>npm init</kbd> in a terminal window open to your project folder. Then follow the prompts shown by the npm init process and a package.json file should have been created. With the package.json file in place we can run additional commands to install the npm packages that are used to convert Markdown to HTML.</p><h4>npm install</h4><p>In the same terminal window run the command <kbd>npm install markdown-it highlight.js fs-extra cross-env rimraf @babel/cli @babel/core @babel/preset-env @babel/preset-typescript --save</kbd>, followed by the command <kbd>npm install typescript @types/node @types/markdown-it @types/fs-extra --save-dev</kbd>.</p><p>After running both of these commands a new folder named "node_modules" should be present in your project folder. In the "node_modules" folder the following npm packages are installed:</p><ul><li><a href="https://www.npmjs.com/package/markdown-it">markdown-it</a></li><li><a href="https://www.npmjs.com/package/highlight.js">highlight.js</a></li></ul><h4>Add Support For ES Modules</h4><p>For this example these packages are also installed, mostly to support using <a href="https://www.typescriptlang.org/">TypeScript</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">ES modules</a> in Node.js, which is optional.</p><ul><li><a href="https://www.npmjs.com/package/fs-extra">fs-extra</a></li><li><a href="https://www.npmjs.com/package/typescript">typescript</a></li><li><a href="https://www.npmjs.com/package/cross-env">cross-env</a></li><li><a href="https://www.npmjs.com/package/rimraf">rimraf</a></li><li><a href="https://www.npmjs.com/package/@babel/cli">@babel/cli</a></li><li><a href="https://www.npmjs.com/package/@babel/core">@babel/core</a></li><li><a href="https://www.npmjs.com/package/@babel/preset-env">@babel/preset-env</a></li><li><a href="https://www.npmjs.com/package/@babel/preset-typescript">@babel/preset-typescript</a></li><li><a href="https://www.npmjs.com/package/@types/fs-extra">@types/fs-extra</a></li><li><a href="https://www.npmjs.com/package/@types/markdown-it">@types/markdown-it</a></li><li><a href="https://www.npmjs.com/package/@types/node">@type/node</a></li></ul><p>The remainder of these steps will include setting up the TypeScript and <a href="https://babeljs.io/">Babel</a> compilers to <a href="https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/">use ES Modules in Node.js</a> for the CLI script that will convert Markdown into HTML, and write the HTML string to a file.</p><p>To support ES modules there is one more configuration that must be included in the package.json file. This is the "type" property with the value set to "module" as indicated below.</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>
}
</code></pre><h4>package.json Scripts</h4><p>Additionally, we need to configure the "scripts" section of the package.json file to include the npm CLI scripts that will be used in the following steps. Since we are modifying the package.json file at this time go ahead and also add the following to the scripts property:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"typecheck"</span>: <span class="hljs-string">"tsc --p ."</span>,
    <span class="hljs-attr">"clean"</span>: <span class="hljs-string">"rimraf dist"</span>,
    <span class="hljs-attr">"compile"</span>: <span class="hljs-string">"cross-env-shell babel src -d dist --source-maps --extensions '.ts'"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run clean &amp;&amp; npm run compile &amp;&amp; node ./dist/index.js"</span>,
    <span class="hljs-attr">"start-typecheck"</span>: <span class="hljs-string">"npm run typecheck &amp;&amp; npm run start"</span>
  }
}
</code></pre><p>These scripts are responsible for invoking the TypeScript and Babel compilers, to carry out typechecking and the compilation of TypeScript into JavaScript. These use most of the optional packages that were installed for that process. In a later step we can run these package.json scripts as CLI commands to first compile TypeScript and then run the JavaScript output with Node.js to convert Markdown into HTML.</p><h4>package.json</h4><p>With all the required packages installed and ES modules configured, the package.json file in your project should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"convertmarkdowntohtml"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>,
    <span class="hljs-attr">"typecheck"</span>: <span class="hljs-string">"tsc --p ."</span>,
    <span class="hljs-attr">"clean"</span>: <span class="hljs-string">"rimraf dist"</span>,
    <span class="hljs-attr">"compile"</span>: <span class="hljs-string">"cross-env-shell babel src -d dist --source-maps --extensions '.ts'"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run clean &amp;&amp; npm run compile &amp;&amp; node ./dist/index.js"</span>,
    <span class="hljs-attr">"start-typecheck"</span>: <span class="hljs-string">"npm run typecheck &amp;&amp; npm run start"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@babel/cli"</span>: <span class="hljs-string">"^7.14.8"</span>,
    <span class="hljs-attr">"@babel/core"</span>: <span class="hljs-string">"^7.14.8"</span>,
    <span class="hljs-attr">"@babel/preset-env"</span>: <span class="hljs-string">"^7.14.9"</span>,
    <span class="hljs-attr">"@babel/preset-typescript"</span>: <span class="hljs-string">"^7.14.5"</span>,
    <span class="hljs-attr">"cross-env"</span>: <span class="hljs-string">"^7.0.3"</span>,
    <span class="hljs-attr">"fs-extra"</span>: <span class="hljs-string">"^10.0.0"</span>,
    <span class="hljs-attr">"highlight.js"</span>: <span class="hljs-string">"^11.2.0"</span>,
    <span class="hljs-attr">"markdown-it"</span>: <span class="hljs-string">"^12.2.0"</span>,
    <span class="hljs-attr">"rimraf"</span>: <span class="hljs-string">"^3.0.2"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/fs-extra"</span>: <span class="hljs-string">"^9.0.12"</span>,
    <span class="hljs-attr">"@types/markdown-it"</span>: <span class="hljs-string">"^12.0.3"</span>,
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^16.4.10"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.3.5"</span>
  }
}
</code></pre><p>If you are having trouble with the package install try copying the package.json from above and save that as your package.json file, then run the command <kbd>npm install</kbd> to install all of the listed packages.</p><h4>Configure TypeScript Compiler with tsconfig.json</h4><p>TypeScript is not required to convert Markdown to HTML, but it is not that much extra configuration to add when compared to the benefits of using TypeScript. Since the npm package for TypeScript was just installed we can add a new file to the project folder named "tsconfig.json" and this will contain the <a href="https://www.typescriptlang.org/docs/handbook/tsconfig-json.html">TypeScript compiler configuration settings</a> that are recommended when <a href="https://www.typescriptlang.org/docs/handbook/babel-with-typescript.html">using TypeScript and Babel</a> in the same project.</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"allowSyntheticDefaultImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2019"</span>],
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*.ts"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules/**/*"</span>, <span class="hljs-string">"dist/**/*"</span>]
}
</code></pre><p>The configuration will use TypeScript for type checking only, and the actual compilation of TypeScript into JavaScript will instead be carried out by the Babel compiler.</p><h4>Configure Babel Compiler with babel.config.json</h4><p>Just as the tsconfig.json file was added, we can add another file for the <a href="https://babeljs.io/docs/en/configuration">Babel configuration settings</a>. This file is named "babel.config.json" and contains the following:</p><pre><code class="hljs">{
  <span class="hljs-attr">"presets"</span>: [
    [
      <span class="hljs-string">"@babel/preset-env"</span>,
      { <span class="hljs-attr">"modules"</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">"targets"</span>: { <span class="hljs-attr">"node"</span>: <span class="hljs-string">"current"</span> } }
    ],
    [<span class="hljs-string">"@babel/preset-typescript"</span>]
  ],
  <span class="hljs-attr">"ignore"</span>: [<span class="hljs-string">"node_modules"</span>]
}
</code></pre><p>The Babel compiler does not type check TypeScript code and will attempt to output valid JavaScript regardless of the TypeScript source. This is why the TypeScript compiler is used for type checking, and the benefit of using both is that the Babel compiler has <a href="https://babeljs.io/docs/en/presets/">presets</a> available to ensure that the JavaScript generated will target a specific environment, in this case the current version of Node.js, and the "<a href="https://babeljs.io/docs/en/babel-preset-env#modules">modules</a>" property is set to false, which will preserve ES modules.</p><h3>Create Markdown File</h3><p>With our Node.js CLI project setup and package.json scripts already configured, the next part of the process to convert Markdown into HTML will be to create a sample Markdown file with a variety of content that includes the basic syntax shared among most Markdown flavors. To do this create a new folder for your project, named "content" and then inside the "content" folder create a new file named "index.md". When you have the index.md file created you can copy the sample Markdown content below into it.</p><pre><code class="hljs"><span class="hljs-section"># H1</span>

<span class="hljs-section">## H2</span>

<span class="hljs-section">### H3</span>

<span class="hljs-section">#### H4</span>

<span class="hljs-strong">**bold text**</span>

<span class="hljs-emphasis">_italicized text_</span>

<span class="hljs-quote">&gt; blockquote</span>

<span class="hljs-bullet">1.</span> First item
<span class="hljs-bullet">2.</span> Second item
<span class="hljs-bullet">3.</span> Third item

<span class="hljs-bullet">-</span> First item
<span class="hljs-bullet">-</span> Second item
<span class="hljs-bullet">-</span> Third item

<span class="hljs-code">`code`</span>

---

<span class="hljs-code">```javascript
function() {
  console.log("This is some javascript included in a markdown code block, and it will be converted to valid HTML with code syntax highlighting.");
}
```</span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">kbd</span>&gt;</span></span>this is a keyboard input html element<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">kbd</span>&gt;</span></span>

<span class="hljs-code">```html
&lt;span&gt;this will remain html even after the Markdown is converted to HTML&lt;/span&gt;
```</span>

[<span class="hljs-string">Dev Extent</span>](<span class="hljs-link">https://www.devextent.com</span>)

![<span class="hljs-string">Dev Extent</span>](<span class="hljs-link">https://www.devextent.com/images/devextent.png</span>)
</code></pre><h3>Create Node.js CLI Script</h3><p>Now that there is a Markdown file in the project we can add a new folder named "src" and in that folder add a new file named "index.ts". This is the Node.js script responsible for converting the Markdown file into an HTML file, and to start it looks like this:</p><pre><code class="hljs">(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">convertMarkdownToHtml</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Converting Markdown to HTML..."</span>);
})();
</code></pre><p>You can now run the command <kbd>npm run start-typecheck</kbd> or <kbd>npm run start</kbd> to compile without typechecking and you should see the console log is displayed. This means that the Node.js CLI project is working correctly, first compiling the TypeScript source code and then executing the generated JavaScript output with Node.js, all in one command.</p><h3>Read Markdown File</h3><p>After verifying that the Node.js CLI script is working correctly go ahead and add this code:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">convertMarkdownToHtml</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Converting Markdown to HTML..."</span>);

  <span class="hljs-comment">// markdown source</span>
  <span class="hljs-keyword">const</span> content = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"./content/index.md"</span>, <span class="hljs-string">"utf8"</span>);
})();
</code></pre><p>The additional code imports one node module, the fs-extra package, and it provides the "readFile" function to asynchronously read the "index.md" file in the content folder. The contents of the Markdown file are then assigned to the variable named "content". We now have a string of Markdown content that is ready to be converted into HTML, and to do that the markdown-it package will be used.</p><h3>Configure markdown-it Markdown Parser Options</h3><p>To configure the markdown parser included in the markdown-it package, create a new folder in the "src" folder named "utils" and then in the "utils" folder create a new TypeScript file named "markdown.ts". In the "markdown.ts" the markdown-it package will be imported and the markdown parser object will be constructed and exported.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> MarkdownIt <span class="hljs-keyword">from</span> <span class="hljs-string">"markdown-it"</span>;

<span class="hljs-keyword">const</span> markdown: MarkdownIt = MarkdownIt({
  <span class="hljs-attr">html</span>: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">export</span> { markdown };
</code></pre><p>There is one configuration option passed into the markdown parser configuration and that is to support HTML tags in the markdown source. This is optional, and not required but it can be helpful to support using HTML for elements that are lacking in Markdown syntax.</p><h4>Add Code Syntax Highlighting With highlight.js</h4><p>Besides optionally supporting HTML tags in the Markdown source, the markdown parser included with the markdown-it package can apply syntax highlighting to designated code blocks. Make the following adjustments to the markdown.ts file to include this option:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> hljs <span class="hljs-keyword">from</span> <span class="hljs-string">"highlight.js"</span>;
<span class="hljs-keyword">import</span> MarkdownIt <span class="hljs-keyword">from</span> <span class="hljs-string">"markdown-it"</span>;

<span class="hljs-keyword">const</span> markdown: MarkdownIt = MarkdownIt({
  <span class="hljs-attr">html</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">highlight</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">str, lang</span>) </span>{
    <span class="hljs-keyword">if</span> (lang &amp;&amp; hljs.getLanguage(lang)) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">return</span> (
          <span class="hljs-string">'&lt;pre&gt;&lt;code class="hljs"&gt;'</span> +
          hljs.highlight(str, { <span class="hljs-attr">language</span>: lang, <span class="hljs-attr">ignoreIllegals</span>: <span class="hljs-literal">true</span> }).value +
          <span class="hljs-string">"&lt;/code&gt;&lt;/pre&gt;"</span>
        );
      } <span class="hljs-keyword">catch</span> (__) {}
    }
    <span class="hljs-keyword">return</span> (
      <span class="hljs-string">'&lt;pre&gt;&lt;code class="hljs"&gt;'</span> +
      markdown.utils.escapeHtml(str) +
      <span class="hljs-string">"&lt;/code&gt;&lt;/pre&gt;"</span>
    );
  },
});

<span class="hljs-keyword">export</span> { markdown };
</code></pre><p>The highlight.js module is able to dynamically determine the language syntax highlighting based on the "lang" variable valuable that is passed into the "highlight" function the highlight.js module API provides.</p><p>Instead of an error when encountering inconsistent syntax, the "ignoreIllegals" parameter configures the highlight.js highlighter to finish highlighting. You may wish to leave this option out, but there is <a href="https://github.com/highlightjs/highlight.js/issues/3149">discussion whether the default value of the "ignoreIllegals" options should be changed to true</a>, as is used in this example.</p><p>If highlight.js cannot determined the language of the code block it will apply the "escapeHtml" function provided to the markdown string, and also wraps the code block section into a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code">code</a> element nested inside a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre">pre</a> element.</p><p>These additions will import the highlight.js module and apply the formatting required to dynamically highlight code blocks based on the language provided. The sample markdown file created in a previous step includes a block of JavaScript code that will have dynamic syntax highlighting applied when converted to HTML.</p><h4>Convert Markdown to HTML with markdown-it parser</h4><p>The "markdown.ts" file can now be imported in the "index.ts" file to access the Markdown parser with the previous configuration applied. To import the "markdown.ts" file and use the "render" function provided by the markdown-it module API, make these changes to the "index.ts" file:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;
<span class="hljs-keyword">import</span> { markdown } <span class="hljs-keyword">from</span> <span class="hljs-string">"./utils/markdown.js"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Converting Markdown to HTML..."</span>);

  <span class="hljs-comment">// markdown source</span>
  <span class="hljs-keyword">const</span> content = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"./content/index.md"</span>, <span class="hljs-string">"utf8"</span>);

  <span class="hljs-comment">// converted to HTML</span>
  <span class="hljs-keyword">const</span> rendered = <span class="hljs-keyword">await</span> markdown.render(content);
})();
</code></pre><p>The Markdown content, converted to HTML, is now assigned to the variable named "rendered". To view the rendered HTML you can output the "rendered" variable to the console and then run the command <kbd>npm run start-typecheck</kbd>, once more.</p><p>The contents of the "rendered" variable are valid HTML, but they do not represent an entire HTML document. To ensure that the Markdown source is converted into a complete and valid HTML document another variable is added, named "htmlFile", and this wraps the "rendered" variable string value in additional HTML code to create an entire HTML document. The "index.ts" should now look like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;
<span class="hljs-keyword">import</span> { markdown } <span class="hljs-keyword">from</span> <span class="hljs-string">"./utils/markdown.js"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Converting Markdown to HTML..."</span>);

  <span class="hljs-comment">// markdown source</span>
  <span class="hljs-keyword">const</span> content = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"./content/index.md"</span>, <span class="hljs-string">"utf8"</span>);

  <span class="hljs-comment">// converted to HTML</span>
  <span class="hljs-keyword">const</span> rendered = <span class="hljs-keyword">await</span> markdown.render(content);

  <span class="hljs-keyword">const</span> htmlFile = <span class="hljs-string">`&lt;!DOCTYPE html&gt;
  &lt;html lang="en"&gt;
  &lt;head&gt;
  &lt;meta charset="UTF-8" /&gt;
  &lt;title&gt;Convert Markdown to HTML with Node.js&lt;/title&gt;
  &lt;link rel="stylesheet" href="./default.css"&gt;
  &lt;/head&gt;
  &lt;body&gt;
  <span class="hljs-subst">${rendered}</span>
  &lt;/body&gt;
  &lt;/html&gt;`</span>;
})();
</code></pre><p><strong>Note</strong>: The "default.css" file referenced in the head of the HTML document will be copied in the following step from the default style sheet theme included with the highlight.js npm package.</p><h3>Write HTML File</h3><p>Instead of writing this file in the project folder root, the fs-extra module includes a "mkdirs" function that can programmatically create a folder. Using this function a new folder will be created named "public", and the generated HTML file saved there.</p><p>The highlight.js module provides many different style sheet themes to choose from when applying code block syntax highlighting. For this example the "default.css" theme is used, and that file is copied from the highlight.js module, inside the "node_modules" folder into the public folder that is programmatically created for the generated HTML. This way when the style sheet is reference in the "index.html" file, the "default.css" file is available in the same folder.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;
<span class="hljs-keyword">import</span> { markdown } <span class="hljs-keyword">from</span> <span class="hljs-string">"./utils/markdown.js"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Converting Markdown to HTML..."</span>);

  <span class="hljs-comment">// markdown source</span>
  <span class="hljs-keyword">const</span> content = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"./content/index.md"</span>, <span class="hljs-string">"utf8"</span>);

  <span class="hljs-comment">// converted to HTML</span>
  <span class="hljs-keyword">const</span> rendered = <span class="hljs-keyword">await</span> markdown.render(content);

  <span class="hljs-keyword">const</span> htmlFile = <span class="hljs-string">`&lt;!DOCTYPE html&gt;
  &lt;html lang="en"&gt;
  &lt;head&gt;
  &lt;meta charset="UTF-8" /&gt;
  &lt;title&gt;Convert Markdown to HTML with Node.js&lt;/title&gt;
  &lt;link rel="stylesheet" href="./default.css"&gt;
  &lt;/head&gt;
  &lt;body&gt;
  <span class="hljs-subst">${rendered}</span>
  &lt;/body&gt;
  &lt;/html&gt;`</span>;

  <span class="hljs-keyword">await</span> fs.mkdirs(<span class="hljs-string">"./public"</span>);

  <span class="hljs-keyword">await</span> fs.writeFile(<span class="hljs-string">"./public/index.html"</span>, htmlFile, <span class="hljs-string">"utf8"</span>);

  <span class="hljs-keyword">await</span> fs.copy(
    <span class="hljs-string">"./node_modules/highlight.js/styles/default.css"</span>,
    <span class="hljs-string">"./public/default.css"</span>,
    { <span class="hljs-attr">overwrite</span>: <span class="hljs-literal">true</span> }
  );

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"HTML generated."</span>);
})();
</code></pre><p>Run the command <kbd>npm run start-typecheck</kbd> once more and a new file "index.html" should be generated inside a new folder named "public" in your project folder, along with the "default.css" file that was copied from the "node_modules" folder.</p><p>You can now view the "index.html" file that will contain the Markdown source converted into HTML. The "index.html" file should look similar to this:</p><pre><code class="hljs"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Convert Markdown to HTML with Node.js<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./default.css"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>H1<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>H2<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>H3<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h4</span>&gt;</span>H4<span class="hljs-tag">&lt;/<span class="hljs-name">h4</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>bold text<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">em</span>&gt;</span>italicized text<span class="hljs-tag">&lt;/<span class="hljs-name">em</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">blockquote</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>blockquote<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">blockquote</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ol</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>First item<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>Second item<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>Third item<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ol</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>First item<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>Second item<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>Third item<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">code</span>&gt;</span>code<span class="hljs-tag">&lt;/<span class="hljs-name">code</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">hr</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">code</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-keyword"</span>&gt;</span>function<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>(<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-params"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>) {
  <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-variable language_"</span>&gt;</span>console<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>.<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-title function_"</span>&gt;</span>log<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>(<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-string"</span>&gt;</span><span class="hljs-symbol">&amp;quot;</span>This is some javascript included in a markdown code block, and it will be converted to valid HTML with code syntax highlighting.<span class="hljs-symbol">&amp;quot;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>);
}
<span class="hljs-tag">&lt;/<span class="hljs-name">code</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">kbd</span>&gt;</span>this is a keyboard input html element<span class="hljs-tag">&lt;/<span class="hljs-name">kbd</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">code</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-tag"</span>&gt;</span><span class="hljs-symbol">&amp;lt;</span><span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-name"</span>&gt;</span>span<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span><span class="hljs-symbol">&amp;gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>this will remain html even after the Markdown is converted to HTML<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-tag"</span>&gt;</span><span class="hljs-symbol">&amp;lt;</span>/<span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hljs-name"</span>&gt;</span>span<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span><span class="hljs-symbol">&amp;gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">code</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://www.devextent.com"</span>&gt;</span>Dev Extent<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
        <span class="hljs-attr">src</span>=<span class="hljs-string">"https://www.devextent.com/images/devextent.png"</span>
        <span class="hljs-attr">alt</span>=<span class="hljs-string">"Dev Extent"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>You can validate the generated HTML code with the <a href="https://validator.w3.org/#validate_by_input">W3C Markup Validation Service</a>, and you can also use the <a href="https://www.npmjs.com/package/http-server">http-server</a> npm package to create a local web server on your computer to view the "index.html" file in a browser.</p><h3>View HTML File Locally</h3><p>To test the Markdown converted into HTML, in a browser you can run the command <kbd>npm install http-server --save-dev</kbd> to install the http-server npm package. Then add the following to the package.json scripts property:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"serve"</span>: <span class="hljs-string">"http-server"</span>
  }
}
</code></pre><p>Then you can run the command <kbd>npm run serve</kbd> and the generated "index.html" file will be served from the public folder in your project. You should be able to navigate to "localhost:8080" and there you will see the content of the "index.html" with the styles from "default.css" applied to the syntax highlighted code block.</p>]]></description></item><item><title>Send a POST Request Containing a GraphQL Query with the Fetch API</title><pubDate>Tue, 01 Jun 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/send-post-request-graphql-query-fetch-api/</link><guid isPermaLink="true">https://www.devextent.com/send-post-request-graphql-query-fetch-api/</guid><description><![CDATA[<p><a href="https://github.com/graphql/graphql-spec">GraphQL</a> is a query language specification that is used for Web APIs to permit the use of API clients to create data queries. The queries can be specific to the client, and they are sent to a GraphQL server that is able to return exactly the data that was requested. A single GraphQL <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">POST</a> request can be used to obtain all of the data that is needed for the current context. This is in contrast to <a href="https://en.wikipedia.org/wiki/Representational_state_transfer">RESTful APIs</a>, that might result in a chain or waterfall of requests, with each request requiring data from the previous, in order to retrieve all of the data from the API server.</p><p>Typically a <a href="https://graphql.org/graphql-js/graphql-clients/">GraphQL client</a> is used to facilitate the client side query building, and to send HTTP POST requests containing GraphQL queries to the GraphQL server responsible for returning the data. It is not required to use a dedicated GraphQL client, as it is possible to send a GraphQL query as a POST request using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a>, and this is similar to the process used to <a href="https://www.devextent.com/fetch-api-post-formdata-object/">submit FormData using the Fetch API</a>. To show how to send a POST request containing a GraphQL query with the Fetch API, data from the GraphQL API: <a href="https://content.wpgraphql.com/graphql">https://content.wpgraphql.com/graphql</a> provided by <a href="https://www.wpgraphql.com/">WPGraphQL</a> can be used. After fetching the latest posts from the GraphQL API, by sending a POST request containing the GraphQL query, we can display the data as a list with each item title as a link.</p><h3>Create HTML File</h3><p>First, create an HTML file that will link to a JavaScript file containing the code that will send the GraphQL query as a POST request with the Fetch API. After sending the POST request containing the GraphQL query, the result of the query will be displayed as HTML, and before any data is received a no data message is displayed. In the project folder add a new file named "index.html" with the following content:</p><pre><code class="hljs"><span class="hljs-comment">&lt;!-- index.html --&gt;</span>
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Post a GraphQL Query with the Fetch API<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"data-container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>no data yet!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"data-button"</span>&gt;</span>Get Data<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/script.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><h3>Add JavaScript File</h3><p>In the "index.html" file there is a JavaScript file referenced that is named "script.js". We can create that file in the same folder as the index html file. After creating "script.js" in the project folder add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> dataFetchDisplay = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">{
  eventListenerSelector,
  eventType,
  dataFetcher,
  displayUpdater,
  dataTargetSelector,
}</span>) </span>{
  <span class="hljs-built_in">document</span>
    .querySelector(eventListenerSelector)
    .addEventListener(eventType, <span class="hljs-keyword">async</span> () =&gt; {
      displayUpdater(dataTargetSelector, <span class="hljs-keyword">await</span> dataFetcher());
    });
};
</code></pre><p>The "dataFetchDisplay" function has an options object as the parameter that contains the information needed to send the Fetch API POST request containing a GraphQL query, although we have yet to call this function or define the functions "displayUpdater" and "dataFetcher" that are included in the options parameter and used within the async callback of the event listener that is instantiated, when the "dataFetchDisplay" function is called. Here is how the "dataFetchDisplay" function will be used:</p><pre><code class="hljs">dataFetchDisplay({
  <span class="hljs-attr">eventListenerSelector</span>: <span class="hljs-string">"#data-button"</span>,
  <span class="hljs-attr">eventType</span>: <span class="hljs-string">"click"</span>,
  <span class="hljs-attr">dataFetcher</span>: getData,
  <span class="hljs-attr">displayUpdater</span>: updateDisplay,
  <span class="hljs-attr">dataTargetSelector</span>: <span class="hljs-string">"#data-container"</span>,
});
</code></pre><p>Notice that the "eventListenerSelector" and "dataTargetSelector" parameters correspond to the ID attributes that are present in the index.html file created in the first step. These values can be changed, but the values must match the HTML document ID attributes. Go ahead and add the invocation of the "dataFetchDisplay" function directly below the function definition previously added to script.js.</p><h3>Fetch API POST Request with GraphQL Query</h3><p>Now that we have the "dataFetchDisplay" function defined and being called, if we try to run this code it will result in an error because the helper functions to get the data and display it are not yet defined. Directly above the "dataFetchDisplay" function add the following code to define the "getData" function that is referenced in the "dataFetcher" options object parameter key value.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> getData = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="hljs-keyword">await</span> (
      <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://content.wpgraphql.com/graphql"</span>, {
        <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
        <span class="hljs-attr">headers</span>: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
          <span class="hljs-attr">query</span>: <span class="hljs-string">"{ posts { nodes { title, link } } }"</span>,
        }),
      })
    ).json()
  ).data.posts.nodes;
};
</code></pre><p>The getData function shown above is where the POST request, sent by the Fetch API, containing the GraphQL query is defined. For this example the GraphQL API is provided by <a href="https://www.wpgraphql.com/">WPGraphQL</a>, and the query will retrieve the link and title information for the ten most recent blog posts. Since we know the format of the data that is returned from the GraphQL query POST request sent with the Fetch API, we can return only the "nodes" in the "getData" function. That way when the "getData" function is used the data is already formatted as an array of post objects.</p><h3>Display GraphQL Query Data</h3><p>Now that we have the "getData" function defined and the GraphQL query data is returned after sending a POST request using the Fetch API, we need to display the data once it is returned from the GraphQL API server. To do this the function that is passed in as the "displayUpdater" parameter in the options object will be used. Add this code above the "dataFetchDisplay" function in the "script.js" file:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> updateDisplay = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">selector, data</span>) </span>{
  <span class="hljs-keyword">const</span> list = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"ul"</span>);

  data.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">item</span>) </span>{
    <span class="hljs-keyword">const</span> listItemLink = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"a"</span>);
    listItemLink.textContent = item.title;
    listItemLink.setAttribute(<span class="hljs-string">"href"</span>, item.link);

    <span class="hljs-keyword">const</span> listItem = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"li"</span>);
    listItem.appendChild(listItemLink);

    list.appendChild(listItem);
  });

  <span class="hljs-built_in">document</span>.querySelector(selector).replaceChildren(list);
};
</code></pre><p>The "updateDisplay" accepts two parameters: one to indicate the target element to insert the HTML that is generated and the second is the data array. In this example a link item is created for each data object using the title. The list of link elements is then used to replace the html of the target element.</p><p>By passing the "getData" and "displayUpdater" functions in as parameters to the "dataFetchDisplay" function, both the query and the way it should be displayed can be changed to suit the usage context. The "dataFetchDisplay" function is generic in that sense as the parameters determine what data to display, and how,based on the specific usage of the function.</p><p>Putting all of the code sections together should result in a script.js file that looks like this:</p><pre><code class="hljs"><span class="hljs-comment">//script.js</span>

<span class="hljs-keyword">const</span> getData = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="hljs-keyword">await</span> (
      <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://content.wpgraphql.com/graphql"</span>, {
        <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
        <span class="hljs-attr">headers</span>: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
          <span class="hljs-attr">query</span>: <span class="hljs-string">"{ posts { nodes { title, link } } }"</span>,
        }),
      })
    ).json()
  ).data.posts.nodes;
};

<span class="hljs-keyword">const</span> updateDisplay = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">selector, data</span>) </span>{
  <span class="hljs-keyword">const</span> list = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"ul"</span>);

  data.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">item</span>) </span>{
    <span class="hljs-keyword">const</span> listItemLink = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"a"</span>);
    listItemLink.textContent = item.title;
    listItemLink.setAttribute(<span class="hljs-string">"href"</span>, item.link);

    <span class="hljs-keyword">const</span> listItem = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"li"</span>);
    listItem.appendChild(listItemLink);

    list.appendChild(listItem);
  });

  <span class="hljs-built_in">document</span>.querySelector(selector).replaceChildren(list);
};

<span class="hljs-keyword">const</span> dataFetchDisplay = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">{
  eventListenerSelector,
  eventType,
  dataFetcher,
  displayUpdater,
  dataTargetSelector,
}</span>) </span>{
  <span class="hljs-built_in">document</span>
    .querySelector(eventListenerSelector)
    .addEventListener(eventType, <span class="hljs-keyword">async</span> () =&gt; {
      displayUpdater(dataTargetSelector, <span class="hljs-keyword">await</span> dataFetcher());
    });
};

dataFetchDisplay({
  <span class="hljs-attr">eventListenerSelector</span>: <span class="hljs-string">"#data-button"</span>,
  <span class="hljs-attr">eventType</span>: <span class="hljs-string">"click"</span>,
  <span class="hljs-attr">dataFetcher</span>: getData,
  <span class="hljs-attr">displayUpdater</span>: updateDisplay,
  <span class="hljs-attr">dataTargetSelector</span>: <span class="hljs-string">"#data-container"</span>,
});
</code></pre><h3>Test GraphQL Post Request Locally</h3><p>At this point we have the "index.html" and the "script.js" file setup so we can make sure that it is working by testing it locally. To do this we will need to install the <a href="https://www.npmjs.com/package/http-server">http-server npm package</a>. Before proceeding make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed as they are required.</p><h4>npm init package.json</h4><p>Once installed you can open the project folder in a terminal window and run the <kbd>npm init</kbd> command, and follow the prompts that are displayed. This will set up the package.json in the project folder.</p><h4>npm install http-server</h4><p>After configuring the package.json file run the command <kbd>npm install http-server --save-dev</kbd>. The http-server npm package is now installed as a development dependency.</p><h4>add npm script</h4><p>In the "scripts" object of the package.json file configuration add the following script:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"http-server"</span>
  }
}
</code></pre><p>The dev script can now be run and this will start the local development environment using the http-server npm package. You should now have a "node_modules" folder that was added to the project folder, and the package.json file should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"post-graphql-query-fetch-api"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"script.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"http-server"</span>,
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"http-server"</span>: <span class="hljs-string">"^0.12.3"</span>
  }
}
</code></pre><p>To start the local development environment with http-server run the command <kbd>npm run dev</kbd> and navigate to the url that is displayed in the console output. The development url will most likely be "http://localhost:8080", as this is the default setting for the local server configuration.</p><p>After running the <kbd>npm run dev</kbd> command and navigating "http://localhost:8080" you should see the "no data yet" message in your browser and the "get data" button we created earlier. To send the GraphQL query POST request with the Fetch API click the "get data" button, and the latest ten posts should will display on the page.</p><p>In some cases it might be beneficial to include a dedicated GraphQL client in your project, but in others using the Fetch API to send a POST request containing a GraphQL query without a GraphQL client can be sufficient. This can save time if the other more advanced features that come with GraphQL clients are not needed, especially if requests to the GraphQL server are infrequent.</p>]]></description></item><item><title>Deploy a Jamstack website to Azure Blob Storage with GitHub Actions</title><pubDate>Mon, 12 Apr 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/github-actions-jamstack-deployment-to-azure-blob-storage/</link><guid isPermaLink="true">https://www.devextent.com/github-actions-jamstack-deployment-to-azure-blob-storage/</guid><description><![CDATA[<p><a href="https://github.com/features/actions">GitHub Actions</a> are included with Github Repositories and can be used to automate project workflows like building and deploying code. In this example we will see how to automate the build process and deployment of a site built with the <a href="https://jamstack.org/">Jamstack</a>. We can use GitHub Actions to checkout a specific branch in a git repository, and then execute a build process that is common to Jamstack sites that are created with a static site generator like <a href="https://nextjs.org/">Next.js</a> or <a href="https://www.11ty.dev/">Eleventy</a>. On completion of the static site generator build process, the static site folder will then be uploaded to <a href="https://azure.microsoft.com/en-us/services/storage/blobs/">Azure Blob Storage</a>, utilizing the <a href="https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website-how-to?tabs=azure-portal">static website hosting</a> feature included with Azure Blob Storage.</p><h3>Azure Blob Storage Static Website Hosting</h3><p>In order to use the static website hosting feature included with Azure Blob Storage you need to <a href="https://azure.microsoft.com/en-us/free/">create an Azure account</a> if you do not already have one. Once you have created your account and logged in you will need to create the Storage Account resource that will provide the Blob Storage service. In the Azure portal select create a new resource and then search for "storage account". Then click create and follow the setup steps that are displayed to give the storage account a name and region. You can leave any pre-configured settings as the default setting.</p><p><img src="https://www.devextent.com/images/portal-storage-account-create.png" alt="Create Storage Account in Azure Portal"></p><p>Within the Azure portal navigate to your newly created storage account and in the left side navigation find the section labelled "Settings" and then within that section select the "Static website" feature.</p><p><img src="https://www.devextent.com/images/portal-storage-account-static-website.png" alt="Storage Account Static website settings"></p><h4>Configure Index Document Name and Error Document Path</h4><p>In the static website settings, enabling the static website feature will automatically generate the primary endpoint based on the storage account name. In the "Index document name" and "Error document path" fields enter the following:</p><p><img src="https://www.devextent.com/images/portal-storage-account-static-website-enabled.png" alt="Storage Account Static Website document name and path settings"></p><p>Based on how your static website is configured, or the static site generator that you are using, the "Error document path" might be different than what is shown for this example, however the "Index document name" will most likely remain as "index.html". Make sure to update these settings to correspond to the configuration that you are using. The storage account static website is now available and you can go to the primary endpoint and you will see this:</p><p><img src="https://www.devextent.com/images/portal-storage-static-website-not-found.png" alt="static website content does not exist"></p><p>This is good and it means that the static website is setup and publicly available. Now we can setup GitHub Actions to automated deployments anytime there is a commit pushed to the main branch in our GitHub repository.</p><h3>Create Remote GitHub Repository and Configure Local Repository Remote</h3><p>If you haven't already go ahead and <a href="https://docs.github.com/en/github/getting-started-with-github/create-a-repo">create a new repository for your project</a>, and after doing so follow the directions that are displayed to create a new repository from the command line on your local computer. This should result in a new repository that can push to the remote repository created on GitHub and any commits that we make will be applied to the main branch which is named "main". Make sure that you are able to push commits to GitHub and you can see them in your GitHub remote repository interface before proceeding.</p><p><strong>Note:</strong> The following steps that configure the GitHub Actions workflow require that the branch name is set to "main".</p><h4>Add .gitignore File</h4><p>Additionally you will want to add a ".gitignore" file to your project that minimally contains:</p><pre><code class="hljs">node_modules
_output
</code></pre><p>The "_output" folder is included because this is created by the static site generator build process that is configured in the next step. This folder will always contain generated files so it does not need to be tracked by git.</p><h3>Setup Static Site Generator</h3><p>If you already have a repository, that includes a Jamstack site setup with a static site generator, you can proceed without this step. In order to illustrate the <a href="https://www.netlify.com/blog/2019/09/27/git-centric-workflow-the-one-api-to-rule-them-all/">git-centric</a> deployment process that is promoted by GitHub Actions we can use, <a href="https://www.npmjs.com/package/@protolith/morphic">morphic</a> a static site generator built with Node.js and TypeScript. You don't need to use a static site generator in order to use GitHub Actions or the Static Website feature included with Azure Blob Storage, however it can be useful since GitHub Actions can execute commands that will invoke the static site generator build process and upon completion carry out the cloud deployment.</p><h4>Install Node.js and Configure package.json</h4><p>Before proceeding make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm installed</a>.</p><p>In your project folder run the command <kbd>npm init</kbd> and follow the prompts that are displayed to create a package.json file for your project. Then run the command <kbd>npm install @protolith/morphic</kbd>. This will install the morphic static site generator and its dependencies.</p><h4>Add Static Website Content as Markdown Files</h4><p>In the project folder where the package.json file was created you can then run this series of commands to create some site content.</p><pre><code class="hljs">mkdir content/pages
</code></pre><pre><code class="hljs">mkdir templates
</code></pre><pre><code class="hljs"><span class="hljs-built_in">cd</span> content/pages
</code></pre><pre><code class="hljs"><span class="hljs-built_in">echo</span> <span class="hljs-string">'---
title: Home
---
# &lt;%= model.title %&gt;
home page content

&lt;a href="/about/"&gt;Go to About Page&lt;/a&gt;'</span> &gt; index.md
</code></pre><pre><code class="hljs"><span class="hljs-built_in">echo</span> <span class="hljs-string">'---
title: Page Not Found
---

The page you are looking for may have been moved or deleted.

[Go to Homepage](/)'</span> &gt; 404.md
</code></pre><pre><code class="hljs"><span class="hljs-built_in">echo</span> <span class="hljs-string">'---
title: About
---
# &lt;%= model.title %&gt;
This is the about page.

&lt;a href="/"&gt;Go to Home Page&lt;/a&gt;'</span> &gt; about.md
</code></pre><pre><code class="hljs"><span class="hljs-built_in">cd</span> ../../templates
</code></pre><pre><code class="hljs"><span class="hljs-built_in">echo</span> <span class="hljs-string">'&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;&lt;%= model.title %&gt;&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;%- model.content %&gt;
  &lt;/body&gt;
&lt;/html&gt;'</span> &gt; index.ejs
</code></pre><pre><code class="hljs"><span class="hljs-built_in">cd</span> ..
</code></pre><p><strong>Note</strong>: make sure the files created (both ".md" and ".ejs" files) are encoded as UTF-8, or the static site generation process will not work. In Visual Studio Code you can change the file encoding by selecting the file and then in the bottom right hand corner toolbar the encoding is displayed. Click the encoding display in the bottom right toolbar to save with the UTF-8 encoding if needed.</p><h4>Build and Serve Static Site Generator Locally</h4><p>To make sure that morphic is setup correctly and generating the appropriate files you can run the command <kbd>npx morphic --serve</kbd>, and a browser window should open with the home page displaying a link to the about page. You can navigate back and forth from the home page and about pages. Also you can verify the 404 page was generated by going to the path "404.html" so the full url when running locally might be "http://localhost:3000/404.html", which corresponds to the error document path setting configured in the Azure Blob Storage Static Website settings.</p><h3>Github Actions Workflow To Deploy Static Site to Azure Blob Storage</h3><p>We now have our static site configured and ready to deploy with a GitHub Actions Workflow. To create a new GitHub Actions Workflow, that will build and deploy the static site from our GitHub repository, add a new folder within the current project folder named ".github". In the ".github" folder add another folder named "workflows" and within the "workflows" folder create a new workflow YAML file named "main.yml". The "main.yml" file is where we will create the GitHub Actions workflow that will build and deploy a Jamstack site to the Azure Blob Storage Static website. Here is the <a href="https://docs.github.com/en/actions/reference">complete reference documentation for creating workflows</a> provided by GitHub. We will only use a subset of the available features in our "main.yml" file to build and deploy on a git push command to the main branch in the repository we created. In the "main.yml" file add the following code:</p><pre><code class="hljs"><span class="hljs-attr">name:</span> <span class="hljs-string">MAIN</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> [<span class="hljs-string">main</span>]

<span class="hljs-attr">env:</span>
  <span class="hljs-attr">BUILD_COMMAND:</span> <span class="hljs-string">npx</span> <span class="hljs-string">morphic</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">fetch-depth:</span> <span class="hljs-number">0</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Dependencies</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">ci</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">${{</span> <span class="hljs-string">env.BUILD_COMMAND</span> <span class="hljs-string">}}</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Update</span> <span class="hljs-string">Blobs</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">azure/cli@v1.0.0</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">inlineScript:</span> <span class="hljs-string">|
            az storage blob sync -c '$web' -s _output --connection-string '${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}'
</span></code></pre><p>This workflow will be activated anytime there is a git push command for the main branch in the remote git repository. It will setup a GitHub Actions environment that is running on the latest version of the Ubuntu operating system, and then checkout the latest version of the code from the main branch. After checking out the main branch, the workflow invokes the <kbd>npm ci</kbd> command, which is similar to the <kbd>npm install</kbd> command, and is suitable for continuous integration environments. This is required to install the morphic static site generator npm package, similar to how we ran the static site generator locally in a previous step.</p><p>With the package.json dependencies installed the "Build" step in the workflow will run the "BUILD_COMMAND" specified as an environment variable. In our case, with the morphic static site generator, this command is the same one as we used before <kbd>npx morphic</kbd>, except this time the <kbd>--serve</kbd> flag is omitted since the files will be deployed to Azure Blob Storage Static Website hosting.</p><p>After running the Build command the workflow will have access to the site output folder: "_output" in our case, and this is what will be used in the <a href="https://docs.microsoft.com/en-us/cli/azure/what-is-azure-cli">Azure CLI</a> portion of the workflow with name "Update Blobs". This uses the <a href="https://github.com/marketplace/actions/azure-cli-action">Azure CLI Action</a> to permit the use of Azure CLI commands. In this case we want to use the <a href="https://docs.microsoft.com/en-us/cli/azure/storage/blob?view=azure-cli-latest#az_storage_blob_sync">az storage blob sync</a> command to ensure that "$web" blob container is synced to the the current build "_output" folder that was just created during the Build step of the workflow file.</p><p>In order to sync the files to the Azure Blob container named "$web", we need to pass in the "--connection-string" value that acts an access password for the storage account. Instead of creating a security risk by including the blob storage connection string directly in our publicly available workflow we can use <a href="https://docs.github.com/en/actions/reference/encrypted-secrets">GitHub Actions Repository Secrets</a> to store secret variables that only the automated workflow environment can access.</p><h3>Git Commit Project and Workflow Files</h3><p>The workflow configuration file is now included in the project, so we can commit the example static files we created and the workflow file using the commands <kbd>git add .</kbd> and then <kbd>git commit -m 'adding website files and workflow'</kbd>. However, before we push these changes to the remote GitHub repository we need to configure the "AZURE_STORAGE_CONNECTION_STRING" GitHub Actions secret value that is referenced in the workflow file.</p><h3>Github Actions Secret</h3><p>In the Azure portal, the storage account connection string can be copied by going to the Storage Account that was created previously. In the left sidebar find the settings section that contains the Access keys configuration.</p><p><img src="https://www.devextent.com/images/portal-storage-account-settings-connection-string.png" alt="Azure Storage connection string settings"></p><p>You can choose the connection string for key1 or key2, but make sure to use the connection string field value and not the Key field value.</p><p><img src="https://www.devextent.com/images/portal-storage-account-access-keys.png" alt="Azure storage connection strings"></p><p>Copy the connection string from the Azure portal settings and go to your GitHub repository interface, and in the top navigation there will be a section labelled "Settings". Clicking on "Settings" will bring you to a new page with a left side navigation that contains a section labelled "Secrets". This is where we will use the copied Azure Blob storage connection string to create a new GitHub Actions secret. On the Secrets page click the button to add a "New repository secret".</p><p><img src="https://www.devextent.com/images/github-actions-secret.png" alt="GitHub Actions add new repository secret"></p><p>Then add the secret name that matches the variable name from the "main.yml" workflow file, in this example it is AZURE_STORAGE_CONNECTION_STRING, and paste the Azure Storage connection string into the Value field.</p><p><img src="https://www.devextent.com/images/github-actions-secret-create.png" alt="GitHub Actions save new repository secret"></p><p>Then click "Add Secret" and our GitHub Actions Workflow can now access the Azure Storage connection string to sync files to Azure Blob Storage. Our GitHub Actions secret is now configured and ready for use.</p><h3>Git Push to Deploy Static Website with GitHub Actions to Azure Blob Storage Static Website</h3><p>Now you can run the command <kbd>git push -u origin main</kbd> from your local project and you should see the commit made previously has been pushed to the remote repository in the GitHub interface. In the top navigation of the GitHub repository, the "Actions" tab should now show the workflow automation is underway.</p><p><img src="https://www.devextent.com/images/github-actions-workflow-build.png" alt="GitHub Actions workflow build"></p><p>When the workflow build and deployment process completes, navigate to the primary endpoint for the Azure Blob Storage static website, that was displayed in the first step.</p><p><img src="https://www.devextent.com/images/github-actions-static-website-deployed.png" alt="Static site available at Azure Blob Storage static website primary endpoint"></p><p>You should see the home page now displays the content of our site and you can navigate to the About page that was created earlier. If you accidentally navigate to a page that does not exist, the error document path is set to 404.html, and Azure Blob Storage will return the 404.md page content that was added as a page within the static site.</p><p>Anytime you want to make changes to your site in the future, you can follow the same process as this example by first committing the changes to the main branch and then pushing the changes to the remote GitHub repository. This will trigger the GitHub Actions workflow and the changes will be automatically updated on the static website.</p>]]></description></item><item><title>Convert JSON to XML with the XML npm package</title><pubDate>Tue, 23 Mar 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/npm-convert-json-to-xml/</link><guid isPermaLink="true">https://www.devextent.com/npm-convert-json-to-xml/</guid><description><![CDATA[<p><a href="https://en.wikipedia.org/wiki/XML">XML</a> is a textual data format that is standardized, and as a result is widely used throughout a variety of systems. Two common usages are for website <a href="https://developers.google.com/search/docs/advanced/sitemaps/overview">sitemaps</a> and <a href="https://en.wikipedia.org/wiki/RSS">RSS</a> feeds, both of which can use XML as the document format. Other usages of XML can include <a href="https://en.wikipedia.org/wiki/Representational_state_transfer">RESTful HTTP API endpoints</a>, both receiving and returning XML requests and responses. This post will include the steps to convert JSON to XML with the <a href="https://www.npmjs.com/package/xml">XML npm package</a>. First we will read a <a href="https://en.wikipedia.org/wiki/JSON">JSON</a> file, convert the JSON object to an XML string, and then write the XML string to a file. Besides reading and writing files, the XML npm package can be used in other scenarios, where no files are involved as long as incoming data format is JSON and the desired data output format is an XML string.</p><h3>npm init package.json</h3><p>You will not need to to do this if you have an existing Node.js project setup but if you do not, then make sure to first install <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a>. Then in a terminal window open new folder for the project and run the command <kbd>npm init</kbd>, and follow the prompts that are displayed. The package.json file should have been added to the project folder.</p><p>We also need to make one addition to the package.json file after it has been generated so that <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">ES Modules</a> can be used with Node.js. To support ES Modules add a "type" property to the package.json file object with the value set to "module". In the following steps we will configure the TypeScript compiler to output JavaScript using the ES Module format.</p><h3>npm install</h3><p>With the package.json generated we can run additional commands to install the npm packages we will use. In the same project folder run the command <kbd>npm install xml typescript --save</kbd>, this will install the XML and TypeScript npm packages. After that run another command <kbd>npm install @types/xml --save-dev</kbd>. This will install the TypeScript type definitions for the XML npm package. Your package.json should look similar to this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"convertjsontoxml"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.2.3"</span>,
    <span class="hljs-attr">"xml"</span>: <span class="hljs-string">"^1.0.1"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/xml"</span>: <span class="hljs-string">"^1.0.5"</span>
  }
}
</code></pre><h3>Compile TypeScript</h3><p>Now that we have installed the XML and TypeScript npm packages installed, we can configure TypeScript to compile our code to use with Node.js, by adding an npm package.json script. To do this add the following the the "scripts" property in the package.json file that was created in the first step:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile"</span>: <span class="hljs-string">"tsc --allowSyntheticDefaultImports --isolatedModules --moduleResolution node --module esnext index.ts"</span>
  }
}
</code></pre><p>The compile command will invoke the TypeScript compiler with the CLI flags that will generate the JavaScript output using the ES Module format. This will match the "type" property set to "module" in the package.json configured earlier. You can run this command using <kbd>npm run compile</kbd> in the terminal window.</p><h3>Create Node.js Script</h3><p>Next we can create a Node.js script, and as referenced in the package.json scripts "compile" command, the name of this file is index.ts. Here we will write the TypeScript code that will use the XML npm package to generate an XML string from a JSON object. In the index.ts file add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">import</span> { readFile, writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;

<span class="hljs-keyword">const</span> readFilePromise = promisify(readFile);
<span class="hljs-keyword">const</span> writeFilePromise = promisify(writeFile);

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">convertJsonToXml</span>(<span class="hljs-params"></span>) </span>{})();
</code></pre><p>This will set up the import statements for the XML npm package and also import the readFile and writeFile functions from the node <a href="https://nodejs.org/api/fs.html">fs module</a>. Since these functions use callbacks by default, the promisify function is imported from the <a href="https://nodejs.org/api/util.html">util module</a> to convert the readFile and writeFile functions into promises. This way we can use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function">async/await</a> syntax.</p><h3>Read JSON File</h3><p>In the ConvertJsonToXml function the first thing we can do is read the JSON file containing a sample JSON object that we can convert to an XML string. Create a new file named "data.json" in the same project folder, and add this sample JSON object:</p><pre><code class="hljs">[
  {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Next.js"</span>,
    <span class="hljs-attr">"language"</span>: <span class="hljs-string">"JavaScript"</span>,
    <span class="hljs-attr">"templates"</span>: <span class="hljs-string">"React"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A framework for statically-exported React apps (supports server side rendering)"</span>
  },
  {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Gatsby"</span>,
    <span class="hljs-attr">"language"</span>: <span class="hljs-string">"JavaScript"</span>,
    <span class="hljs-attr">"templates"</span>: <span class="hljs-string">"React"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Build blazing fast, modern apps and websites with React"</span>
  },
  {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Nuxt"</span>,
    <span class="hljs-attr">"language"</span>: <span class="hljs-string">"JavaScript"</span>,
    <span class="hljs-attr">"templates"</span>: <span class="hljs-string">"Vue"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A minimalistic framework for serverless Vue.js applications."</span>
  }
]
</code></pre><p>In the index.js file, inside of the ConvertJsonToXml function we can add this code to read the JSON file and parse it into a JSON object with the corresponding type signature:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> staticSiteGeneratorData = <span class="hljs-built_in">JSON</span>.parse(
  <span class="hljs-keyword">await</span> readFilePromise(<span class="hljs-string">"data.json"</span>, <span class="hljs-string">"utf8"</span>)
) <span class="hljs-keyword">as</span> [
  {
    <span class="hljs-attr">name</span>: <span class="hljs-built_in">string</span>;
    language: <span class="hljs-built_in">string</span>;
    templates: <span class="hljs-built_in">string</span>;
    description: <span class="hljs-built_in">string</span>;
  }
];
</code></pre><p>Once the json file is read and saved as the "staticSiteGeneratorData" variable we can use the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map">Array.prototype.map(</a>) method to shape the data into the format we need, in order to use the XML npm package to convert the JSON object into an XML string. Below the code that is reading the data.json file add this code:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> xmlFormattedStaticSiteGeneratorData = [
  {
    <span class="hljs-attr">staticSiteGenerators</span>: [
      ...staticSiteGeneratorData.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> {
          <span class="hljs-attr">staticSiteGenerator</span>: [
            {
              <span class="hljs-attr">_attr</span>: {
                <span class="hljs-attr">language</span>: item.language,
                <span class="hljs-attr">templates</span>: item.templates,
                <span class="hljs-attr">description</span>: item.description,
              },
            },
            item.name,
          ],
        };
      }),
    ],
  },
];
</code></pre><p>The result of the data that is assigned to the "xmlFormattedStaticSiteGeneratorData" variable will look like this:</p><pre><code class="hljs">[
  {
    <span class="hljs-attr">"staticSiteGenerators"</span>: [
      {
        <span class="hljs-attr">"staticSiteGenerator"</span>: [
          {
            <span class="hljs-attr">"_attr"</span>: {
              <span class="hljs-attr">"language"</span>: <span class="hljs-string">"JavaScript"</span>,
              <span class="hljs-attr">"templates"</span>: <span class="hljs-string">"React"</span>,
              <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A framework for statically-exported React apps (supports server side rendering)"</span>
            }
          },
          <span class="hljs-string">"Next.js"</span>
        ]
      },
      {
        <span class="hljs-attr">"staticSiteGenerator"</span>: [
          {
            <span class="hljs-attr">"_attr"</span>: {
              <span class="hljs-attr">"language"</span>: <span class="hljs-string">"JavaScript"</span>,
              <span class="hljs-attr">"templates"</span>: <span class="hljs-string">"React"</span>,
              <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Build blazing fast, modern apps and websites with React"</span>
            }
          },
          <span class="hljs-string">"Gatsby"</span>
        ]
      },
      {
        <span class="hljs-attr">"staticSiteGenerator"</span>: [
          {
            <span class="hljs-attr">"_attr"</span>: {
              <span class="hljs-attr">"language"</span>: <span class="hljs-string">"JavaScript"</span>,
              <span class="hljs-attr">"templates"</span>: <span class="hljs-string">"Vue"</span>,
              <span class="hljs-attr">"description"</span>: <span class="hljs-string">"A minimalistic framework for serverless Vue.js applications."</span>
            }
          },
          <span class="hljs-string">"Nuxt"</span>
        ]
      }
    ]
  }
]
</code></pre><h3>Convert JSON File to an XML String</h3><p>The JSON data assigned to the "xmlFormattedStaticSiteGeneratorData" variable, is now in the appropriate format to use with the XML npm package. Directly below the code that formats the data, and inside the "convertJsonToXml" function, add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> staticSiteGeneratorXmlString = xml(xmlFormattedStaticSiteGeneratorData);
</code></pre><p>The format of the xml string assigned to the "staticSiteGeneratorXmlString" is going to look like this:</p><pre><code class="hljs"><span class="hljs-tag">&lt;<span class="hljs-name">staticSiteGenerators</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">staticSiteGenerator</span> <span class="hljs-attr">language</span>=<span class="hljs-string">"JavaScript"</span> <span class="hljs-attr">templates</span>=<span class="hljs-string">"React"</span> <span class="hljs-attr">description</span>=<span class="hljs-string">"A framework for statically-exported React apps (supports server side rendering)"</span>&gt;</span>Next.js<span class="hljs-tag">&lt;/<span class="hljs-name">staticSiteGenerator</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">staticSiteGenerator</span> <span class="hljs-attr">language</span>=<span class="hljs-string">"JavaScript"</span> <span class="hljs-attr">templates</span>=<span class="hljs-string">"React"</span> <span class="hljs-attr">description</span>=<span class="hljs-string">"Build blazing fast, modern apps and websites with React"</span>&gt;</span>Gatsby<span class="hljs-tag">&lt;/<span class="hljs-name">staticSiteGenerator</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">staticSiteGenerator</span> <span class="hljs-attr">language</span>=<span class="hljs-string">"JavaScript"</span> <span class="hljs-attr">templates</span>=<span class="hljs-string">"Vue"</span> <span class="hljs-attr">description</span>=<span class="hljs-string">"A minimalistic framework for serverless Vue.js applications."</span>&gt;</span>Nuxt<span class="hljs-tag">&lt;/<span class="hljs-name">staticSiteGenerator</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">staticSiteGenerators</span>&gt;</span>
</code></pre><h3>Write XML File</h3><p>The XML string assigned to the variable "staticSiteGeneratorDataXmlString" can be written to an XML file with the writeFile module that we imported and promisified at the beginning of the index.ts file. To write the XML string to a file in the same project folder add this code below the XML npm package usage that was included in the prior step:</p><pre><code class="hljs"><span class="hljs-keyword">await</span> writeFilePromise(<span class="hljs-string">"data.xml"</span>, staticSiteGeneratorXmlString, <span class="hljs-string">"utf8"</span>);
</code></pre><p>Put all the code together and the index.ts file should look like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">import</span> { readFile, writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;

<span class="hljs-keyword">const</span> readFilePromise = promisify(readFile);
<span class="hljs-keyword">const</span> writeFilePromise = promisify(writeFile);

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">convertJsonToXml</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> staticSiteGeneratorData = <span class="hljs-built_in">JSON</span>.parse(
    <span class="hljs-keyword">await</span> readFilePromise(<span class="hljs-string">"data.json"</span>, <span class="hljs-string">"utf8"</span>)
  ) <span class="hljs-keyword">as</span> [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-built_in">string</span>;
      language: <span class="hljs-built_in">string</span>;
      templates: <span class="hljs-built_in">string</span>;
      description: <span class="hljs-built_in">string</span>;
    }
  ];

  <span class="hljs-keyword">const</span> xmlFormattedStaticSiteGeneratorData = [
    {
      <span class="hljs-attr">staticSiteGenerators</span>: [
        ...staticSiteGeneratorData.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
          <span class="hljs-keyword">return</span> {
            <span class="hljs-attr">staticSiteGenerator</span>: [
              {
                <span class="hljs-attr">_attr</span>: {
                  <span class="hljs-attr">language</span>: item.language,
                  <span class="hljs-attr">templates</span>: item.templates,
                  <span class="hljs-attr">description</span>: item.description,
                },
              },
              item.name,
            ],
          };
        }),
      ],
    },
  ];

  <span class="hljs-keyword">const</span> staticSiteGeneratorXmlString = xml(xmlFormattedStaticSiteGeneratorData);

  <span class="hljs-keyword">await</span> writeFilePromise(<span class="hljs-string">"data.xml"</span>, staticSiteGeneratorXmlString, <span class="hljs-string">"utf8"</span>);
})();
</code></pre><h3>Run Node.js Script with npm package.json Scripts</h3><p>To test this code out and run the Node.js script we can add another package.json script that will first compile the TypeScript into JavaScript and then run the JavaScript output with Node.js. In the package.json file add a new package.json script named "start" that looks like this:</p><pre><code class="hljs">{ <span class="hljs-attr">"scripts"</span>: { <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run compile &amp;&amp; node index.js"</span> } }
</code></pre><p>To use the start script run the command <kbd>npm run start</kbd> and you should then see the XML file generated and saved to the project folder. The contents of this file should match the format of the XML string shown previously. Anytime you want to change the data or formatting make sure to run the <kbd>npm run start</kbd> again to regenerate the data.xml file.</p><p>The XML npm package is a convenient way to convert JSON into XML, as long as the JSON data is formatted appropriately, or there is a step involved to properly format the original JSON data source into the format the XML npm package requires. For other usages of the XML npm packages you can read my other posts showing how to <a href="https://www.devextent.com/generate-xml-sitemap-nodejs/">generate an XML sitemap</a> and <a href="https://www.devextent.com/xml-rss-feed-nodejs/">generate an XML RSS feed</a>, like this example, both of these posts are using Node.js and npm.</p>]]></description></item><item><title>Create a Service Worker with TypeScript</title><pubDate>Fri, 19 Mar 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/create-service-worker-typescript/</link><guid isPermaLink="true">https://www.devextent.com/create-service-worker-typescript/</guid><description><![CDATA[<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">Service Worker API</a> is available to use in all major browsers. Service Workers are JavaScript files that contain event driven worker code, that do not run on the main thread of the browser. They are used as a proxy for network requests, more specifically, service workers can be used to intercept requests and modify them as well as cache responses. In addition to caching responses a service worker can be used to enhance the user experience of your website by displaying an offline page when there is no network connection available.</p><p>In this example, <a href="https://www.typescriptlang.org/">TypeScript</a> is used to create a service worker with a network first then cache, caching strategy, to support viewing previously visited pages when offline. When there is no network connection available and a previous page version is not cached, the service worker will display an offline page.</p><h3>Configure TypeScript and Babel with ES Modules</h3><p>Before adding the TypeScript code for the service worker there are some prerequisite steps to configure the TypeScript compiler. If you want to include Typescript into your project you can view these posts:</p><ul><li><a href="https://www.devextent.com/npm-compile-typescript/">Setup TypeScript Compilation with npm package.json Scripts</a></li><li><a href="https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/">Import and Export ES Modules in Node.js using TypeScript with Babel Compilation</a></li></ul><p>to find more information about using TypeScript. The configuration will include <a href="https://babeljs.io/">Babel</a> for TypeScript compilation as shown in the second post above with some minor adjustments, to support browser use rather than Node.js, made to the Babel configuration.</p><h4>Configure package.json Scripts</h4><p>Create a package.json file by running <kbd>npm init</kbd> and then run the command <kbd>npm install typescript cross-env @babel/cli @babel/core @babel/preset-env @babel/preset-env @babel/preset-typescript --save</kbd>. You then need to add three package.json scripts that look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"typecheck"</span>: <span class="hljs-string">"tsc --p ."</span>,
    <span class="hljs-attr">"compile"</span>: <span class="hljs-string">"cross-env-shell babel $INIT_CWD -d $INIT_CWD --extensions '.ts' --no-comments --source-maps"</span>,
    <span class="hljs-attr">"typecheck-compile"</span>: <span class="hljs-string">"npm run typecheck &amp;&amp; npm run compile"</span>
  }
}
</code></pre><h4>Configure TypeScript for Type Checking Only</h4><p>The package.json "typecheck" script will invoke the TypeScript compiler and only is responsible for type checking the TypeScript code, and it will not emit any JavaScript. The TypeScript compiler configuration options are specified in a file named "tsconfig.json". You can create this file with the following settings:</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"allowSyntheticDefaultImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"es2019"</span>, <span class="hljs-string">"es6"</span>, <span class="hljs-string">"dom"</span>, <span class="hljs-string">"webworker"</span>],
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"*.ts"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules/**/*"</span>]
}
</code></pre><p>Notice that the "lib" property contains a value for "webworker" among the values. This is important because it indicates to the TypeScript compiler that it should load the <a href="https://github.com/microsoft/TypeScript/blob/master/lib/lib.webworker.d.ts">type definitions for the Worker APIs</a>.</p><p>The package.json "compile" script uses Babel to compile TypeScript and will be responsible for outputting JavaScript. The Babel compiler does not type check the TypeScript source code, so if there are any errors present the Babel compiler may attempt to provide output rather than indicating there was an error. Also the Babel compiler CLI command requires a source folder and output folder to be specified so this is set to the project folder with the "$INIT_CWD" npm CLI variable, for both the source and output folders. This way the JavaScript files will be written to the same folder as the TypeScript files.</p><h4>Configure Babel with @babel/preset-typescript and @babel/preset-env</h4><p>We are going to target browsers that are using ES Modules with the <a href="https://babeljs.io/docs/en/babel-preset-env">preset-env preset</a>, in addition to using the <a href="https://babeljs.io/docs/en/babel-preset-typescript">preset-typescript preset</a>. You can configure this by adding a <a href="https://babeljs.io/docs/en/configuration#babelconfigjson">babel.config.json</a> file in the same folder as the package.json with the following settings:</p><pre><code class="hljs">{
  <span class="hljs-attr">"presets"</span>: [
    [
      <span class="hljs-string">"@babel/preset-env"</span>,
      {
        <span class="hljs-attr">"modules"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"targets"</span>: { <span class="hljs-attr">"esmodules"</span>: <span class="hljs-literal">true</span> }
      }
    ],
    [<span class="hljs-string">"@babel/preset-typescript"</span>]
  ],
  <span class="hljs-attr">"ignore"</span>: [<span class="hljs-string">"node_modules"</span>],
  <span class="hljs-attr">"comments"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"minified"</span>: <span class="hljs-literal">true</span>
}
</code></pre><p>The presets are applied in the reverse order that they are listed in the Babel.config.json, so the preset-env changes will be applied to the JavaScript output created by preset-typescript.</p><h4>TypeScript Type Checking with Babel Compilation</h4><p>To combine both the typecheck and compile scripts you can run the package.json typecheck-compile command: <kbd>npm run typecheck-compile</kbd>. This will first use the TypeScript compiler to typecheck the code and then use the Babel compiler to generate JavaScript for the browser. If there are any type checking errors the command will fail and they will be shown in the console.</p><h3>Register Service Worker</h3><p>Now that we have TypeScript and Babel configured to use <a href="https://nodejs.org/api/esm.html">ES Modules</a>, but before creating the actual service worker code, we need to write some code that will register the service worker in the browser. This code can be included in the HTML source directly, or in a script tag. If you are including the code with a script tag make sure to reference the JavaScript output file that is generated as a result of the TypeScript compilation process and not the TypeScript source file. We can name this file "script.ts" and place it in the root of the project.</p><pre><code class="hljs"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Main</span> </span>{
  <span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params"></span>)</span> {
    <span class="hljs-keyword">if</span> (<span class="hljs-string">"serviceWorker"</span> <span class="hljs-keyword">in</span> navigator) {
      navigator.serviceWorker
        .register(<span class="hljs-string">"/service-worker.js"</span>, { <span class="hljs-attr">scope</span>: <span class="hljs-string">"/"</span> })
        .then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Service Worker Registered"</span>);
        });
    }
  }
}

<span class="hljs-keyword">new</span> Main();
</code></pre><h3>HTML include script type="module"</h3><p>You can include this script in an HTML file by creating a new file named "index.html", and save it in the root of the project, with this content:</p><pre><code class="hljs"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Service Worker Example<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    This page is loading a service worker!
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/script.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>When the browser loads the HTML page the JavaScript above will attempt to load the service worker. If the browser does not support service workers the code will be skipped.</p><p>Besides the index.html file we are also going to need an HTML file for an offline page. In the same folder that the index.html file is saved create a new file name "offline.html" and add this content to show an offline state:</p><pre><code class="hljs"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>You are not connected to the internet<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    You are not connected to the internet!
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/script.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>When the service worker code is added in the following step the offline page will be automatically cached for future usage.</p><h3>Set up Service Worker</h3><p>Registering the service worker is what invokes the service worker code that is not running on the main thread. Inside of the service worker we will write code to listen for certain types of events that are triggered by the browser. These events are:</p><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/install_event">install</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/activate_event">activate</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/onfetch">fetch</a></li></ul><p>We can add the install event first by adding the following code to a file named "service-worker.ts". You can use a different name if you want, but make sure that it matches what is included in the "navigator.serviceWorker.register" function used in the previous step. In the service-worker.js file add the following code:</p><pre><code class="hljs"><span class="hljs-comment">/// &lt;reference lib="WebWorker" /&gt;</span>

<span class="hljs-comment">// export empty type because of tsc --isolatedModules flag</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> {};
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> self: ServiceWorkerGlobalScope;
</code></pre><p>This is the beginning code for using a service worker with TypeScript and Babel compilation. The first line is a <a href="https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html">triple-slash directive</a> to indicate to the TypeScript compiler to use the Worker API type definitions. This is why it was important to note the inclusion of the Worker API type definitions in the "lib" setting of the tsconfig.json file.</p><p>After loading the Worker API type definitions, the next line is and empty type export that prevents an error as a result of the typescript compiler option "isolatedModules". Using "isolatedModules" is recommend when <a href="https://www.typescriptlang.org/docs/handbook/babel-with-typescript.html">using Babel with TypeScript</a>, but this requires all modules to have either one export or import statement. Since the service worker is a standalone script it cannot import or export modules, and by including the empty type export, the typescript compiler will no longer show the error:</p><pre><code class="hljs">(error TS1208: All files must be modules when the <span class="hljs-string">'--isolatedModules'</span> flag is provided)[]
</code></pre><p>The empty type export will be removed when they TypeScript code is compiled into JavaScript with Babel. This is a workaround until service workers have module support, and only needed when using the "--isolatedModules" flag with TypeScript.</p><p>The following declaration allows the type "ServiceWorkerGlobalScope" included in the WebWorker API type definitions, to be specified for the "self" variable that already exists. Without the type declaration the self variable would have the type "Window &amp; typeof globalThis", and the type definitions of that type do not overlap with the "ServiceWorkerGlobalScope" type. This is because the service worker does not have access to the window object or any other global scope, and the self variable used in the service worker code is of type <a href="https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/self">WorkerGlobalScope.self</a>, rather than <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/self">Window.self</a>.</p><p>Similar to the empty type export shown above this is also a workaround that is only needed to inform the TypeScript compiler of the proper types that are being used. There is some discussion regarding the type signatures on the TypeScript Github repository:</p><ul><li><a href="https://github.com/microsoft/TypeScript/issues/11781">Service Worker Typings</a></li><li><a href="https://github.com/microsoft/TypeScript/issues/14877">Unable to access ServiceWorkerGlobalScop via self</a></li></ul><h4>Service Worker install Event</h4><p>The first event listener that is added to the code, listens for the service worker install event to be triggered. When the install event occurs a cache using a combination of the "cacheName" and "version" variables is created, if it does not exist, and the index and offline HTML pages will be automatically added to the cache. Add this code below the "self" variable declaration:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> cacheName = <span class="hljs-string">"::yourserviceworker"</span>;
<span class="hljs-keyword">const</span> version = <span class="hljs-string">"v0.0.1"</span>;

self.addEventListener(<span class="hljs-string">"install"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.waitUntil(
    caches.open(version + cacheName).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">cache</span>) </span>{
      <span class="hljs-keyword">return</span> cache.addAll([<span class="hljs-string">"/"</span>, <span class="hljs-string">"/offline"</span>]);
    })
  );
});
</code></pre><h4>Service Worker activate Event</h4><p>Below the install event listener code we can add a separate event listener that will be triggered on the activate event. The activate event occurs after the install event, and is used to clean up any old service worker caches.</p><pre><code class="hljs">self.addEventListener(<span class="hljs-string">"activate"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.waitUntil(
    caches.keys().then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">keys</span>) </span>{
      <span class="hljs-comment">// Remove caches whose name is no longer valid</span>
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.all(
        keys
          .filter(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">key</span>) </span>{
            <span class="hljs-keyword">return</span> key.indexOf(version) !== <span class="hljs-number">0</span>;
          })
          .map(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">key</span>) </span>{
            <span class="hljs-keyword">return</span> caches.delete(key);
          })
      );
    })
  );
});
</code></pre><p>On activation if the "version" variable declared at the top of the service worker has been updated, any existing caches that do not match the new variable value will be deleted. This makes sure that the service worker is always using the latest cache values. The version variable acts as a cache bust when making changes to the service worker, after it has been previously deployed, otherwise the cached values will continue to be used until the service worker is no longer registered.</p><h4>Service Worker fetch Event</h4><p>After install and activation the service worker will listen for fetch events. In the fetch event listener function we can intercept responses and add them to the cache after the fetch response is complete, or the network can be bypassed and the response can be returned directly from the cache. The fetch event listener can be included below the activate event listener like this:</p><pre><code class="hljs">self.addEventListener(<span class="hljs-string">"fetch"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  <span class="hljs-keyword">const</span> request = event.request;

  <span class="hljs-comment">// Always fetch non-GET requests from the network</span>
  <span class="hljs-keyword">if</span> (request.method !== <span class="hljs-string">"GET"</span>) {
    event.respondWith(
      fetch(request).catch(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">return</span> caches.match(<span class="hljs-string">"/offline"</span>);
      }) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Promise</span>&lt;Response&gt;
    );
    <span class="hljs-keyword">return</span>;
  }
});
</code></pre><p>If the request is not a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET">GET</a> request, for example it could be a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">POST</a> or <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT">PUT</a> request, the service worker will always defer these requests to the network and the response is never cached. In the event that there is no network connection and the network request fails the service worker will return the offline page for any requests that are not GET requests.</p><h5>Network First then Cache - HTML Caching Strategy</h5><p>Any HTML request will use a network first then cache, caching strategy. This enables the latest version of the page to be requested from the origin server if there is a network connection. Without a network connection, the service worker will check if a prior version of an HTML page is available in the cache, and if not the offline page will be returned. Below the non-GET requests conditional block add this code to proxy HTML requests:</p><pre><code class="hljs"><span class="hljs-comment">// For HTML requests, try the network first, fall back to the cache,</span>
<span class="hljs-comment">// finally the offline page</span>
<span class="hljs-keyword">if</span> (
  request.headers.get(<span class="hljs-string">"Accept"</span>)?.indexOf(<span class="hljs-string">"text/html"</span>) !== -<span class="hljs-number">1</span> &amp;&amp;
  request.url.startsWith(<span class="hljs-built_in">this</span>.origin)
) {
  <span class="hljs-comment">// The request is text/html, so respond by caching the</span>
  <span class="hljs-comment">// item or showing the /offline offline</span>
  event.respondWith(
    fetch(request)
      .then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
        <span class="hljs-comment">// Stash a copy of this page in the cache</span>
        <span class="hljs-keyword">const</span> copy = response.clone();
        caches.open(version + cacheName).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">cache</span>) </span>{
          cache.put(request, copy);
        });
        <span class="hljs-keyword">return</span> response;
      })
      .catch(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">return</span> caches.match(request).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
          <span class="hljs-comment">// return the cache response or the /offline page.</span>
          <span class="hljs-keyword">return</span> response || caches.match(<span class="hljs-string">"/offline"</span>);
        });
      }) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Promise</span>&lt;Response&gt;
  );
  <span class="hljs-keyword">return</span>;
}
</code></pre><h5>Cache First then Network - non-HTML Caching Strategy</h5><p>For non-HTML requests, first the cache is checked and if the resource is in the cache it will be returned. If the non-HTML requests is not in the cache it will be requested from the origin server as long as there is a network connection, and then the response will be added to the cache for future requests. Add this code below the code that handles HTML requests:</p><pre><code class="hljs"><span class="hljs-comment">// For non-HTML requests, look in the cache first, fall back to the network</span>
<span class="hljs-keyword">if</span> (
  request.headers.get(<span class="hljs-string">"Accept"</span>)?.indexOf(<span class="hljs-string">"text/plain"</span>) === -<span class="hljs-number">1</span> &amp;&amp;
  request.url.startsWith(<span class="hljs-built_in">this</span>.origin)
) {
  event.respondWith(
    caches.match(request).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
      <span class="hljs-keyword">return</span> (
        response ||
        fetch(request)
          .then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
            <span class="hljs-keyword">const</span> copy = response.clone();

            <span class="hljs-keyword">if</span> (
              copy.headers.get(<span class="hljs-string">"Content-Type"</span>)?.indexOf(<span class="hljs-string">"text/plain"</span>) === -<span class="hljs-number">1</span>
            ) {
              caches.open(version + cacheName).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">cache</span>) </span>{
                cache.put(request, copy);
              });
            }

            <span class="hljs-keyword">return</span> response;
          })
          .catch(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            <span class="hljs-comment">// you can return an image placeholder here with</span>
            <span class="hljs-keyword">if</span> (request.headers.get(<span class="hljs-string">"Accept"</span>)?.indexOf(<span class="hljs-string">"image"</span>) !== -<span class="hljs-number">1</span>) {
            }
          })
      );
    }) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Promise</span>&lt;Response&gt;
  );
  <span class="hljs-keyword">return</span>;
}
</code></pre><p><strong>NOTE</strong>: If the request is for a plaintext resource it will be ignored by the service worker with this configuration. Additionally if request <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept">Accept</a> header does not include "text/plain", but the response <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type">Content-Type</a> header is of "text/plain" then the response will not be stored in the service worker cache. For any other response types like css, js, png, or jpg, the response will be cached by the service worker in this code section.</p><p>Here's what the entire service-worker.js file should look like will all the sections put together:</p><pre><code class="hljs"><span class="hljs-comment">/// &lt;reference lib="WebWorker" /&gt;</span>

<span class="hljs-comment">// export empty type because of tsc --isolatedModules flag</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> {};
<span class="hljs-keyword">declare</span> <span class="hljs-keyword">const</span> self: ServiceWorkerGlobalScope;

<span class="hljs-keyword">const</span> cacheName = <span class="hljs-string">"::yourserviceworker"</span>;
<span class="hljs-keyword">const</span> version = <span class="hljs-string">"v0.0.1"</span>;

self.addEventListener(<span class="hljs-string">"install"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.waitUntil(
    caches.open(version + cacheName).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">cache</span>) </span>{
      <span class="hljs-keyword">return</span> cache.addAll([<span class="hljs-string">"/"</span>, <span class="hljs-string">"/offline"</span>]);
    })
  );
});

self.addEventListener(<span class="hljs-string">"activate"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.waitUntil(
    caches.keys().then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">keys</span>) </span>{
      <span class="hljs-comment">// Remove caches whose name is no longer valid</span>
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.all(
        keys
          .filter(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">key</span>) </span>{
            <span class="hljs-keyword">return</span> key.indexOf(version) !== <span class="hljs-number">0</span>;
          })
          .map(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">key</span>) </span>{
            <span class="hljs-keyword">return</span> caches.delete(key);
          })
      );
    })
  );
});

self.addEventListener(<span class="hljs-string">"fetch"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  <span class="hljs-keyword">const</span> request = event.request;

  <span class="hljs-comment">// Always fetch non-GET requests from the network</span>
  <span class="hljs-keyword">if</span> (request.method !== <span class="hljs-string">"GET"</span>) {
    event.respondWith(
      fetch(request).catch(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">return</span> caches.match(<span class="hljs-string">"/offline"</span>);
      }) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Promise</span>&lt;Response&gt;
    );
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">// For HTML requests, try the network first, fall back to the cache,</span>
  <span class="hljs-comment">// finally the offline page</span>
  <span class="hljs-keyword">if</span> (
    request.headers.get(<span class="hljs-string">"Accept"</span>)?.indexOf(<span class="hljs-string">"text/html"</span>) !== -<span class="hljs-number">1</span> &amp;&amp;
    request.url.startsWith(<span class="hljs-built_in">this</span>.origin)
  ) {
    <span class="hljs-comment">// The request is text/html, so respond by caching the</span>
    <span class="hljs-comment">// item or showing the /offline offline</span>
    event.respondWith(
      fetch(request)
        .then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
          <span class="hljs-comment">// Stash a copy of this page in the cache</span>
          <span class="hljs-keyword">const</span> copy = response.clone();
          caches.open(version + cacheName).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">cache</span>) </span>{
            cache.put(request, copy);
          });
          <span class="hljs-keyword">return</span> response;
        })
        .catch(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
          <span class="hljs-keyword">return</span> caches.match(request).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
            <span class="hljs-comment">// return the cache response or the /offline page.</span>
            <span class="hljs-keyword">return</span> response || caches.match(<span class="hljs-string">"/offline"</span>);
          });
        }) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Promise</span>&lt;Response&gt;
    );
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">// For non-HTML requests, look in the cache first, fall back to the network</span>
  <span class="hljs-keyword">if</span> (
    request.headers.get(<span class="hljs-string">"Accept"</span>)?.indexOf(<span class="hljs-string">"text/plain"</span>) === -<span class="hljs-number">1</span> &amp;&amp;
    request.url.startsWith(<span class="hljs-built_in">this</span>.origin)
  ) {
    event.respondWith(
      caches.match(request).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
        <span class="hljs-keyword">return</span> (
          response ||
          fetch(request)
            .then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
              <span class="hljs-keyword">const</span> copy = response.clone();

              <span class="hljs-keyword">if</span> (
                copy.headers.get(<span class="hljs-string">"Content-Type"</span>)?.indexOf(<span class="hljs-string">"text/plain"</span>) === -<span class="hljs-number">1</span>
              ) {
                caches.open(version + cacheName).then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">cache</span>) </span>{
                  cache.put(request, copy);
                });
              }

              <span class="hljs-keyword">return</span> response;
            })
            .catch(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
              <span class="hljs-comment">// you can return an image placeholder here with</span>
              <span class="hljs-keyword">if</span> (request.headers.get(<span class="hljs-string">"Accept"</span>)?.indexOf(<span class="hljs-string">"image"</span>) !== -<span class="hljs-number">1</span>) {
              }
            })
        );
      }) <span class="hljs-keyword">as</span> <span class="hljs-built_in">Promise</span>&lt;Response&gt;
    );
    <span class="hljs-keyword">return</span>;
  }
});
</code></pre><h3>Test Service Worker Locally</h3><p>You can test the service worker by running your project locally with the <a href="https://www.npmjs.com/package/http-server">http-server npm package</a>. First make sure to compile the TypeScript service worker code by using the command <kbd>npm run build-typecheck</kbd>. Then, to install the http-server npm package run the command <kbd>npm i http-server --save-dev</kbd>. After installing run the command <kbd>http-server</kbd> in your project folder where the index.html and offline.html pages are. You should then see your website in the browser by navigating to the default http-server url localhost:8080. With browser dev tools you can inspect your website including managing service worker installation state and cache status.</p><p>In Chrome this is in the "Application" tab of chrome DevTools:</p><p><img src="https://www.devextent.com/images/application-service-worker.png" alt="Chrome DevTools Application tab Service Worker information"></p><p>In the left hand navigation panel of the Application tab you can also see a section for "Cache Storage", and in the dropdown list should be an entry for the service worker, with the name you chose, listing all of the cached assets. There is three cached responses: the index.html page, the offline.html page, and the script.js file that registers the service worker.</p><p>Here's what that looks like in Chrome:</p><p><img src="https://www.devextent.com/images/application-cache-storage.png" alt="Chrome DevTools Application tab Cache Storage"></p><p>You can test offline mode by selecting the checkbox for "Offline" and deleting the cache item with name "/". Refreshing the page should load the offline.html since the index.html is no longer cached.</p><p>Your site can now support offline viewing for pages that are cached with the service worker, or show an offline page when there is no response previously cached and a network connection is unavailable.</p>]]></description></item><item><title>Generate an XML Sitemap with Node.js</title><pubDate>Tue, 09 Mar 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/generate-xml-sitemap-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/generate-xml-sitemap-nodejs/</guid><description><![CDATA[<p>An xml sitemap informs search engines with information regarding the structure of a website and which pages should be available to be indexed in search results. The <a href="https://developers.google.com/search/docs/advanced/sitemaps/overview">xml sitemap</a> file includes the url location for all the pages included and the date the page was last modified. If you are building a blog website it is especially important to include a sitemap file containing information about all the blog posts. Recently more blogs are being built with the <a href="https://jamstack.org/">Jamstack</a>, and since there is no server to dynamically serve the sitemap file, we can use Node.js to statically generate an xml sitemap file from blog post data. In this example we will use <a href="https://www.typescriptlang.org/">TypeScript</a> along with the <a href="https://www.npmjs.com/package/xml">xml npm package</a> to convert a JSON object containing blog post data into an xml string, and then write the generated xml string to a sitemap file. Before following these steps make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>npm init package.json</h3><p>The first thing we need to do is to generate a package.json file so that we can install the xml npm package. If you don't already have a package.json file setup for your project run the command <kbd>npm init</kbd> in the project folder and follow the prompts. Once the package.json file is created run the command <kbd>npm install xml typescript --save</kbd>. This will install the xml npm package and the <a href="https://www.npmjs.com/package/typescript">TypeScript npm package</a>. Since we are using TypeScript for this example we also need to install the type definitions for the xml package. These can be installed by running the command <kbd>npm install @types/xml --save-dev</kbd>.</p><h3>Configure ES Module format</h3><p>We are also going to use <a href="https://nodejs.org/api/esm.html">ECMAScript modules</a>, or ES modules, instead of <a href="https://nodejs.org/api/modules.html">CommonJS modules</a>, as Node.js now supports ES Module format. In order to use ES Modules the "type" property with value of "module" also needs to be added to the package.json file. Please read my other post for more information regarding how to <a href="https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/">import and export ES Modules in Node.js</a> With these settings in place the package.json file should look similar to this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"xmlsitemap"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.2.3"</span>,
    <span class="hljs-attr">"xml"</span>: <span class="hljs-string">"^1.0.1"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/xml"</span>: <span class="hljs-string">"^1.0.5"</span>
  }
}
</code></pre><h3>Configure TypeScript with Node.js</h3><p>After adding the package.json file we can include the configuration steps that are needed to use TypeScript with ES Modules. To do this we can add a "tsconfig.json" file with the following settings:</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"allowSyntheticDefaultImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2019"</span>],
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"index.ts"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules/**/*"</span>]
}
</code></pre><p>The "module" setting with value "esnext" is the setting that configures the TypeScript compiler to compile TypeScript into JavaScript using the ES Module format.</p><h3>Generate XML String with TypeScript</h3><p>Now we can create the Node.js script that will generate the XML string that will be written to the sitemap file. To do this add a new file to the project named "index.ts". And add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;
<span class="hljs-keyword">import</span> { writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> pages = [
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page One"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Dec 22 2020"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-one"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page Two"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Feb 1 2021"</span>,
      <span class="hljs-attr">lastModified</span>: <span class="hljs-string">"Feb 2 2021"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-two"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page Three"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Mar 2 2021"</span>,
      <span class="hljs-attr">lastModified</span>: <span class="hljs-string">"Mar 5 2021"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-three"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page Four"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Mar 20 2021"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-four"</span>,
    },
  ];

  <span class="hljs-keyword">const</span> indexItem = {
    <span class="hljs-comment">//todo: build index item</span>
  };

  <span class="hljs-keyword">const</span> sitemapItems = pages.reduce(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
    items: { url: [{ loc: <span class="hljs-built_in">string</span> }, { lastmod: <span class="hljs-built_in">string</span> }] }[],
    item: {
      title: <span class="hljs-built_in">string</span>;
      lastModified?: <span class="hljs-built_in">string</span>;
      created: <span class="hljs-built_in">string</span>;
      slug: <span class="hljs-built_in">string</span>;
    }
  </span>) </span>{
    <span class="hljs-comment">// todo: build page items</span>
    <span class="hljs-keyword">return</span> items;
  }, []);
}

main();
</code></pre><p>This code is now set up to use sample data that is stored in the "pages" array. In this example we are including data to represent pages that would be included as part of a static site generator build process. Typically with a Jamstack blog this data would be sourced from markdown files, or another common option is requesting data from a <a href="https://en.wikipedia.org/wiki/Headless_content_management_system">headless content management system</a>. For the purposes of this example we are including a short page list directly in the code, but usually this would be dynamically included at build time. After the sample page data there is one object that will contain the data for the sitemap index item, and the other is an array of objects containing the sitemap data for each individual page.</p><h4>Create Sitemap Index Item</h4><p>The first item in the sitemap will include optional tags that won't be included in the individual page sitemap items, and that is why it is created separately. Besides including the url location and a last modified time, the index sitemap item includes the <a href="https://www.sitemaps.org/protocol.html#changefreqdef">change frequency parameter</a> and a <a href="https://www.sitemaps.org/protocol.html#prioritydef">priority parameter</a>. These are optional and can be included for each sitemap item, but in this case we are only including it for the root url of the sitemap. Go ahead an add the following inside of the "indexItem" object shown above:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> indexItem = {
  <span class="hljs-comment">//build index item</span>
  <span class="hljs-attr">url</span>: [
    {
      <span class="hljs-attr">loc</span>: <span class="hljs-string">"YOUR-DOMAIN-HERE"</span>,
    },
    {
      <span class="hljs-attr">lastmod</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
        <span class="hljs-built_in">Math</span>.max.apply(
          <span class="hljs-literal">null</span>,
          pages.map(<span class="hljs-function">(<span class="hljs-params">page</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
              page.lastModified ?? page.created
            ) <span class="hljs-keyword">as</span> unknown <span class="hljs-keyword">as</span> <span class="hljs-built_in">number</span>;
          })
        )
      )
        .toISOString()
        .split(<span class="hljs-string">"T"</span>)[<span class="hljs-number">0</span>],
    },
    { <span class="hljs-attr">changefreq</span>: <span class="hljs-string">"daily"</span> },
    { <span class="hljs-attr">priority</span>: <span class="hljs-string">"1.0"</span> },
  ],
};
</code></pre><p>Make sure to replace "YOUR-DOMAIN-HERE" with your actual domain. Also note that in order to find the most recent date of all of the pages, the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max">Math.max()</a> function is used, in combination with the function prototype method <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply">.apply()</a>, which passes the array of page object dates as parameters to the Math.max function. The first parameter of the .apply method is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this">this</a>, which is not needed so it is set to null.</p><p>Additionally since we are using TypeScript the date objects cannot be cast directly from a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date">JavaScript Date object</a> into a number, so they are cast to the <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type">unknown type</a> as an intermediary step to prevent the TypeScript compiler from showing type errors. Once the maximum date of all the pages last modified or created dates is determined, it is formatted as an <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString">ISO string date format</a> and then trimmed to only include the year, month, and day.</p><h4>Create Sitemap Page Items</h4><p>With the index sitemap item created, we can now build the individual page items in the "sitemapItems" array. This process will be similar to creating the index item, but each page item will only include a url location property and a last modified timestamp. To build the sitemap items add the following code to the sitemapItems array creation step:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> sitemapItems = pages.reduce(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  items: { url: [{ loc: <span class="hljs-built_in">string</span> }, { lastmod: <span class="hljs-built_in">string</span> }] }[],
  item: {
    title: <span class="hljs-built_in">string</span>;
    lastModified?: <span class="hljs-built_in">string</span>;
    created: <span class="hljs-built_in">string</span>;
    slug: <span class="hljs-built_in">string</span>;
  }
</span>) </span>{
  <span class="hljs-comment">// build page items</span>
  items.push({
    <span class="hljs-attr">url</span>: [
      {
        <span class="hljs-attr">loc</span>: <span class="hljs-string">`YOUR-DOMAIN-HERE/<span class="hljs-subst">${item.slug}</span>`</span>,
      },
      {
        <span class="hljs-attr">lastmod</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(item.lastModified ?? item.created)
          .toISOString()
          .split(<span class="hljs-string">"T"</span>)[<span class="hljs-number">0</span>],
      },
    ],
  });
  <span class="hljs-keyword">return</span> items;
}, []);
</code></pre><p>For each page items url location property make sure to replace the placeholder text with your actual domain.</p><h4>Build Sitemap Object</h4><p>Now that both the sitemap index item and the sitemap items for each page are created we can combine them into one object that will be the entire sitemap object. At the end of the main function add in this code:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> sitemapObject = {
  <span class="hljs-attr">urlset</span>: [
    {
      <span class="hljs-attr">_attr</span>: {
        <span class="hljs-attr">xmlns</span>: <span class="hljs-string">"http://www.sitemaps.org/schemas/sitemap/0.9"</span>,
      },
    },
    indexItem,
    ...sitemapItems,
  ],
};

<span class="hljs-keyword">const</span> sitemap = <span class="hljs-string">`&lt;?xml version="1.0" encoding="UTF-8"?&gt;<span class="hljs-subst">${xml(sitemapObject)}</span>`</span>;

<span class="hljs-built_in">console</span>.log(sitemap);
</code></pre><p>At this point we can test to make sure that the JSON to xml string conversion is working properly by logging the xml string to the console. To do this we need to add a script command to the package.json file created earlier.</p><h3>Run Node.js Script with npm Scripts</h3><p>In order to test the xml sitemap creation process we can add a script to the package.json file named "generate-sitemap". This script will invoke the TypeScript compiler and then run the transpiled JavaScript with Node.js. Here is what the script should look like in the package.json file:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"generate-sitemap"</span>: <span class="hljs-string">"tsc &amp;&amp; node index.js"</span>
  }
}
</code></pre><p>We can run this script with the command <kbd>npm run generate-sitemap</kbd>. After running the generate sitemap command, the xml sitemap string should be output to the console. Here is what it will look like:</p><pre><code class="hljs"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">urlset</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.sitemaps.org/schemas/sitemap/0.9"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">loc</span>&gt;</span>YOUR-DOMAIN-HERE<span class="hljs-tag">&lt;/<span class="hljs-name">loc</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">lastmod</span>&gt;</span>2021-03-20<span class="hljs-tag">&lt;/<span class="hljs-name">lastmod</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">changefreq</span>&gt;</span>daily<span class="hljs-tag">&lt;/<span class="hljs-name">changefreq</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">priority</span>&gt;</span>1.0<span class="hljs-tag">&lt;/<span class="hljs-name">priority</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">loc</span>&gt;</span>YOUR-DOMAIN-HERE/sample-page-one<span class="hljs-tag">&lt;/<span class="hljs-name">loc</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">lastmod</span>&gt;</span>2020-12-22<span class="hljs-tag">&lt;/<span class="hljs-name">lastmod</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">loc</span>&gt;</span>YOUR-DOMAIN-HERE/sample-page-two<span class="hljs-tag">&lt;/<span class="hljs-name">loc</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">lastmod</span>&gt;</span>2021-02-02<span class="hljs-tag">&lt;/<span class="hljs-name">lastmod</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">loc</span>&gt;</span>YOUR-DOMAIN-HERE/sample-page-three<span class="hljs-tag">&lt;/<span class="hljs-name">loc</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">lastmod</span>&gt;</span>2021-03-05<span class="hljs-tag">&lt;/<span class="hljs-name">lastmod</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">loc</span>&gt;</span>YOUR-DOMAIN-HERE/sample-page-four<span class="hljs-tag">&lt;/<span class="hljs-name">loc</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">lastmod</span>&gt;</span>2021-03-20<span class="hljs-tag">&lt;/<span class="hljs-name">lastmod</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">url</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">urlset</span>&gt;</span>
</code></pre><p>Instead of outputting the sitemap as an xml string we can write it to a file using the Node.js <a href="https://nodejs.org/api/fs.html#fs_fspromises_writefile_file_data_options">write file method</a> in the fs module.</p><h3>Write XML String to Sitemap File</h3><p>You can replace the "console.log" statement at the bottom of the index.ts main function with the following code to write the sitemap xml string to a file named "sitemap.xml":</p><pre><code class="hljs"><span class="hljs-keyword">await</span> writeFileAsync(<span class="hljs-string">"./sitemap.xml"</span>, sitemap, <span class="hljs-string">"utf8"</span>);
</code></pre><p>You will also need to add one import statements at the top of the index.ts file. This will import the promisify function from the util module. This way we can convert the writeFile module to use promises instead of callbacks, which enables the use of async await syntax.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">const</span> writeFileAsync = promisify(writeFile);
</code></pre><p>Here is what the entire index.ts file should look like with all the code included:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;
<span class="hljs-keyword">import</span> { writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">const</span> writeFileAsync = promisify(writeFile);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> pages = [
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page One"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Dec 22 2020"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-one"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page Two"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Feb 1 2021"</span>,
      <span class="hljs-attr">lastModified</span>: <span class="hljs-string">"Feb 2 2021"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-two"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page Three"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Mar 2 2021"</span>,
      <span class="hljs-attr">lastModified</span>: <span class="hljs-string">"Mar 5 2021"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-three"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Sample Page Four"</span>,
      <span class="hljs-attr">created</span>: <span class="hljs-string">"Mar 20 2021"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"sample-page-four"</span>,
    },
  ];

  <span class="hljs-keyword">const</span> indexItem = {
    <span class="hljs-comment">//build index item</span>
    <span class="hljs-attr">url</span>: [
      {
        <span class="hljs-attr">loc</span>: <span class="hljs-string">"YOUR-DOMAIN-HERE"</span>,
      },
      {
        <span class="hljs-attr">lastmod</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
          <span class="hljs-built_in">Math</span>.max.apply(
            <span class="hljs-literal">null</span>,
            pages.map(<span class="hljs-function">(<span class="hljs-params">page</span>) =&gt;</span> {
              <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
                page.lastModified ?? page.created
              ) <span class="hljs-keyword">as</span> unknown <span class="hljs-keyword">as</span> <span class="hljs-built_in">number</span>;
            })
          )
        )
          .toISOString()
          .split(<span class="hljs-string">"T"</span>)[<span class="hljs-number">0</span>],
      },
      { <span class="hljs-attr">changefreq</span>: <span class="hljs-string">"daily"</span> },
      { <span class="hljs-attr">priority</span>: <span class="hljs-string">"1.0"</span> },
    ],
  };

  <span class="hljs-keyword">const</span> sitemapItems = pages.reduce(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
    items: { url: [{ loc: <span class="hljs-built_in">string</span> }, { lastmod: <span class="hljs-built_in">string</span> }] }[],
    item: {
      title: <span class="hljs-built_in">string</span>;
      lastModified?: <span class="hljs-built_in">string</span>;
      created: <span class="hljs-built_in">string</span>;
      slug: <span class="hljs-built_in">string</span>;
    }
  </span>) </span>{
    <span class="hljs-comment">// build page items</span>
    items.push({
      <span class="hljs-attr">url</span>: [
        {
          <span class="hljs-attr">loc</span>: <span class="hljs-string">`YOUR-DOMAIN-HERE/<span class="hljs-subst">${item.slug}</span>`</span>,
        },
        {
          <span class="hljs-attr">lastmod</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(item.lastModified ?? item.created)
            .toISOString()
            .split(<span class="hljs-string">"T"</span>)[<span class="hljs-number">0</span>],
        },
      ],
    });
    <span class="hljs-keyword">return</span> items;
  }, []);

  <span class="hljs-keyword">const</span> sitemapObject = {
    <span class="hljs-attr">urlset</span>: [
      {
        <span class="hljs-attr">_attr</span>: {
          <span class="hljs-attr">xmlns</span>: <span class="hljs-string">"http://www.sitemaps.org/schemas/sitemap/0.9"</span>,
        },
      },
      indexItem,
      ...sitemapItems,
    ],
  };

  <span class="hljs-keyword">const</span> sitemap = <span class="hljs-string">`&lt;?xml version="1.0" encoding="UTF-8"?&gt;<span class="hljs-subst">${xml(sitemapObject)}</span>`</span>;

  <span class="hljs-keyword">await</span> writeFileAsync(<span class="hljs-string">"./sitemap.xml"</span>, sitemap, <span class="hljs-string">"utf8"</span>);
}

main();
</code></pre><p>You can then run the <kbd>npm run generate-sitemap</kbd> command again, and a new file named "sitemap.xml" should be created in the project folder. The contents of this file should be identical to the sitemap xml string that was logged to the console in the previous step.</p><h3>Test Sitemap File in a Browser</h3><p>To test out the sitemap in a browser, in the same project folder that we created the index.ts file run the command <kbd>npm install http-server --save-dev</kbd>, and add another script to the package.json file like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"generate-sitemap"</span>: <span class="hljs-string">"tsc &amp;&amp; node index.js"</span>,
    <span class="hljs-attr">"serve"</span>: <span class="hljs-string">"http-server"</span>
  }
}
</code></pre><p>Then to use the <a href="https://www.npmjs.com/package/http-server">http-server npm package</a> run the command <kbd>npm run serve</kbd>, and you should see the http-server npm package output the url it is serving. It is most likely the default setting so navigating to "localhost:8080/sitemap.xml" should show the sitemap file that will look similar to this:</p><p><img src="https://www.devextent.com/images/xml-sitemap.png" alt="xml sitemap browser display"></p><h3>Add Sitemap to robots.txt</h3><p>Now you can include the sitemap generation step in the build process of the static site generator that you might be using for your Jamstack blog. You can also add a line to the robots.txt file to indicate the url of the sitemap file. If you are using a robots.txt file for your site make sure to add the following with your domain included:</p><pre><code class="hljs">Sitemap: https://YOUR-DOMAIN-HERE/sitemap.xml
</code></pre>]]></description></item><item><title>Use the Dart Sass JavaScript Implementation to Compile SASS with Node.js</title><pubDate>Thu, 25 Feb 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/dart-sass-javascript-implementation-npm-compile-sass/</link><guid isPermaLink="true">https://www.devextent.com/dart-sass-javascript-implementation-npm-compile-sass/</guid><description><![CDATA[<p><em>This post is an updated version of a previous post containing instructions on <a href="https://www.devextent.com/npm-compile-sass/">how to compile sass with the node-sass npm package</a>, which is now deprecated.</em></p><hr><p>The <a href="https://sass-lang.com/community">SASS team</a> now recommends using <a href="https://sass-lang.com/dart-sass">Dart Sass</a> in favor of <a href="https://sass-lang.com/blog/libsass-is-deprecated">LibSass</a> for new development projects. This means that the <a href="https://www.npmjs.com/package/sass">sass</a> npm package should be used instead of the <a href="https://www.npmjs.com/package/node-sass">node-sass</a> npm package, which is built on top of LibSass, to compile sass with Node.js. The sass npm package is a pure JavaScript implementation of Dart Sass. The Dart Sass JavaScript API strives to be compatible with the existing node-sass API, so that it can be integrated into existing workflows with minimal changes. This post will show how to install the Dart Sass Javascript implementation with npm and use it via the supported JavaScript API and the command line. Before proceeding make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>npm install sass</h3><p>After installing Node.js and npm we can create a sample project to demonstrate the functionality of the sass npm package. To do this create a project folder and open it in a terminal window. Then run the command <kbd>npm init</kbd> and follow the prompts, which will create a package.json file. Then we can install the sass node module into the project, by running the command <kbd>npm install sass --save</kbd>.</p><p>We will also be using <a href="https://nodejs.org/api/esm.html">ES Module</a> format for this example so the package.json requires an additional setting after generating. Add the "type" property to the package.json with the value set to "module", so that Node.js will use ES Modules rather than <a href="https://nodejs.org/docs/latest/api/modules.html">CommonJS modules</a>. Here is some additional information about how to <a href="https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/">import and export ES Modules in Node.js</a>, which explains why this setting is necessary.</p><p>Your package.json file should now look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npmsass"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"sass"</span>: <span class="hljs-string">"^1.32.8"</span>
  }
}
</code></pre><h3>SCSS</h3><p>The sass npm package is now installed, but in order to use it we will need a SCSS file. In the same project folder create a new file named "styles.scss" and place the following code inside:</p><pre><code class="hljs"><span class="hljs-comment">/* This CSS will print because %message-shared is extended. */</span>
%message-shared {
  <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#ccc</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>;
}

<span class="hljs-comment">// This CSS won't print because %equal-heights is never extended.</span>
%equal-heights {
  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">flex-wrap</span>: wrap;
}

<span class="hljs-selector-class">.message</span> {
  <span class="hljs-keyword">@extend</span> %message-shared;
}

<span class="hljs-selector-class">.success</span> {
  <span class="hljs-keyword">@extend</span> %message-shared;
  <span class="hljs-attribute">border-color</span>: green;
}

<span class="hljs-selector-class">.error</span> {
  <span class="hljs-keyword">@extend</span> %message-shared;
  <span class="hljs-attribute">border-color</span>: red;
}

<span class="hljs-selector-class">.warning</span> {
  <span class="hljs-keyword">@extend</span> %message-shared;
  <span class="hljs-attribute">border-color</span>: yellow;
}
</code></pre><p>The above SCSS code is borrowed from the <a href="https://sass-lang.com/guide">Sass Basics</a> guide and demonstrates one of the most useful features of Sass which is the <a href="https://sass-lang.com/documentation/at-rules/extend">@extend</a> at-rule, to share a set of CSS properties among different selectors. Now that we have a SCSS file we can compile it to CSS using the sass npm package.</p><h3>Compile Sass with Dart Sass JavaScript API</h3><p>To use the sass npm package JavaScript API, we need to create the index.js file that is set to the "main" property value in the package.json file, created in the first step. This will be the entry point for the Node.js process that will carry out the SASS compilation. In the same project folder create a new file named "index.js", and add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> sass <span class="hljs-keyword">from</span> <span class="hljs-string">"sass"</span>;
<span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">const</span> sassRenderPromise = promisify(sass.render);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> styleResult = <span class="hljs-keyword">await</span> sassRenderPromise({
    <span class="hljs-attr">file</span>: <span class="hljs-string">`<span class="hljs-subst">${process.cwd()}</span>/styles.scss`</span>,
  });

  <span class="hljs-built_in">console</span>.log(styleResult.css.toString());
}
main();
</code></pre><p>This code imports the sass package along with the <a href="https://nodejs.org/api/util.html#util_util_promisify_original">util.promisify</a> module and converts the sass render function to use promises instead of the default callback implementation. This makes working with the asynchronous API of the sass npm package more manageable because it allows for the use of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function">async/await</a> syntax.</p><p>After importing and "promisifying" the sass npm package, the main function contains the code to compile the styles.scss file into CSS. In order to run this code add the following the scripts property in the package.json file:</p><pre><code class="hljs">{
  <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node index.js"</span>
}
</code></pre><p>We can then execute the main function by running the command <kbd>npm run start</kbd>, and the css output will be logged to the console.</p><p>Instead of logging directly to the console it is much more useful to write the CSS output to a file. The sass npm package does not expose a JavaScript API to write a file directly, however it does support a configuration property to indicate which file the CSS output will be written to. By adding this configuration and using the <a href="https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback">fs.writeFile</a> module the CSS can be written to an output to a file like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> sass <span class="hljs-keyword">from</span> <span class="hljs-string">"sass"</span>;
<span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">import</span> { writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">const</span> sassRenderPromise = promisify(sass.render);
<span class="hljs-keyword">const</span> writeFilePromise = promisify(writeFile);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> styleResult = <span class="hljs-keyword">await</span> sassRenderPromise({
    <span class="hljs-attr">file</span>: <span class="hljs-string">`<span class="hljs-subst">${process.cwd()}</span>/styles.scss`</span>,
    <span class="hljs-attr">outFile</span>: <span class="hljs-string">`<span class="hljs-subst">${process.cwd()}</span>/styles.css`</span>,
  });

  <span class="hljs-built_in">console</span>.log(styleResult.css.toString());

  <span class="hljs-keyword">await</span> writeFilePromise(<span class="hljs-string">"styles.css"</span>, styleResult.css, <span class="hljs-string">"utf8"</span>);
}
main();
</code></pre><p>After running the <kbd>npm run start</kbd> command again, you should now see a styles.css file in the same project folder, that contains the compiled CSS output:</p><pre><code class="hljs"><span class="hljs-comment">/* This CSS will print because %message-shared is extended. */</span>
<span class="hljs-selector-class">.warning</span>,
<span class="hljs-selector-class">.error</span>,
<span class="hljs-selector-class">.success</span>,
<span class="hljs-selector-class">.message</span> {
  <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#ccc</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>;
}

<span class="hljs-selector-class">.success</span> {
  <span class="hljs-attribute">border-color</span>: green;
}

<span class="hljs-selector-class">.error</span> {
  <span class="hljs-attribute">border-color</span>: red;
}

<span class="hljs-selector-class">.warning</span> {
  <span class="hljs-attribute">border-color</span>: yellow;
}
</code></pre><h3>SASS Render Configuration Options</h3><p>The sass npm package supports other render options including:</p><ul><li>sourceMap</li><li>sourceMapContents</li><li>outputStyle</li></ul><p>These can be added by modifying the options object passed into the sass render function. When including a source map file, a separate file needs to be written to the project folder containing the sourcemap information. To add these options make the following changes to the index.js:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> sass <span class="hljs-keyword">from</span> <span class="hljs-string">"sass"</span>;
<span class="hljs-keyword">import</span> { promisify } <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">import</span> { writeFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">const</span> sassRenderPromise = promisify(sass.render);
<span class="hljs-keyword">const</span> writeFilePromise = promisify(writeFile);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> styleResult = <span class="hljs-keyword">await</span> sassRenderPromise({
    <span class="hljs-attr">file</span>: <span class="hljs-string">`<span class="hljs-subst">${process.cwd()}</span>/styles.scss`</span>,
    <span class="hljs-attr">outFile</span>: <span class="hljs-string">`<span class="hljs-subst">${process.cwd()}</span>/styles.css`</span>,
    <span class="hljs-attr">sourceMap</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">sourceMapContents</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">outputStyle</span>: <span class="hljs-string">"compressed"</span>,
  });

  <span class="hljs-built_in">console</span>.log(styleResult.css.toString());

  <span class="hljs-keyword">await</span> writeFilePromise(<span class="hljs-string">"styles.css"</span>, styleResult.css, <span class="hljs-string">"utf8"</span>);

  <span class="hljs-keyword">await</span> writeFilePromise(<span class="hljs-string">"styles.css.map"</span>, styleResult.map, <span class="hljs-string">"utf8"</span>);
}
main();
</code></pre><p>Then run the <kbd>npm run start</kbd> command again and you should see the "styles.css" and "styles.css.map" files have both been updated.</p><p>The styles.css should now output with the blank spaces removed, and it will include a comment at the bottom to indicate the corresponding sourcemap file, which will look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"version"</span>: <span class="hljs-number">3</span>,
  <span class="hljs-attr">"sourceRoot"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"sources"</span>: [<span class="hljs-string">"styles.scss"</span>],
  <span class="hljs-attr">"names"</span>: [],
  <span class="hljs-attr">"mappings"</span>: <span class="hljs-string">"AACA,kCACE,sBACA,aACA,WAaF,SAEE,mBAGF,OAEE,iBAGF,SAEE"</span>,
  <span class="hljs-attr">"file"</span>: <span class="hljs-string">"styles.css"</span>,
  <span class="hljs-attr">"sourcesContent"</span>: [
    <span class="hljs-string">"/* This CSS will print because %message-shared is extended. */\r\n%message-shared {\r\n  border: 1px solid #ccc;\r\n  padding: 10px;\r\n  color: #333;\r\n}\r\n\r\n// This CSS won't print because %equal-heights is never extended.\r\n%equal-heights {\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n}\r\n\r\n.message {\r\n  @extend %message-shared;\r\n}\r\n\r\n.success {\r\n  @extend %message-shared;\r\n  border-color: green;\r\n}\r\n\r\n.error {\r\n  @extend %message-shared;\r\n  border-color: red;\r\n}\r\n\r\n.warning {\r\n  @extend %message-shared;\r\n  border-color: yellow;\r\n}\r\n"</span>
  ]
}
</code></pre><p>The sourcemap will allow for easier debugging and the browser will now load both files. In the debug inspector the browser will show the line in the SCSS source code that corresponds to the CSS output being inspected.</p><h3>Compile SASS using Dart Sass CLI</h3><p>It is also possible to use the sass npm package directly from the command line. To do this with the same configuration as the example using the JavaScript API add the following the the package.json scripts property:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compileSass"</span>: <span class="hljs-string">"sass styles.scss styles.css --style=compressed --embed-sources"</span>
  }
}
</code></pre><p>This will add a package.json script to run the SASS compiler, by running the command <kbd>npm run compileSass</kbd>. To make sure it is working as expected you might want to delete the previously generated styles.css and styles.css.map files, before running the <kbd>npm run compileSass</kbd> command.</p><p>Using the sass npm package JavaScript API or command line interface, should result in the same output consisting of both the css and css.map files, as both methods rely on the JavaScript implementation of Dart Sass. The main difference is that when using the CLI option the files will automatically be written based on the input and output specified, but when using the JavaScript API we must use the fs.writeFile module to write these files to the project folder.</p>]]></description></item><item><title>Secure a Github Webhook with Node.js</title><pubDate>Tue, 23 Feb 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/secure-github-webhook-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/secure-github-webhook-nodejs/</guid><description><![CDATA[<p>GitHub provides <a href="https://docs.github.com/en/developers/webhooks-and-events">webhooks</a> that can send a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">POST</a> request when a predetermined event is triggered. There are many different <a href="https://docs.github.com/en/developers/webhooks-and-events/github-event-types">GitHub event types</a>, and a common event to integrate into workflows is the <a href="https://docs.github.com/en/developers/webhooks-and-events/github-event-types#pullrequestevent">PullRequestEvent</a>. Any time a pull request has event activity of the following action types:</p><ul><li>opened</li><li>closed</li><li>reopened</li><li>assigned</li><li>unassigned</li><li>review_requested</li><li>review_requested_removed</li><li>labeled</li><li>unlabeled</li><li>synchronize</li></ul><p>a POST request can automatically be sent to trigger an integration that is waiting to accept the incoming request. In this example we can set up an <a href="https://docs.microsoft.com/en-us/azure/developer/javascript/how-to/develop-serverless-apps">Azure Serverless Function using Node.js</a> to accept a GitHub Webhook POST payload. The serverless function will only run when the pull request is from the main branch, the branch associated with the pull request is merged, and the pull request is closed. If all of the following conditions are true, we will also make sure to <a href="https://docs.github.com/en/developers/webhooks-and-events/securing-your-webhooks">secure the GitHub webhook</a> by using the <a href="https://www.npmjs.com/package/@octokit/webhooks">@octokit/webhooks</a> npm package to verify the "x-hub-signature-256" request header using an application secret. When the incoming POST request payload is verified to be originating from GitHub's servers any application logic relating to the pull request closing, in the serverless function, can run as expected.</p><h3>Set up Azure Serverless Function to Accept Webhook Post Request</h3><p>The first thing we'll need to do is set up an Azure Serverless function so that there is an HTTP endpoint available to accept the incoming webhook POST request that will be sent from GitHub any time an event associated with a pull request occurs. It is not required to use Azure Serverless Functions with GitHub webhooks, so you can exchange this with another technology like a Node.js server using express. All that is required is an HTTP endpoint, using Node.js, that can accept incoming post requests.</p><p>Microsoft provides documentation for a <a href="https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-typescript">quick start to create a function in Azure with TypeScript and Visual Studio code</a>. This steps in this guide will build off that documentation so it is required to set that up before proceeding.</p><h3>npm install @octokit/webhooks</h3><p>Once you have the HTTP trigger function setup and you are able to run it locally as indicated in the quick start, we can add the @octokit/webhooks into the package.json that was automatically generated in the functions project. To do this use Visual Studio Code to open a terminal window in the folder where the package.json file was generated for the functions project. Then run the command <kbd>npm install @octokit/webhooks --save</kbd>. This will add the @octokit/webhooks npm package into the node_modules folder for the project, so that it can be imported into function code.</p><h3>import @octokit/webhooks</h3><p>In the HTTP Trigger function that was created by following quick start guide, this will be called "HTTPExample" if you did not change it, we need to add code to utilize the @octokit/webhooks package that was just installed. You can delete the sample code provided for "HTTPExample" function file named "index.ts". Then go ahead and add the following code into the index.ts file:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> { Webhooks } <span class="hljs-keyword">from</span> <span class="hljs-string">"@octokit/webhooks"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-keyword">const</span> payload = req.body;

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = { <span class="hljs-attr">message</span>: <span class="hljs-string">"success"</span> };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>This is the starting code needed to utilize the @octokit/webhooks npm package <a href="https://www.npmjs.com/package/@octokit/webhooks#webhooksverify">verify</a> method. The code to do the verification hasn't been added, only the import statement that is on the second line of code. To use the verification method update the index.ts file to look like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> { Webhooks } <span class="hljs-keyword">from</span> <span class="hljs-string">"@octokit/webhooks"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-comment">// application/json post request body</span>
  <span class="hljs-keyword">const</span> payload = req.body;

  <span class="hljs-keyword">if</span> (
    payload.action != <span class="hljs-string">"closed"</span> ||
    payload.pull_request.base.ref != <span class="hljs-string">"main"</span> ||
    !payload.pull_request.merged_at ||
    !<span class="hljs-keyword">new</span> Webhooks({
      <span class="hljs-attr">secret</span>: process.env[<span class="hljs-string">"GitHubWebhookSecret"</span>],
    }).verify(payload, req.headers[<span class="hljs-string">"x-hub-signature-256"</span>])
  ) {
    <span class="hljs-comment">// this pull request is either:</span>
    <span class="hljs-comment">//  not closed,</span>
    <span class="hljs-comment">//  not referencing the main branch,</span>
    <span class="hljs-comment">//  not merged,</span>
    <span class="hljs-comment">//  or is not valid</span>
    <span class="hljs-comment">// so by returning we are not going to process the application logic below</span>
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">// your application logic goes here</span>

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = { <span class="hljs-attr">message</span>: <span class="hljs-string">"success"</span> };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p><strong>Note</strong>: The "GitHubWebhookSecret" is not directly included in the code. Since this is a secret value it is more secure to access this as an environment variable. To add an environment variable within an Azure Functions project, you can view the documentation for how to <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal">add an application setting with the Azure portal</a>. This value should be secret and not shared with anyone. In the upcoming steps we will add this to the GitHub repository webhook settings so that the @octokit/webhooks npm package can use this value to verify the request payload. If you are running your function locally you will also need to add the same "GitHubWebhookSecret" setting to the "local.settings.json" file that was automatically generated in the function project folder. Your local.settings.json file can include this value like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"IsEncrypted"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"Values"</span>: {
    <span class="hljs-attr">"AzureWebJobsStorage"</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">"FUNCTIONS_WORKER_RUNTIME"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"GitHubWebhookSecret"</span>: <span class="hljs-string">"&lt;YOUR-SECRET-VALUE&gt;"</span>
  },
  <span class="hljs-attr">"Host"</span>: {
    <span class="hljs-attr">"LocalHttpPort"</span>: <span class="hljs-number">7071</span>,
    <span class="hljs-attr">"CORS"</span>: <span class="hljs-string">"*"</span>,
    <span class="hljs-attr">"CORSCredentials"</span>: <span class="hljs-literal">false</span>
  }
}
</code></pre><p>With this code in place to secure the webhook, we can now be sure that any incoming requests are coming from GitHub's servers and meet the conditional criteria before processing further. Since the POST request from GitHub is sent when any event relating to a pull request occurs, the code above makes sure to only act on the incoming request if the pull request payload data indicates that the pull request is merged, closed, and from the main branch. This way if a pull request is opened or not associated to the main branch the webhook can ignore that request.</p><h3>Configure GitHub Webhook Settings</h3><p>Now that we can accept incoming POST requests with webhook payloads from GitHub we need to configure the repository settings to send the request when the pull request event occurs. To do this a GitHub repository will need to be created, if not existing already, and once created navigate to the "Settings" tab. Then in the secondary navigation for the repository settings there will be a nav item labelled "Webhooks". This is where we can configure the url for the webhook and the secret value that is used to verify the incoming request shown in the code above. You can click the button labelled "Add webhook" and GitHub will prompt you to enter your password to continue. Once you have entered your password you will see a screen like this:</p><p><img src="https://www.devextent.com/images/github-add-webhook.png" alt="GitHub Webhooks Add Webhook"></p><p>To get the value for the Payload URL field, we need to enter the url for the function we created earlier. At this point if you have not deployed your Azure Serverless Function application, you can do this to get the url or, in the next section, follow the steps to set up <a href="https://docs.github.com/en/developers/webhooks-and-events/configuring-your-server-to-receive-payloads#using-ngrok">ngrok</a> to enable testing the locally running functions application.</p><p>If you want to deploy to Azure you will find the url in the function app overview settings panel in the Azure portal. This is only the function base url so you will need to append the route of the function that was created. If you kept the default function name the entire Payload URL field value will look something like this:</p><pre><code class="hljs">https://functionapp1.azurewebsites.net/api/httptrigger1
</code></pre><p>After the Payload URL field, the next field is for the Content type of the request that we are expecting from GitHub, in this case our function is set up to accept:</p><pre><code class="hljs">application/json
</code></pre><p>so make sure to update this setting. In the following field for the Webhook Secret provide the secret value that was saved as an application setting in the function app settings within the Azure portal. This will also be the same as the "secret" property value that was added to the "local.settings.json" file, shown earlier, within the functions project folder. Next we'll need to update the events that trigger the webhook so select the radio button for "Let me select individual events" then make sure to deselect the checkbox for "Pushes" and only select the checkbox for "Pull requests".</p><p><img src="https://www.devextent.com/images/webhook-event-pull-requests-checkbox.png" alt="GitHub Webhooks Checkbox Pull Requests Event"></p><p>Then select Add webhook at the bottom and this will save the webhook configuration, and GitHub will automatically run a test request to make sure the webhook integration is working as expected.</p><h3>Use ngrok to Test GitHub Webhook with Azure Functions Locally</h3><p>If you don't want to deploy your Azure Severless Functions project to the Azure cloud while testing, you can use ngrok to test the GitHub webhook integration while running the function project locally. To do this <a href="https://ngrok.com/download">download ngrok</a>, and follow the installation instructions. Once it is setup, you can run the command <kbd>ngrok http 7071</kbd> and ngrok will provide a publicly available url that will forward the port your functions app is running on. If you changed the default port for the functions project to something different than localhost:7071 make sure to run the ngrok command with the port you are using. With ngrok running you should get a url that looks like this:</p><pre><code class="hljs">http://92832de0.ngrok.io
</code></pre><p>With that url, go back to the GitHub webhook settings page in the repository settings and update the Payload URL field to match, ensuring that you have appended the entire url for the function so it would look like this:</p><pre><code class="hljs">http://92832de0.ngrok.io/api/httptrigger1
</code></pre><p>Then run your function app locally inside of Visual Studio Code, and save the webhook settings in GitHub. This will send another test request and you will be able to see the request, from GitHub's servers, being processed in the console output logs of your function app running locally.</p><p>Moving back to the "index.ts" file in your function project you can now add the code you need to integrate with the GitHub Webhook and it will use the @octokit/webhooks npm package to verify the incoming request was signed with the secret value you provided. Anytime an event occurs matching the webhook settings criteria, GitHub will send a POST request, and the webhook integration will occur automatically and securely. For an example of what can be done with GitHub webhooks, checkout out how to <a href="https://www.devextent.com/jamstack-blog-serverless-comment-system/">build a serverless comment system for a jamstack blog</a>. Where you can find detailed instructions about how to setup a GitHub Webhook integration to provide a moderated commenting system for a blog without a database or servers.</p>]]></description></item><item><title>Generate a Universally Unique Identifier (uuid) with Node.js</title><pubDate>Mon, 15 Feb 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/npm-generate-unique-id/</link><guid isPermaLink="true">https://www.devextent.com/npm-generate-unique-id/</guid><description><![CDATA[<p>The uuid, or universally unique identifier, npm package is a secure way to generate cryptographically strong unique identifiers with Node.js that doesn't require a large amount of code. The <a href="https://www.npmjs.com/package/uuid">uuid npm package</a> has zero dependencies, and over thirty thousand packages depend on it, making it a safe choice when an id is needed that is guaranteed to be unique. It supports commonJS modules and also <a href="https://nodejs.org/api/esm.html">ECMAScript Modules</a>, making it a good cross-platform choice. Besides generating a unique id, the uuid npm package has other utility methods available in its API that can be useful when working with unique identifiers, to ensure that they are valid.</p><h3>npm install uuid</h3><p>We can create a sample node.js script to test out the functionality of the uuid npm package, but first make sure that <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> are installed. Then run the command <kbd>npm init</kbd> in a test project folder terminal window, and follow the prompts that are shown. This will create a package.json file, which will be used to install the uuid package. Creating a package.json file is not needed, if you are adding uuid to an existing project.</p><p>After creating the package.json file run the command <kbd>npm install uuid --save</kbd>, and the uuid npm package will be added to the node_modules folder inside the project folder. If you are using TypeScript you can also run the command <kbd>npm install @types/uuid</kbd> to install the type definitions for the uuid npm package. See how to <a href="https://www.devextent.com/npm-compile-typescript/">compile TypeScript with npm</a> for more information if you want to use the uuid npm package with TypeScript.</p><h3>Generate uuid with Node.js</h3><p>With the uuid npm package installed we can now import it into a Node.js script and use the functions provided. Create a new file named "index.js" and include the uuid npm package like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> uniqueId = uuidv4();
  <span class="hljs-built_in">console</span>.log(uniqueId);
}

main();
</code></pre><p>What this does is import the <a href="https://tools.ietf.org/html/rfc4122">RFC4122</a> version 4 uuid export using ECMAScript modules syntax. If you aren't using ES Modules, you can by following the steps to <a href="https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/">import and export ES Modules in Node.js</a>, or you can use commonJS modules with a different import syntax that looks like this:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> { <span class="hljs-attr">v4</span>: uuidv4 } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"uuid"</span>);
</code></pre><p>For the remainder of this example we will use the ES Module syntax.</p><p>To test out the Node.js script, we can add a package.json script to run it. In the "scripts" property of the package.json file add the following:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node index.js"</span>
  }
}
</code></pre><p>After adding the additional script run the command <kbd>npm run start</kbd>, and you should see a uuid output to the console that looks similar to this:</p><pre><code class="hljs">d160b410-e6a8-4cbb-92c2-068112187503
</code></pre><p>You can re-run the command and a new uuid will be generated each time.</p><h3>Validate uuid</h3><p>The uuid npm package can also validate uuid strings to test whether they are a valid uuid. To do this add some additional code to the index.js file we just created.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { validate <span class="hljs-keyword">as</span> uuidValidate } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> uniqueId = uuidv4();
  <span class="hljs-built_in">console</span>.log(uniqueId);

  <span class="hljs-keyword">const</span> isValid = uuidValidate(uniqueId);
  <span class="hljs-built_in">console</span>.log(isValid);
}

main();
</code></pre><p>Then run the <kbd>npm run start</kbd> command again and you will see the result of the uuidValidate method is output as true. If the value passed into the uuidValidate function is not a valid uuid the output will be false.</p><h3>Detect uuid RFC version</h3><p>After validation, if we have a valid uuid, the uuid npm package can also detect the version of a uuid string. Building off the index.js sample code add another import to access the uuid version function and test the uniqueId that is generated like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { validate <span class="hljs-keyword">as</span> uuidValidate } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { version <span class="hljs-keyword">as</span> uuidVersion } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> uniqueId = uuidv4();
  <span class="hljs-built_in">console</span>.log(uniqueId);

  <span class="hljs-keyword">const</span> isValid = uuidValidate(uniqueId);
  <span class="hljs-built_in">console</span>.log(isValid);

  <span class="hljs-keyword">const</span> version = uuidVersion(uniqueId);
  <span class="hljs-built_in">console</span>.log(version);
}

main();
</code></pre><p>Now when we run the index.js script we can see the version of the uniqueId that is generated is output as "4", matching the uuidv4 version we imported initially. If the uuidVersion function is not passed a valid uuid an error will be thrown, so it can be helpful to wrap this function in a try/catch block to capture any errors.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { validate <span class="hljs-keyword">as</span> uuidValidate } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { version <span class="hljs-keyword">as</span> uuidVersion } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> uniqueId = uuidv4();
  <span class="hljs-built_in">console</span>.log(uniqueId);

  <span class="hljs-keyword">const</span> isValid = uuidValidate(uniqueId);
  <span class="hljs-built_in">console</span>.log(isValid);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> version = uuidVersion(uniqueId);
    <span class="hljs-built_in">console</span>.log(version);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }
}

main();
</code></pre><p>This way any resulting error can be output to the console, or handled in a way that is best for the current usage.</p><h3>Generate NIL uuid</h3><p>If you need to generate a NIL uuid, or a uuid that is entirely zeros, the uuid npm package provides the NIL_UUID as an export. Adding it to the index.js sample script looks like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { validate <span class="hljs-keyword">as</span> uuidValidate } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { version <span class="hljs-keyword">as</span> uuidVersion } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
<span class="hljs-keyword">import</span> { NIL <span class="hljs-keyword">as</span> NIL_UUID } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> uniqueId = uuidv4();
  <span class="hljs-built_in">console</span>.log(uniqueId);

  <span class="hljs-keyword">const</span> isValid = uuidValidate(uniqueId);
  <span class="hljs-built_in">console</span>.log(isValid);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> version = uuidVersion(uniqueId);
    <span class="hljs-built_in">console</span>.log(version);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }

  <span class="hljs-built_in">console</span>.log(NIL_UUID);
}

main();
</code></pre><p>I haven't come across a use case where a NIL uuid is needed, but it is provided in the uuid npm package so I'm sure there are plenty.</p><p>The uuid npm package is a helpful utility to generate a unique id with Node.js that saves a lot of the potential headache from generating one from scratch, while also ensuring the value are unique, secure, and matching a specific RFC version.</p>]]></description></item><item><title>Import and Export ES Modules in Node.js using TypeScript with Babel Compilation</title><pubDate>Tue, 09 Feb 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/</link><guid isPermaLink="true">https://www.devextent.com/import-es-modules-in-nodejs-with-typescript-and-babel/</guid><description><![CDATA[<p>As of Node.js version 13.2.0 ECMAScript modules are now supported by default without adding an experimental flag. Although, using ES Modules without making the required configuration changes will result in the error "SyntaxError: Cannot use import statement outside a module". This is because Node.js, by default, is expecting the <a href="https://nodejs.org/docs/latest/api/modules.html">CommonJS module</a> format.</p><p>Using TypeScript in combination with ES Modules brings many added benefits. To use TypeScript with ES Modules, the TypeScript compiler configuration in <a href="https://www.typescriptlang.org/docs/handbook/tsconfig-json.html">tsconfig.json</a> can be updated to process code in ES Module format. Additionally, <a href="https://babeljs.io/">Babel</a> can be used for TypeScript compilation, and the TypeScript compiler will be used for type checking, as Babel can not type check TypeScript code. Once the TypeScript code is being compiled by Babel into JavaScript, retaining the ES Module format, the ES Modules can be exported, imported, and run with Node.js.</p><h3>package.json Type Module</h3><p>The first configuration change we can make, to use ES Modules in Node.js is configuring the package.json file to include the type module property value. To do this add the following code to the package.json file in your Node.js project:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>
}
</code></pre><p>If you are starting a new project you can run the command <kbd>npm init</kbd> in a terminal window, follow the prompts that follow, and a package.json file will be generated in the current project folder. Although, before doing so make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed. Once the package.json file is added to your project then add the extra configuration shown above as the <kbd>npm init</kbd> command does not generate a package.json file with this ES Module setting pre-configured.</p><h3>npm install</h3><p>We will also be using some additional npm packages to carry out the compilation and type checking processes.</p><ul><li><a href="https://www.npmjs.com/package/cross-env">cross-env</a></li><li><a href="https://www.npmjs.com/package/@babel/cli">@babel/cli</a></li><li><a href="https://www.npmjs.com/package/@babel/core">@babel/core</a></li><li><a href="https://www.npmjs.com/package/@babel/preset-env">@babel/preset-env</a></li><li><a href="https://www.npmjs.com/package/@babel/preset-typescript">@babel/preset-typescript</a></li><li><a href="https://www.npmjs.com/package/rimraf">rimraf</a></li><li><a href="https://www.npmjs.com/package/typescript">typescript</a></li></ul><p>Before proceeding run the command <kbd>npm install cross-env @babel/cli @babel/core @babel/preset-env @babel/preset-typescript rimraf typescript --save</kbd>. This will install the npm packages in the project "node_modules" folder and create a package-lock.json file. The npm packages are now available for usage in the project. Since we are using TypeScript, we can also run the command <kbd>npm install @types/node --save-dev</kbd> which will install the Node.js type definitions as a devDependency.</p><h3>Configure TypeScript compiler to use ES Module format</h3><p>Using ES Modules does not require the use of TypeScript, however the overhead of including TypeScript is minimal and including it provides many benefits such as static typing, which can enable code editors or an <a href="https://en.wikipedia.org/wiki/Integrated_development_environment">IDE</a> to offer more predictive assistance. You may have heard referred to as intellisense or <a href="https://en.wikipedia.org/wiki/Intelligent_code_completion">intelligent code completion</a>. In the same folder as the package.json add a new file named "tsconfig.json" containing this configuration:</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"allowSyntheticDefaultImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2019"</span>],
    <span class="hljs-attr">"noEmit"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*.ts"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules/**/*"</span>, <span class="hljs-string">"dist/**/*"</span>]
}
</code></pre><p>More info on tsconfig settings can be found in the <a href="https://www.typescriptlang.org/tsconfig">TSConfig reference</a> provided by Microsoft. The most important compiler option included is setting the "module" property to "esnext". This informs the TypeScript compiler to recognize source code in the ES Module format as well as retain the format when generating JavaScript code.</p><p>Since Babel will be configured to do the compilation of TypeScript into JavaScript the "noEmit" property is set to true, and what this does is allow for the use of the TypeScript compiler only to indicate when there are type checking errors. When configured this way the <a href="https://www.typescriptlang.org/docs/handbook/compiler-options.html">tsc compile command</a> will not generate any JavaScript code, but it will output any errors that would occur during compilation to the console. It is also recommended, when <a href="https://www.typescriptlang.org/docs/handbook/babel-with-typescript.html">using TypeScript with the Babel compiler</a>, to set the "allowSyntheticDefaultImports" and "isolatedModules" to true as this ensures that the TypeScript compiler will process source code similar to how the Babel compiler does. This way the type checking and compilation configurations are in sync, even though separate steps are responsible for each.</p><h3>Configure Babel to compile TypeScript into ES Modules</h3><p>With TypeScript configured, we can add the Babel configuration that enables TypeScript compilation with the Babel compiler. To do this create a new file in the same folder as the tsconfig.json file named ".babelrc.json" and add this configuration:</p><pre><code class="hljs">{
  <span class="hljs-attr">"presets"</span>: [
    [<span class="hljs-string">"@babel/preset-env"</span>, { <span class="hljs-attr">"modules"</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">"targets"</span>: { <span class="hljs-attr">"node"</span>: <span class="hljs-literal">true</span> } }],
    [<span class="hljs-string">"@babel/preset-typescript"</span>]
  ],
  <span class="hljs-attr">"ignore"</span>: [<span class="hljs-string">"node_modules"</span>],
  <span class="hljs-attr">"comments"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"minified"</span>: <span class="hljs-literal">true</span>
}
</code></pre><p>This will configure Babel to use the <a href="https://babeljs.io/docs/en/babel-preset-typescript">preset-typescript</a> and <a href="https://babeljs.io/docs/en/babel-preset-env">preset-env</a> when generating JavaScript code. The presets are executed in a bottom to top order, meaning that first Babel will compile the TypeScript into JavaScript and then on the resulting JavaScript code the preset-env configuration will be applied. This is where Babel is configured to use ES Modules as the "modules" setting is set to false, which is somewhat confusing because ES Modules are being used. It is necessary to set this to false otherwise Babel will use the default CommonJS module format for Node.js. Additionally the compilation target is set to Node so that Babel can apply code transforms that ensure the code will be able to run in the LTS version of Node.js.</p><p>In this example there are two extra babel settings included that instruct the Babel compiler to remove any comments in the source code and minify the JavaScript output. These can be removed if not desired for your use case, however this is beneficial for using in production to minimize code size.</p><h3>Export ES Module</h3><p>Now we can add some sample TypeScript code to test out the configuration changes.</p><p>In the same project folder create a new folders named "src", so that the file structure matches the "include" pattern in the tsconfig.json file. Then in the "src" folder create a new file named "helpers.ts" and place the following code in it:</p><pre><code class="hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">log</span>(<span class="hljs-params">value: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-built_in">console</span>.log(value);
}

<span class="hljs-keyword">export</span> { log };
</code></pre><p>This code is only logging the value that is passed in to the console, and is not really representative of actual code that would be used, but it allows for the demonstration of using ES Modules with TypeScript and Babel. The export of the "log" function is the key item to notice about this code, as this is all that is needed to export an ES Module. Now we can create another file to import the "log" helper function module.</p><h3>Import ES Module</h3><p>In the same "src" folder create a new file named "index.ts" this will be the main entry point for our ES Module code. Once that file is created add in this TypeScript code to import the helper function that was created in the previous step.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { log } <span class="hljs-keyword">from</span> <span class="hljs-string">"./helpers.js"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  log(<span class="hljs-string">"testing es modules"</span>);
}

main();
</code></pre><p>Similar to the helpers.ts file the index.ts files is mainly for demonstrating ES Module import syntax. It imports the helper function and then the main function is called to execute the "log" function. Although it is important to note that the file imported must end with a ".js" file extension rather than a ".ts" file extension. This is because when the code is eventually compiled the ES Module code will be a JavaScript file. Make sure that anytime a module is imported from a separate file the path is relative to the current file and the extension is set to ".js", otherwise both the TypeScript compiler and Babel compiler will not be able to resolve the file location.</p><h3>Run ES Modules in Node.js</h3><p>At this point the source code is configured to run with ES Modules, so we can now look at how to compile the code and run it with Node.js. To do this we'll need to add six additional scripts to the "scripts" property in the package.json file.</p><p>In the package.json "scripts" property add the following:</p><pre><code class="hljs">{
  <span class="hljs-attr">"clean"</span>: <span class="hljs-string">"rimraf dist"</span>,
  <span class="hljs-attr">"compile"</span>: <span class="hljs-string">"cross-env-shell babel src -d dist --source-maps --extensions '.ts'"</span>,
  <span class="hljs-attr">"build"</span>: <span class="hljs-string">"npm run clean &amp;&amp; npm run compile"</span>,
  <span class="hljs-attr">"typecheck"</span>: <span class="hljs-string">"tsc --p ."</span>,
  <span class="hljs-attr">"build-typecheck"</span>: <span class="hljs-string">"npm run typecheck &amp;&amp; npm run build"</span>,
  <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run build-typecheck &amp;&amp; node ./dist/index.js"</span>
}
</code></pre><p>The "clean" script will ensure that prior to the compilation, the output directory "dist" will be deleted. This way the latest code will copied into an empty folder.</p><p>The "compile" script is where the cross-env package is used to run the babel compilation command. This babel compilation command specifies that the source files will be located in the "src" folder and when compilation is complete the JavaScript output will be copied to a folder named "dist". The flags that are passed in indicate that source maps should be generated for debugging purposes and also the "--extensions" flag is required so that Babel will look for files ending with the ".ts" extension.</p><p>To use the "clean" and "compile" script sequentially they are combined in a new script named "build", which can be run using the command <kbd>npm run build</kbd>. This will remove the old files from the "dist" folder and compile the TypeScript source code with Babel, however no typechecking errors will be indicated and Babel may fail to compile the code if there are errors present.</p><p>To resolve this an additional script "typecheck" is included that will pass the TypeScript source code through the TypeScript compiler, and if there are errors present, they will be output to the console. Since the tsconfig.json settings include the "noEmit" property the typecheck command won't output any JavaScript code.</p><p>The command that will be most commonly used is the "build-typecheck" command, which can be used by running <kbd>npm run build-typecheck</kbd>. This will sequentially run the "typecheck" command and then if there are no errors present as a result of the TypeScript compilation with the TypeScript compiler, the "build" command will be executed, invoking the Babel compiler and generating JavaScript code that can be run by Node.js in ES Module format.</p><p>Since the JavaScript code is being output to a folder named "dist" the "main" property in the package.json should be changed to:</p><pre><code class="hljs">{
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"./dist/index.js"</span>
}
</code></pre><p>To run the compiled JavaScript code, execute the command <kbd>npm run start</kbd> and this will carry out the type checking and compilation steps as well as run the index.js file with Node.js. If everything is setup and working as expected you should see the value included in the "main" function - "testing es modules" output to the console. Now you can use this configuration to create node modules that are statically typed and run in Node.js using the ES Module format.</p>]]></description></item><item><title>Use Azure Table Storage with Azure Serverless Functions and Node.js</title><pubDate>Mon, 01 Feb 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/table-storage-serverless-api-typescript-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/table-storage-serverless-api-typescript-nodejs/</guid><description><![CDATA[<p>Before developing with <a href="https://azure.microsoft.com/en-us/services/functions/">Azure Serverless Functions</a> and <a href="https://azure.microsoft.com/en-us/services/storage/tables/">Azure Table storage</a> locally, there are some tools required to emulate <a href="https://azure.microsoft.com/en-us/services/storage/">Azure Storage</a> and provide a run-time environment for the Node.js serverless functions. Please make sure the prerequisites are set up before running the example code that follows.</p><h3>Setup Azure Storage Emulator</h3><p>In order to save on development costs, instead of creating cloud resources, we can install the <a href="https://docs.microsoft.com/en-us/azure/storage/common/storage-use-emulator">Azure Storage Emulator for development and testing</a>. If you aren't using windows, <a href="https://github.com/azure/azurite">Azurite</a> is an open source Azure storage API compatible server, and it is recommended by Microsoft to use. Otherwise, after installing, Windows users can search in the start menu for "azure storage emulator" and press enter to start the emulator. This should open a cmd window that will indicate the emulator is running, and some helpful commands. The cmd window can be closed and the emulator will continue to run.</p><h3>Install Azure Storage Explorer</h3><p>Next we'll need to download <a href="https://azure.microsoft.com/en-us/features/storage-explorer/">Azure Storage Explorer</a> to interact with the emulated storage environment. This application is available for Windows, Mac, and Linux machines. After installing go ahead and start the Azure Storage Explorer, and in the left hand column navigator find the dropdown section labelled "Local &amp; Attached" and then within that section find the secondary dropdown "Storage Accounts" and within the tertiary dropdown "(Emulator - Default Ports)" is where the resources, that we have not yet created, will be displayed. Here you can see three additional dropdown sections:</p><ul><li>Blob containers</li><li>Queues</li><li>Tables</li></ul><p>Our focus will be on the "Tables" section, which should be empty since no tables have been programmatically created yet.</p><p><img src="https://www.devextent.com/images/storage-explorer-navigator.png" alt="Azure Storage Explorer resource navigator sidebar"></p><h3>Setup Azure Functions for Local Development</h3><p>Now that we have the storage emulator and storage explorer configured we can download the <a href="https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-typescript">Azure Functions extension for Visual Studio Code</a>. If you don't have Visual Studio Code you can <a href="https://code.visualstudio.com/download">download it</a>, and then follow the instructions to configure the local project. You don't need to follow the naming convention indicated in the documentation, but what is important is that there is a package.json created in the functions project. The package.json file is created automatically and allows us to include the npm package provided by Microsoft to interact with Azure Storage.</p><h3>npm Install azure-storage</h3><p>In the same folder as the package.json that was created, run the command <kbd>npm install azure-storage --save</kbd> and then run the command <kbd>npm install @types/node --save-dev</kbd> to install the type definitions for Node.js. This will install the <a href="https://www.npmjs.com/package/azure-storage">azure-storage npm package</a> to the local functions project so that we can import it in our code.</p><p>Congratulations, you made it through the setup configuration!</p><h3>Http Trigger Serverless Function</h3><p>Now we can write the code to use Azure Table Storage with Azure Serverless Typescript Functions. To begin find the file "index.ts" in the <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=javascript">HTTP trigger function</a> that was created earlier (if using the default it will be called HttpTrigger1). Right now there is sample code in that function that can be deleted, and the code below can be added.</p><p>The function should now look like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> azureStorage <span class="hljs-keyword">from</span> <span class="hljs-string">"azure-storage"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  <span class="hljs-comment">// set content type for all responses</span>
  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"POST"</span>) {
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"GET"</span>) {
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"PUT"</span>) {
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"DELETE"</span>) {
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// request method does not match</span>
    context.res!.status = <span class="hljs-number">500</span>;
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><h3>Programmatically Create Table If Not Exists</h3><p>Before we can retrieve data from Azure Storage we need to insert data by using an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">HTTP POST</a> request, additionally a table must be created to store the data. To ensure there is a table to store data we can programmatically create the entity table if it does not exist with the azure storage npm package, at the time of the POST request. In order to connect to the storage emulator a connection string is required, which can be stored as an environment variable to be passed into the Node.js serverless functions process. To do this add the default local connection string to the file "local.settings.json" that is in the same folder as the HttpTrigger1 function. Additionally we want to add a "Host" configuration to permit <a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">CORS</a> requests and set the default port that the functions will run on.</p><p>The local.settings.json file should now look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"IsEncrypted"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"Values"</span>: {
    <span class="hljs-attr">"AzureWebJobsStorage"</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">"FUNCTIONS_WORKER_RUNTIME"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"TableStorageConnection"</span>: <span class="hljs-string">"UseDevelopmentStorage=true"</span>
  },
  <span class="hljs-attr">"Host"</span>: {
    <span class="hljs-attr">"LocalHttpPort"</span>: <span class="hljs-number">7071</span>,
    <span class="hljs-attr">"CORS"</span>: <span class="hljs-string">"*"</span>,
    <span class="hljs-attr">"CORSCredentials"</span>: <span class="hljs-literal">false</span>
  }
}
</code></pre><p>Now we can use the "TableStorageConnection" environment variable to create a table.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> azureStorage <span class="hljs-keyword">from</span> <span class="hljs-string">"azure-storage"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  <span class="hljs-keyword">const</span> tableService = azureStorage.createTableService(
    process.env[<span class="hljs-string">"TableStorageConnection"</span>]
  );

  <span class="hljs-keyword">const</span> createTableIfNotExists = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span></span>) =&gt;</span>
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      tableService.createTableIfNotExists(tableName, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (error) {
          reject(error);
        } <span class="hljs-keyword">else</span> {
          resolve(result);
        }
      });
    });

  <span class="hljs-comment">// set content type for all responses</span>
  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"POST"</span>) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> createTableIfNotExists(<span class="hljs-string">"TestTable"</span>);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred."</span>,
      };
    }
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"GET"</span>) {
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"PUT"</span>) {
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"DELETE"</span>) {
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// request method does not match</span>
    context.res!.status = <span class="hljs-number">500</span>;
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>Once that table service is initialized the "tableService.createTableIfNotExists" function can be used. This function by default, uses a callback function to obtain the result. Instead of using the callback, the function is wrapped in a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</a> which can asynchronously resolve the callback function or return an error. Following that the promise is awaited inside a conditional branch that will only execute if the incoming request is a POST request.</p><p>The function can now create a new table if it doesn't exist named "TestTable" on any incoming POST request. To test this out run the function (in Visual Studio Code press F5), and then <a href="https://www.postman.com/downloads/">download Postman</a> to mock requests. Copy the url provided in the terminal window where the function is running, if you kept the default configuration this url will be "http://localhost:7071/api/HttpTrigger1", and change the request method in Postman from GET to POST and send the request. In the response body displayed in Postman all that will show is the number "1", however if we use the Azure Storage Explorer to view the emulator tables we can see that the "TestTable" was successfully created. You may need to select "refresh all" in the storage explorer to see the new table.</p><h3>Insert Azure Table Storage Entity</h3><p>Now that the table will be programmatically created if it doesn't exist, we can add a request body to the POST request that is being sent in Postman. This data will be parsed with the <a href="https://nodejs.org/api/querystring.html">querystring</a> module included with Node.js and then a storage entity can be generated from the incoming data. Once the storage entity is generated it can then be saved to the storage table.</p><p>To facilitate the saving of the table data we can use the uuid npm package, to install run the command <kbd>npm install uuid --save</kbd> and then install the typescript type definitions with the command <kbd>npm install @types/uuid --save-dev</kbd>.</p><p>Add the following import statements to the index.ts file:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;
</code></pre><p>Then add the following inside the POST method conditional branch:</p><pre><code class="hljs"><span class="hljs-comment">//parses www-form-urlencoded request body</span>
<span class="hljs-keyword">const</span> body = querystring.parse(req.body) <span class="hljs-keyword">as</span> {
  <span class="hljs-attr">firstName</span>: <span class="hljs-built_in">string</span>;
  lastName: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">if</span> (!(body &amp;&amp; body.firstName &amp;&amp; body.lastName &amp;&amp; !<span class="hljs-built_in">isNaN</span>(<span class="hljs-built_in">Number</span>(body.age)))) {
  context.res!.status = <span class="hljs-number">400</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"The data is invalid."</span>,
  };
  <span class="hljs-keyword">return</span>;
}

<span class="hljs-comment">// inform table storage of row types</span>
<span class="hljs-keyword">const</span> entityGenerator = azureStorage.TableUtilities.entityGenerator;

<span class="hljs-comment">// storing data within the same storage partition</span>
<span class="hljs-comment">// partition key and row key combo must be unique but also type string</span>
<span class="hljs-keyword">const</span> entityData = {
  <span class="hljs-attr">PartitionKey</span>: entityGenerator.String(<span class="hljs-string">"TestPartition"</span>),
  <span class="hljs-attr">RowKey</span>: entityGenerator.String(uuidv4()),
  <span class="hljs-attr">firstName</span>: entityGenerator.String(body.firstName),
  <span class="hljs-attr">lastName</span>: entityGenerator.String(body.lastName),
  <span class="hljs-attr">age</span>: entityGenerator.Int32(<span class="hljs-built_in">Number</span>(body.age)),
};

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> tableName = <span class="hljs-string">"TestTable"</span>;

  <span class="hljs-keyword">await</span> createTableIfNotExists(tableName);

  <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> insertEntity(tableName, entityData);

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Data is saved."</span>,
    <span class="hljs-attr">data</span>: entity,
  };
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-built_in">console</span>.log(error);

  context.res!.status = <span class="hljs-number">400</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred."</span>,
  };
}
</code></pre><p><strong>Note:</strong> Azure Table Storage requires both the partition key and the row key value to be present on storage entities and it also enforces that the type of these columns is a string. The "RowKey" property is utilizing the uuid package that was installed to guarantee that the partition key and row key combination is unique regardless of the other entity data. It's also worth noting that the entity generator isn't required and Azure Table Storage will default to a type of string if the entity row type is not specified.</p><p>You will notice that there is no function declared yet with the name "insertEntity". We can add that helper function below the "createTableIfNotExists" function.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> insertEntity = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span>, entity: {}</span>) =&gt;</span>
  <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    tableService.insertEntity(tableName, entity, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        reject(error);
      } <span class="hljs-keyword">else</span> {
        resolve(result);
      }
    });
  });
</code></pre><p>After adding the code to save the table storage entity run the serverless functions again with visual studio code, and submit a post request containing sample data with Postman.</p><p>Here is what the request should look like in Postman:</p><p><img src="https://www.devextent.com/images/table-storage-postman-insert-data.png" alt="Postman request to insert table storage data entity"></p><p>Checking with the Azure Storage Explorer, inside of the "TestTable" there should be one entity, and now we can add code to retrieve this data entity using the partition key and row key values that are saved.</p><h3>Retrieve Azure Storage Entity</h3><p>In order to retrieve the Azure Storage entity we will need to add a second helper function to the index.ts file inside the HttpTrigger1 serverless function. This helper function will allow us to retrieve storage entities using the partition key and the row key. Below the "insertEntity" function declaration add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> retrieveEntity = <span class="hljs-function">(<span class="hljs-params">
  tableName: <span class="hljs-built_in">string</span>,
  partitionKey: <span class="hljs-built_in">string</span>,
  rowKey: <span class="hljs-built_in">string</span>
</span>) =&gt;</span>
  <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    tableService.retrieveEntity(
      tableName,
      partitionKey,
      rowKey,
      <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (error) {
          reject(error);
        } <span class="hljs-keyword">else</span> {
          resolve(result);
        }
      }
    );
  });
</code></pre><p>Then the "retrieveEntity" helper function can be called in the conditional branch that will execute on incoming GET requests, however we will need a way to pass the row key value to the function from the incoming request data. To do this we can <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=javascript#customize-the-http-endpoint">customize the http endpoint</a> using the functions.json file that is in the HttpTrigger1 function (the same folder as index.ts). In that file add a new key to the first object in the "bindings" array.</p><p>The functions.json file should look similar to this :</p><pre><code class="hljs">{
  <span class="hljs-attr">"bindings"</span>: [
    {
      <span class="hljs-attr">"authLevel"</span>: <span class="hljs-string">"anonymous"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"httpTrigger"</span>,
      <span class="hljs-attr">"direction"</span>: <span class="hljs-string">"in"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"req"</span>,
      <span class="hljs-attr">"methods"</span>: [<span class="hljs-string">"get"</span>, <span class="hljs-string">"post"</span>],
      <span class="hljs-attr">"route"</span>: <span class="hljs-string">"HttpTrigger1/{rowKey:guid?}"</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"http"</span>,
      <span class="hljs-attr">"direction"</span>: <span class="hljs-string">"out"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"res"</span>
    }
  ],
  <span class="hljs-attr">"scriptFile"</span>: <span class="hljs-string">"../dist/HttpTrigger1/index.js"</span>
}
</code></pre><p>In the route parameter that is being added the pattern to match the row key in the request is specified. The row key will be of type GUID and is optional since post requests will not have a row key to specify. We can now use the retrieve entity function in combination with the request parameter to query Azure Table Storage for the entity data.</p><p>Add this code into the GET request method branch to retrieve and return the data:</p><pre><code class="hljs"><span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> retrieveEntity(
    <span class="hljs-string">"TestTable"</span>,
    <span class="hljs-string">"TestPartition"</span>,
    context.bindingData.rowKey
  );

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Data retrieved."</span>,
    <span class="hljs-attr">data</span>: entity,
  };
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-built_in">console</span>.log(error);
  context.res!.status = <span class="hljs-number">400</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred"</span>,
  };
}
</code></pre><p>Then in Postman change the request method to GET and copy the entity row key from table storage so that the url in Postman looks similar to</p><pre><code class="hljs">http://localhost:7071/api/HttpTrigger1/99baf118-fb0b-495e-b839-432264ff6aaa
</code></pre><p>The row key will be different in your case since it is automatically generated for each entity, so make sure to change that to the entity row key saved to your local table storage. In the response data from postman you should see the following data returned:</p><pre><code class="hljs">{
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Data retrieved."</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"PartitionKey"</span>: {
      <span class="hljs-attr">"$"</span>: <span class="hljs-string">"Edm.String"</span>,
      <span class="hljs-attr">"_"</span>: <span class="hljs-string">"TestPartition"</span>
    },
    <span class="hljs-attr">"RowKey"</span>: {
      <span class="hljs-attr">"$"</span>: <span class="hljs-string">"Edm.String"</span>,
      <span class="hljs-attr">"_"</span>: <span class="hljs-string">"99baf118-fb0b-495e-b839-432264ff6aaa"</span>
    },
    <span class="hljs-attr">"Timestamp"</span>: {
      <span class="hljs-attr">"$"</span>: <span class="hljs-string">"Edm.DateTime"</span>,
      <span class="hljs-attr">"_"</span>: <span class="hljs-string">"2021-01-30T20:51:49.323Z"</span>
    },
    <span class="hljs-attr">"firstName"</span>: {
      <span class="hljs-attr">"_"</span>: <span class="hljs-string">"test first"</span>
    },
    <span class="hljs-attr">"lastName"</span>: {
      <span class="hljs-attr">"_"</span>: <span class="hljs-string">"test last"</span>
    },
    <span class="hljs-attr">"age"</span>: {
      <span class="hljs-attr">"_"</span>: <span class="hljs-number">99</span>
    },
    <span class="hljs-attr">".metadata"</span>: {
      <span class="hljs-attr">"metadata"</span>: <span class="hljs-string">"http://127.0.0.1:10002/devstoreaccount1/$metadata#TestTable/@Element"</span>,
      <span class="hljs-attr">"etag"</span>: <span class="hljs-string">"W/\"datetime'2021-01-30T20%3A51%3A49.323Z'\""</span>
    }
  }
}
</code></pre><p>In the data property of the response, each of the table storage columns is returned as an object containing two properties, one indicating the table storage data type and the other is the value of the property. There is also an additional metadata field included in the response that provides extra info about the response from Azure Table Storage, or in this case the Azure Storage Emulator.</p><p>Azure table storage entities can now be inserted and retrieved, but it is also useful to be able to update an entity that has been previously saved. To do this we can add the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT">PUT</a> request method to the "methods" property of the first object in the "bindings" array located in the functions.json file. Update the "methods" property to look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"methods"</span>: [<span class="hljs-string">"get"</span>, <span class="hljs-string">"post"</span>, <span class="hljs-string">"put"</span>, <span class="hljs-string">"delete"</span>]
}
</code></pre><p>The code for the delete method is going to be added later, so that string value has also been added to the array at this time.</p><h3>Update Azure Storage Entity</h3><p>After permitting PUT requests in the functions.json add this code to the PUT method conditional branch:</p><pre><code class="hljs"><span class="hljs-comment">//parses www-form-urlencoded request body</span>
<span class="hljs-keyword">const</span> body = querystring.parse(req.body) <span class="hljs-keyword">as</span> {
  <span class="hljs-attr">rowKey</span>: <span class="hljs-built_in">string</span>;
  firstName: <span class="hljs-built_in">string</span>;
  lastName: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">string</span>;
};

<span class="hljs-comment">// inform table storage of row types</span>
<span class="hljs-keyword">const</span> entityGenerator = azureStorage.TableUtilities.entityGenerator;

<span class="hljs-comment">// use request body data to maintain row key for entity</span>
<span class="hljs-keyword">const</span> entityData = {
  <span class="hljs-attr">PartitionKey</span>: entityGenerator.String(<span class="hljs-string">"TestPartition"</span>),
  <span class="hljs-attr">RowKey</span>: entityGenerator.String(body.rowKey),
  <span class="hljs-attr">firstName</span>: entityGenerator.String(body.firstName),
  <span class="hljs-attr">lastName</span>: entityGenerator.String(body.lastName),
  <span class="hljs-attr">age</span>: entityGenerator.Int32(<span class="hljs-built_in">Number</span>(body.age)),
};

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> updateEntity(<span class="hljs-string">"TestTable"</span>, entityData);

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Data is updated."</span>,
    <span class="hljs-attr">data</span>: entity,
  };
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-built_in">console</span>.log(error);
  context.res!.status = <span class="hljs-number">400</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred"</span>,
  };
}
</code></pre><p>A third helper function is also needed, shown as "updateEntity" so it can be added below the "retrieveEntity" helper function, above the request method conditional branches:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> updateEntity = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span>, entity: {}</span>) =&gt;</span>
  <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    tableService.replaceEntity(tableName, entity, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        reject(error);
      } <span class="hljs-keyword">else</span> {
        resolve(result);
      }
    });
  });
</code></pre><p>The "updateEntity" function takes two parameters one being the table name and the other is the updated entity. The partition key and the row key of the entity must match an existing partition/row key combination, or table storage will return an error. If desired there is a function provided by the azure-storage npm package named "insertOrReplaceEntity" which, as the name indicates can either update existing entities or create a new one if one does not exist. In this example the entity already exists so only the "replaceEntity" function is needed.</p><p>The PUT request method branch to update an existing entity is almost the same as the POST method branch to insert a new storage entity. The url is the same for both, and the main difference is that the "rowKey" is included in the request body so that the appropriate entity can have it's data updated. You can try it out by changing one of the fields in the request body to a different value and then check in the storage explorer to confirm the entity that was previously inserted and retrieved has the matching table column value updated.</p><h3>Delete Azure Storage Entity</h3><p>Much like the GET request method branch the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE">DELETE</a> request method does not contain a request body, instead the row key will be passed in the request as a parameter, and like the examples above we can add a fourth helper function to carry out the deletion.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> deleteEntity = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span>, entity: {}</span>) =&gt;</span>
  <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
    tableService.deleteEntity(tableName, entity, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        reject(error);
      } <span class="hljs-keyword">else</span> {
        resolve(result);
      }
    });
  });
</code></pre><p>Then use the "deleteEntity" function in the DELETE request method branch by adding this code:</p><pre><code class="hljs"><span class="hljs-keyword">try</span> {
  <span class="hljs-comment">// inform table storage of row types</span>
  <span class="hljs-keyword">const</span> entityGenerator = azureStorage.TableUtilities.entityGenerator;

  <span class="hljs-comment">// use request body data to maintain row key for entity</span>
  <span class="hljs-keyword">const</span> entityData = {
    <span class="hljs-attr">PartitionKey</span>: entityGenerator.String(<span class="hljs-string">"TestPartition"</span>),
    <span class="hljs-attr">RowKey</span>: entityGenerator.String(context.bindingData.rowKey),
  };

  <span class="hljs-keyword">const</span> statusMessage = <span class="hljs-keyword">await</span> deleteEntity(<span class="hljs-string">"TestTable"</span>, entityData);

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Data deleted."</span>,
    <span class="hljs-attr">data</span>: statusMessage,
  };
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-built_in">console</span>.log(error);
  context.res!.status = <span class="hljs-number">400</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred"</span>,
  };
}
</code></pre><p>To test this copy the row key value from the storage explorer for the entity previously saved and use the same url from the GET request method example in Postman, but change the request method to DELETE. Then execute the delete request with Postman and in the response section a success message will be displayed:</p><pre><code class="hljs">{
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Data deleted."</span>,
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"isSuccessful"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"statusCode"</span>: <span class="hljs-number">204</span>,
    <span class="hljs-attr">"body"</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">"headers"</span>: {
      <span class="hljs-attr">"cache-control"</span>: <span class="hljs-string">"no-cache"</span>,
      <span class="hljs-attr">"content-length"</span>: <span class="hljs-string">"0"</span>,
      <span class="hljs-attr">"server"</span>: <span class="hljs-string">"Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0"</span>,
      <span class="hljs-attr">"x-ms-request-id"</span>: <span class="hljs-string">"3c378130-7a6d-4652-9022-d02320d29c05"</span>,
      <span class="hljs-attr">"x-ms-version"</span>: <span class="hljs-string">"2018-03-28"</span>,
      <span class="hljs-attr">"x-content-type-options"</span>: <span class="hljs-string">"nosniff"</span>,
      <span class="hljs-attr">"date"</span>: <span class="hljs-string">"Sun, 31 Jan 2021 21:23:06 GMT"</span>
    }
  }
}
</code></pre><p>The response status from Azure Table Storage is <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204">204 No Content</a>, since there is no longer an entity saved in the table. We can verify the entity was deleted by refreshing the table in the storage explorer. The response items shown in the "statusMessage" variable, is the response from Azure Table Storage, and it is being included in the response back from the serverless function to show the consumer of the serverless function API that the delete request to Azure Storage was successful. If the delete request failed the status message would indicate that by setting the "isSuccessful" property value to false.</p><p>Here is the complete function file with all code include:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> azureStorage <span class="hljs-keyword">from</span> <span class="hljs-string">"azure-storage"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> { v4 <span class="hljs-keyword">as</span> uuidv4 } <span class="hljs-keyword">from</span> <span class="hljs-string">"uuid"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  <span class="hljs-keyword">const</span> tableService = azureStorage.createTableService(
    process.env[<span class="hljs-string">"TableStorageConnection"</span>]
  );

  <span class="hljs-keyword">const</span> createTableIfNotExists = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span></span>) =&gt;</span>
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      tableService.createTableIfNotExists(tableName, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (error) {
          reject(error);
        } <span class="hljs-keyword">else</span> {
          resolve(result);
        }
      });
    });

  <span class="hljs-keyword">const</span> insertEntity = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span>, entity: {}</span>) =&gt;</span>
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      tableService.insertEntity(tableName, entity, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (error) {
          reject(error);
        } <span class="hljs-keyword">else</span> {
          resolve(result);
        }
      });
    });

  <span class="hljs-keyword">const</span> retrieveEntity = <span class="hljs-function">(<span class="hljs-params">
    tableName: <span class="hljs-built_in">string</span>,
    partitionKey: <span class="hljs-built_in">string</span>,
    rowKey: <span class="hljs-built_in">string</span>
  </span>) =&gt;</span>
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      tableService.retrieveEntity(
        tableName,
        partitionKey,
        rowKey,
        <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
          <span class="hljs-keyword">if</span> (error) {
            reject(error);
          } <span class="hljs-keyword">else</span> {
            resolve(result);
          }
        }
      );
    });

  <span class="hljs-keyword">const</span> updateEntity = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span>, entity: {}</span>) =&gt;</span>
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      tableService.replaceEntity(tableName, entity, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (error) {
          reject(error);
        } <span class="hljs-keyword">else</span> {
          resolve(result);
        }
      });
    });

  <span class="hljs-keyword">const</span> deleteEntity = <span class="hljs-function">(<span class="hljs-params">tableName: <span class="hljs-built_in">string</span>, entity: {}</span>) =&gt;</span>
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      tableService.deleteEntity(tableName, entity, <span class="hljs-function">(<span class="hljs-params">error, result</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (error) {
          reject(error);
        } <span class="hljs-keyword">else</span> {
          resolve(result);
        }
      });
    });

  <span class="hljs-comment">// set content type for all responses</span>
  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"POST"</span>) {
    <span class="hljs-comment">//parses www-form-urlencoded request body</span>
    <span class="hljs-keyword">const</span> body = querystring.parse(req.body) <span class="hljs-keyword">as</span> {
      <span class="hljs-attr">firstName</span>: <span class="hljs-built_in">string</span>;
      lastName: <span class="hljs-built_in">string</span>;
      age: <span class="hljs-built_in">string</span>;
    };

    <span class="hljs-keyword">if</span> (
      !(body &amp;&amp; body.firstName &amp;&amp; body.lastName &amp;&amp; !<span class="hljs-built_in">isNaN</span>(<span class="hljs-built_in">Number</span>(body.age)))
    ) {
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"The data is invalid."</span>,
      };
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">// inform table storage of row types</span>
    <span class="hljs-keyword">const</span> entityGenerator = azureStorage.TableUtilities.entityGenerator;

    <span class="hljs-comment">// storing data within the same storage partition</span>
    <span class="hljs-comment">// partition key and row key combo must be unique but also type string</span>
    <span class="hljs-keyword">const</span> entityData = {
      <span class="hljs-attr">PartitionKey</span>: entityGenerator.String(<span class="hljs-string">"TestPartition"</span>),
      <span class="hljs-attr">RowKey</span>: entityGenerator.String(uuidv4()),
      <span class="hljs-attr">firstName</span>: entityGenerator.String(body.firstName),
      <span class="hljs-attr">lastName</span>: entityGenerator.String(body.lastName),
      <span class="hljs-attr">age</span>: entityGenerator.Int32(<span class="hljs-built_in">Number</span>(body.age)),
    };

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> tableName = <span class="hljs-string">"TestTable"</span>;

      <span class="hljs-keyword">await</span> createTableIfNotExists(tableName);

      <span class="hljs-keyword">await</span> insertEntity(tableName, entityData);

      context.res!.status = <span class="hljs-number">200</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"Data is saved."</span>,
        <span class="hljs-attr">data</span>: entityData,
      };
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred."</span>,
      };
    }
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"GET"</span>) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> retrieveEntity(
        <span class="hljs-string">"TestTable"</span>,
        <span class="hljs-string">"TestPartition"</span>,
        context.bindingData.rowKey
      );
      context.res!.status = <span class="hljs-number">200</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"Data retrieved."</span>,
        <span class="hljs-attr">data</span>: entity,
      };
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred"</span>,
      };
    }
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"PUT"</span>) {
    <span class="hljs-comment">//parses www-form-urlencoded request body</span>
    <span class="hljs-keyword">const</span> body = querystring.parse(req.body) <span class="hljs-keyword">as</span> {
      <span class="hljs-attr">rowKey</span>: <span class="hljs-built_in">string</span>;
      firstName: <span class="hljs-built_in">string</span>;
      lastName: <span class="hljs-built_in">string</span>;
      age: <span class="hljs-built_in">string</span>;
    };

    <span class="hljs-comment">// inform table storage of row types</span>
    <span class="hljs-keyword">const</span> entityGenerator = azureStorage.TableUtilities.entityGenerator;

    <span class="hljs-comment">// use request body data to maintain row key for entity</span>
    <span class="hljs-keyword">const</span> entityData = {
      <span class="hljs-attr">PartitionKey</span>: entityGenerator.String(<span class="hljs-string">"TestPartition"</span>),
      <span class="hljs-attr">RowKey</span>: entityGenerator.String(body.rowKey),
      <span class="hljs-attr">firstName</span>: entityGenerator.String(body.firstName),
      <span class="hljs-attr">lastName</span>: entityGenerator.String(body.lastName),
      <span class="hljs-attr">age</span>: entityGenerator.Int32(<span class="hljs-built_in">Number</span>(body.age)),
    };

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> entity = <span class="hljs-keyword">await</span> updateEntity(<span class="hljs-string">"TestTable"</span>, entityData);
      context.res!.status = <span class="hljs-number">200</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"Data is updated."</span>,
        <span class="hljs-attr">data</span>: entity,
      };
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred"</span>,
      };
    }
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.method == <span class="hljs-string">"DELETE"</span>) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// inform table storage of row types</span>
      <span class="hljs-keyword">const</span> entityGenerator = azureStorage.TableUtilities.entityGenerator;

      <span class="hljs-comment">// use request body data to maintain row key for entity</span>
      <span class="hljs-keyword">const</span> entityData = {
        <span class="hljs-attr">PartitionKey</span>: entityGenerator.String(<span class="hljs-string">"TestPartition"</span>),
        <span class="hljs-attr">RowKey</span>: entityGenerator.String(context.bindingData.rowKey),
      };

      <span class="hljs-keyword">const</span> statusMessage = <span class="hljs-keyword">await</span> deleteEntity(<span class="hljs-string">"TestTable"</span>, entityData);

      context.res!.status = <span class="hljs-number">200</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"Data deleted."</span>,
        <span class="hljs-attr">data</span>: statusMessage,
      };
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred"</span>,
      };
    }
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// method does not match any</span>
    context.res!.status = <span class="hljs-number">500</span>;
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>Azure serverless functions are a scalable and cost efficient method to create a RESTful API interface to interact with Azure Table Storage. The code above covers the actions needed to insert, retrieve, update and delete Azure Storage entities, while using TypeScript and the azure-storage and uuid npm packages to execute the methods that correspond to the Azure Table Storage API. Azure Storage entities can be accessed from a consumer of the serverless functions REST API, like a web application, and the Azure Storage credentials and connection string remain secure.</p>]]></description></item><item><title>Split a TypeScript Array into Chunks with a Generic Reduce Method</title><pubDate>Tue, 26 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/split-typescript-array-into-chunks/</link><guid isPermaLink="true">https://www.devextent.com/split-typescript-array-into-chunks/</guid><description><![CDATA[<p>Running too many asynchronous processes simultaneously with Node.js can cause issues that will lead to the process crashing. An example of this is when reading files inside of an asynchronous callback function that is being executed using the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map">map() method</a> on an array. To prevent a scenario where the node.js process might crash with an <a href="https://nodejs.org/api/errors.html#errors_common_system_errors">EMFILE error</a>, it can be helpful to split an array into smaller arrays or chunks, and process the group of smaller arrays synchronously while asynchronously mapping over the items in each of the smaller arrays. By doing this the contents of the original array can be processed in batches, preventing an error caused by opening too many files at once in parallel. The following configuration will allow us to demonstrate the EMFILE error and then add code to split an array into chunks, batching the process, and preventing the error from occurring.</p><h3>Setup Node.js and npm package.json</h3><p>Make sure to have <a href="https://nodejs.org/en/">node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed before following these steps. Then run the command <kbd>npm init</kbd> and follow the prompts to create a package.json file. Once the package.json file is created add the setting:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>
}
</code></pre><p>This will permit the use of <a href="https://nodejs.org/api/esm.html">ECMAScript modules</a> in the code, specifically it will allow for the use of <a href="https://nodejs.org/api/esm.html#esm_import_specifiers">es module imports</a> from npm packages. After that we need to install TypeScript, so run the command <kbd>npm install typescript --save</kbd> and then run the command <kbd>npm install @types/node --save-dev</kbd>. At this point also go ahead and add a new script property called "start", that will <a href="https://www.devextent.com/npm-compile-typescript/">initiate the TypeScript compiler and run the JavaScript output with Node.js</a>.</p><p>The package.json file should look similar to this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"splitarrayintochunks"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"tsc &amp;&amp; node index.js"</span>,
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^14.14.22"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.1.3"</span>
  }
}
</code></pre><h3>Setup TypeScript</h3><p>After configuring Node.js, add a tsconfig.json file to same folder as the package.json file. This lets us use TypeScript, which we just installed, instead of JavaScript and as a result we get the advantage of <a href="https://www.typescriptlang.org/docs/handbook/generics.html">generic types</a> among other features. Copy this config into the tsconfig.json file:</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"allowSyntheticDefaultImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2019"</span>],
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"*.ts"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules/**/*"</span>]
}
</code></pre><p>Now the output of the TypeScript compilation, indicated in the tsconfig "module" field, will be created as ECMAScript modules, which matches the type field added to the package.json configuration.</p><h3>Node.js EMFILE Error When Reading Files</h3><p>The configuration steps are now complete and we can add some code that will demonstrate the EMFILE error that can be prevented by batch processing the array in smaller chunks. This sample code, that results in an error, can be added to index.ts.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> util <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">//an array containing ten thousand undefined items</span>
  <span class="hljs-keyword">const</span> originalArray = <span class="hljs-built_in">Array</span>.from(<span class="hljs-built_in">Array</span>(<span class="hljs-number">10000</span>));

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// awaiting all ten thousand promises simultaneously</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
      originalArray.map(<span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">const</span> file = <span class="hljs-keyword">await</span> readFile(<span class="hljs-string">"./data.json"</span>, <span class="hljs-string">"utf8"</span>);
        <span class="hljs-built_in">console</span>.log(file);
      })
    );
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }
})();
</code></pre><p>At this point also create a sample <a href="https://www.json.org/json-en.html">JSON</a> file referenced in the code above named "data.json". All that you need to add to this file is "{}" which will be interpreted as an empty JSON object. With the data file created run the command <kbd>npm run start</kbd> and as expected you should see an error in the console:</p><pre><code class="hljs">[Error: EMFILE: too many open files, open <span class="hljs-string">'/../../data.json'</span>] {
  errno: -4066,
  code: <span class="hljs-string">'EMFILE'</span>,
  syscall: <span class="hljs-string">'open'</span>,
  path: <span class="hljs-string">'/../../data.json'</span>
}
</code></pre><p>What is occurring is that we are trying to asynchronously read the data.json file ten thousand times all at once, and the error is informing us that there are too many <a href="https://en.wikipedia.org/wiki/File_descriptor">file descriptors</a> for the system that the code is being run on. The access to the data.json file is happening too frequently for the system to keep track of and as a result the process crashes.</p><p>Rather than trying all ten thousand file read attempts at once, the array can be split into chunks and the read requests can be processed in batches, ensuring the number total number of file descriptors is within a suitable limit for the system that Node.js is operating on. To do this we can create a generic TypeScript function that will split any type of array into chunks of the original array type.</p><h3>TypeScript Generic Reducer to Split Array Into Chunks</h3><p>In the index.ts file, and above the main function that is immediately invoked we can create another function named "chunkItems". This will utilize TypeScript generics to create an array containing groups of smaller arrays, that match the type of the original array.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> chunkItems = &lt;T&gt;<span class="hljs-function">(<span class="hljs-params">items: T[]</span>) =&gt;</span>
  items.reduce(<span class="hljs-function">(<span class="hljs-params">chunks: T[][], item: T, index</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> chunk = <span class="hljs-built_in">Math</span>.floor(index / <span class="hljs-number">512</span>);
    chunks[chunk] = ([] <span class="hljs-keyword">as</span> T[]).concat(chunks[chunk] || [], item);
    <span class="hljs-keyword">return</span> chunks;
  }, []);
</code></pre><p>The <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce">reduce() method</a> is used to create an array containing chunks of smaller arrays, and for this example the chunk size is set to be a limit of 512 items per chunk. This way the maximum number of file descriptors that can be allocated at once, is below the default limit of most systems. Now we can use the generic "chunkItems" function to create a batched process by wrapping the existing file read code in a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of">for...of loop</a>, so that each of the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all()</a> results can be awaited asynchronously.</p><p>Putting all the code together in the index.ts file looks like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> util <span class="hljs-keyword">from</span> <span class="hljs-string">"util"</span>;
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);

<span class="hljs-keyword">const</span> chunkItems = &lt;T&gt;<span class="hljs-function">(<span class="hljs-params">items: T[]</span>) =&gt;</span>
  items.reduce(<span class="hljs-function">(<span class="hljs-params">chunks: T[][], item: T, index</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> chunk = <span class="hljs-built_in">Math</span>.floor(index / <span class="hljs-number">512</span>);
    chunks[chunk] = ([] <span class="hljs-keyword">as</span> T[]).concat(chunks[chunk] || [], item);
    <span class="hljs-keyword">return</span> chunks;
  }, []);

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> originalArray = <span class="hljs-built_in">Array</span>.from(<span class="hljs-built_in">Array</span>(<span class="hljs-number">10000</span>));
  <span class="hljs-keyword">const</span> chunks = chunkItems(originalArray);
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> chunk <span class="hljs-keyword">of</span> chunks)
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
        chunk.map(<span class="hljs-keyword">async</span> (item, index) =&gt; {
          <span class="hljs-keyword">const</span> file = <span class="hljs-keyword">await</span> readFile(<span class="hljs-string">"./data.json"</span>, <span class="hljs-string">"utf8"</span>);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"-----start item------"</span>);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"current array chunk:"</span> + chunks.indexOf(chunk));
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"file contents: "</span> + file);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"current item index: "</span> + index);
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"-----end item-------"</span>);
        })
      );
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }
})();
</code></pre><p>Run the <kbd>npm run start</kbd> command again, and the EMFILE error will not occur. The output of the above code will display rather quickly, but it will show the index of each chunk currently being processed synchronously and the contents of the sample data.json file. Watching closely (or by stopping the output after it has ran for sometime), you can see that the chunk index goes in numerical order, but the intentionally limited number of file reads is still happening asynchronously and the current item indexes are not in numerical order. By splitting the array into smaller chunks the system is not overloaded and Node.js is able to process the files asynchronously.</p>]]></description></item><item><title>Convert Relative URL to Absolute URL with Node.js</title><pubDate>Tue, 19 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/relative-url-to-absolute-url-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/relative-url-to-absolute-url-nodejs/</guid><description><![CDATA[<p>Let's say you are building a site with the <a href="https://jamstack.org/">Jamstack</a> and you want to <a href="https://www.devextent.com/xml-rss-feed-nodejs/">use node.js to generate the rss feed for your site</a>. In doing so you realize that your post content contains relative links when checking with the validator provided by the <a href="https://validator.w3.org/">W3C Feed validation service</a>, and it indicates <a href="https://validator.w3.org/feed/docs/warning/ContainsRelRef.html">elements should not contain relative URL references</a>. In order to make sure the RSS feed is valid, and only containing absolute URLs, we can use the <a href="https://www.npmjs.com/package/cheerio">cheerio npm package</a> to parse an HTML source and transform relative anchor links and image sources to absolute URLs. To demonstrate this we can create an HTML file that represents sample post content.</p><h3>HTML with relative links</h3><pre><code class="hljs"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
  This is the sample content that contains a
  <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/relative-link"</span>&gt;</span>relative link<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>, that will be converted into an
  absolute link.
<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Post content can also include images like this one:<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/sample-image"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>These will get transformed too.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre><p>This isn't a full HTML document, only a fragment that represents a sample of what may be contained in a blog post that has been converted from markdown into HTML with a node.js static site generator. Now that the sample HTML file is created and saved as "sample-post.html" we can read it and process the relative links.</p><h3>Cheerio npm package</h3><p>To use the cheerio npm package we need to create a node script, and for this we can optionally use TypeScript. For more info about using TypeScript with Node.js, read about how to <a href="https://www.devextent.com/npm-compile-typescript/">compile TypeScript with npm</a>. If you aren't using TypeScript you can omit the type declarations from the following code.</p><p>What is important is that the package.json file is configured for the project (if not use the <kbd>npm init</kbd> command), and then run the command <kbd>npm install cheerio fs-extra typescript --save</kbd> followed by the command <kbd>npm install @types/cheerio @types/fs-extra @types/node --save-dev</kbd> to install the cheerio npm package with the corresponding type declaration files.</p><p>The script code will use es modules to import these npm package libraries, so at the top of the generated package.json file add the following line:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>
}
</code></pre><p>Your package.json file should look similar to this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"relativeurltoabsoluteurl"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"cheerio"</span>: <span class="hljs-string">"^1.0.0-rc.5"</span>,
    <span class="hljs-attr">"fs-extra"</span>: <span class="hljs-string">"^9.0.1"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.1.3"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/cheerio"</span>: <span class="hljs-string">"^0.22.23"</span>,
    <span class="hljs-attr">"@types/fs-extra"</span>: <span class="hljs-string">"^9.0.6"</span>,
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^14.14.21"</span>
  }
}
</code></pre><p>You can also copy the above json and save as package.json, then run the command <kbd>npm install</kbd> to install all of the dependencies listed.</p><h3>Transform relative URL to absolute URL</h3><p>Then create a new file named script.ts and place the following code inside of it:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">"cheerio"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">convertRelativeToAbsolute</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> postContent = <span class="hljs-keyword">await</span> fs.readFile(<span class="hljs-string">"./sample-post.html"</span>);

  <span class="hljs-keyword">const</span> $ = cheerio.load(postContent <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>, {
    <span class="hljs-attr">decodeEntities</span>: <span class="hljs-literal">false</span>,
  });

  $(<span class="hljs-string">"a[href^='/'], img[src^='/']"</span>).each(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"><span class="hljs-built_in">this</span>: cheerio.Element</span>) </span>{
    <span class="hljs-keyword">const</span> $this = $(<span class="hljs-built_in">this</span>);
    <span class="hljs-keyword">if</span> ($this.attr(<span class="hljs-string">"href"</span>)) {
      $this.attr(<span class="hljs-string">"href"</span>, <span class="hljs-string">`YOUR-DOMAIN-HERE/<span class="hljs-subst">${$<span class="hljs-built_in">this</span>.attr(<span class="hljs-string">"href"</span>)}</span>`</span>);
    }
    <span class="hljs-keyword">if</span> ($this.attr(<span class="hljs-string">"src"</span>)) {
      $this.attr(<span class="hljs-string">"src"</span>, <span class="hljs-string">`YOUR-DOMAIN-HERE/<span class="hljs-subst">${$<span class="hljs-built_in">this</span>.attr(<span class="hljs-string">"src"</span>)}</span>`</span>);
    }
  });

  <span class="hljs-keyword">await</span> fs.writeFile($(<span class="hljs-string">"body"</span>).html() <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);
})();
</code></pre><p>Make sure to replace "YOUR-DOMAIN-HERE" with actual domain you want to convert the relative links to use.</p><p>The code inside the "convertRelativeToAbsolute" function, first reads the sample post file containing the HTML file with relative links. Then it uses the cheerio package to load the file and parse it to find all of the anchor tags and images tags that are referencing relative URLs. The selectors used scope either anchor tags or image tags to those that begin with a forward slash, which can most likely be safely assumed to be a relative link. Depending on whether the element is an anchor link or an image, either the href attribute or the src attribute will be prepended with the site domain, to make it an absolute link. When all of the link and image attributes are processed the sample html file is written back to the original file location.</p><h3>Compile TypeScript and Run Node script</h3><p>Now we can add a script to the package.json file that will compile the TypeScript script file and run the "convertRelativeToAbsolute" function. In the package.json file add this line to the scripts property:</p><pre><code class="hljs">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"convertRelativeToAbsolute"</span>: <span class="hljs-string">"tsc --allowSyntheticDefaultImports --moduleResolution node --module esnext script.ts &amp;&amp; node script.js"</span>
  }
}
</code></pre><p>This will first run the TypeScript compiler, with the flag options specified to indicate the use of es modules with node.js, to convert script.ts into JavaScript output. Then the script.js file is run using node. We can run the "convertRelativeToAbsolute" package.json script by running the command <kbd>npm run convertRelativeToAbsolute</kbd>. After it completes you should be able to see the sample-post.html file has been updated to use absolute links in the sample content included earlier.</p><p>Now the sample-post.html file content can be shared and referenced from any source, while ensuring that any of the internal links will load as expected. In a more typical scenario the cheerio parsing can be included as a plugin or middleware in a static site generator's build process, rather than working with HTML files directly. This would enable the output of the build process to apply the relative to absolute link conversion to all of the, possibly syndicated, content for the site.</p><p>This is one example of how the cheerio npm package is helpful for DOM parsing and manipulation outside of the browser, especially in the context of a static, pre-rendered site that is built using Jamstack technologies.</p>]]></description></item><item><title>Build a Jamstack Form with Serverless Functions and a Stateless CSRF Token</title><pubDate>Sat, 16 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/jamstack-serverless-form-stateless-csrf-token/</link><guid isPermaLink="true">https://www.devextent.com/jamstack-serverless-form-stateless-csrf-token/</guid><description><![CDATA[<p>To mitigate <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">Cross-site request forgery</a> attacks, websites that submit forms can include a <a href="https://en.wikipedia.org/wiki/Cryptographic_nonce">nonce</a>, to make sure that the request is being sent from the origin that is expected. This way, a post request containing the nonce, or public token, can be verified with a secret, and stored on the server before mutating any data. Using a CSRF token doesn't guarantee that a website will be safe from malicious requests, however it can help prevent malicious requests, or requests generated by automated bots.</p><p>This example will show how a publicly available HTML form, can be submitted using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a>, with <a href="https://www.typescriptlang.org/">TypeScript</a>, to first asynchronously retrieve a valid token, and then submit that token in a second request to save the form information. For the <a href="https://en.wikipedia.org/wiki/Server-side#:~:text=Server%2Dside%20refers%20to%20operations,relationship%20in%20a%20computer%20network.">server-side</a> components, <a href="https://docs.microsoft.com/en-us/azure/azure-functions/">Azure Functions</a> will be used, however these techniques can be applied to other server-side technologies, including a typical server.</p><h3>HTML Form</h3><p>We can create a form containing any fields we would like to submit. Let's create a sample contact form with some standard information to collect. There is one extra field at the bottom of the form that is hidden to act as a decoy field for bots to incorrectly submit. This can be ignored for now, but it will be validated in the serverless function handling submissions of the contact form.</p><pre><code class="hljs"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Contact Form<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span>
      <span class="hljs-attr">id</span>=<span class="hljs-string">"contactForm"</span>
      <span class="hljs-attr">action</span>=<span class="hljs-string">"YOUR-DOMAIN/api"</span>
      <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span>
      <span class="hljs-attr">data-type</span>=<span class="hljs-string">"contact"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"firstName"</span>&gt;</span>first name<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">required</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"firstName"</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">"firstName"</span>
          <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"given-name"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"lastName"</span>&gt;</span>last name<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">required</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"lastName"</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">"lastName"</span>
          <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"family-name"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">required</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>
          <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"email"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"website"</span>&gt;</span>website<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"website"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"website"</span> <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"url"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"message"</span>&gt;</span>message<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span> <span class="hljs-attr">required</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"message"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"message"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"position: absolute; left: -5000px"</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"password"</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
          <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span>
          <span class="hljs-attr">tabindex</span>=<span class="hljs-string">"-1"</span>
          <span class="hljs-attr">value</span>=<span class="hljs-string">""</span>
          <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"off"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"form-submit-msg"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"form.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>Make sure to replace "YOUR-DOMAIN" in the form action attribute to the domain you are using. For Azure functions local development the form action could be http://localhost:7071/api. We want the form action to end with "/api", rather than include the full url, so that the form "data-type" attribute can be appended to the url later with JavaScript. This way if anyone is attempting to scrape this form they would not get the full url without inspecting the JavaScript code executing the <a href="https://en.wikipedia.org/wiki/Ajax_(programming)">AJAX</a> request.</p><p>The bottom of the HTML document includes a reference to a script named "form.js" and this is where the JavaScript code to submit the form will be included. We can create that file now with TypeScript.</p><h3>TypeScript Form Submit</h3><p>For this example we'll use TypeScript, which will transpile to the script referenced in the HTML form (script.js). More info on how to use TypeScript with HTML forms can be found in this article showing how to <a href="https://www.devextent.com/fetch-api-post-formdata-object/">submit a FormData object using the ES6 Fetch Web API</a>. With TypeScript properly configured, we can create the form.ts file and add some of the needed code:</p><pre><code class="hljs"><span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">"load"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">new</span> FormHandler();
});
</code></pre><p>Now we can create the FormHandler class that is instantiated when the HTML document is loaded, by adding it directly below the window event listener.</p><pre><code class="hljs"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FormHandler</span> </span>{
  <span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params"></span>)</span> {
    <span class="hljs-built_in">this</span>.formSubmitListener();
  }

  <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-title">formSubmitListener</span>(<span class="hljs-params"></span>)</span> {
    <span class="hljs-built_in">document</span>.body.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
      event.preventDefault();
    });
  }
}
</code></pre><p>The private method "formSubmitListener" is invoked during the constructor of the FormHandler class, and includes the registration of an additional event listener that will be activated on the HTML form submit event. Currently this only prevents the default event from occurring, so we can add additional code to get the data from the form.</p><pre><code class="hljs"><span class="hljs-comment">// inform user form is submitting</span>
<span class="hljs-keyword">const</span> submitButton = <span class="hljs-built_in">document</span>.querySelector(
  <span class="hljs-string">"button[type=submit]"</span>
) <span class="hljs-keyword">as</span> HTMLInputElement;

submitButton.disabled = <span class="hljs-literal">true</span>;

<span class="hljs-keyword">const</span> statusMsgElement = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"form-submit-msg"</span>);

statusMsgElement!.innerText = <span class="hljs-string">"Submitting reply... Please wait."</span>;

<span class="hljs-comment">// gather form element data</span>
<span class="hljs-keyword">const</span> form = event.target <span class="hljs-keyword">as</span> HTMLFormElement;

<span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(form);
</code></pre><p>The first bit of code added, will select the submit button of the form and disable it during the submission so that the form cannot be submitted multiple times. Then the "form-submit-msg" element will show a message that indicates to the viewer the form is processing. After alerting the user, the form is gathered from the event target passed as an argument of the submit event listener. The "event.target" value is cast to an <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement">HTMLFormElement</a> so that TypeScript will permit the access of the "target" property. Then a <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">FormData</a> object is instantiated with the form element. Next we can send the formData variable using the Fetch API.</p><h3>Get csrf Token and Post FormData with Fetch API</h3><p>Before accessing the result of the form submission, two extra helper functions are created to handle and log any errors that may occur during the Fetch API post request. Once the helper functions are created the Fetch request is stored in the "result" variable.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> errorHandler = <span class="hljs-keyword">async</span> (response: Response) =&gt; {
  <span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">const</span> err = <span class="hljs-keyword">await</span> response.json().then(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> err);

    <span class="hljs-keyword">throw</span> <span class="hljs-built_in">Error</span>(
      <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-attr">status</span>: response.status,
        <span class="hljs-attr">statusText</span>: response.statusText,
        <span class="hljs-attr">error</span>: err,
      })
    );
  }

  <span class="hljs-keyword">return</span> response;
};

<span class="hljs-keyword">const</span> errorLogger = <span class="hljs-function">(<span class="hljs-params">error: <span class="hljs-built_in">Error</span></span>) =&gt;</span> {
  <span class="hljs-comment">// overwrite message to inform user</span>
  error.message = <span class="hljs-string">"An error occurred. Please try again."</span>;
  <span class="hljs-keyword">return</span> error;
};

<span class="hljs-comment">// submit formData with error handling and logging</span>
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> fetch(
  <span class="hljs-string">`<span class="hljs-subst">${form.action}</span>/formToken/<span class="hljs-subst">${<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString()).getTime()}</span>/<span class="hljs-subst">${
    form.dataset.<span class="hljs-keyword">type</span>
  }</span>`</span>
)
  .then(errorHandler)
  .then(<span class="hljs-function">(<span class="hljs-params">response: Response</span>) =&gt;</span> response.json())
  .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    <span class="hljs-comment">// anti-forgery</span>
    formData.append(<span class="hljs-string">"_csrf"</span>, data.token);
    <span class="hljs-keyword">return</span> data.type;
  })
  .then(
    <span class="hljs-keyword">async</span> (<span class="hljs-keyword">type</span>) =&gt;
      <span class="hljs-comment">// casting to any here to satisfy tsc</span>
      <span class="hljs-comment">// sending body as x-www-form-url-encoded</span>
      <span class="hljs-comment">// formData convert to array for edge browser support</span>
      <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`<span class="hljs-subst">${form.action}</span>/<span class="hljs-subst">${<span class="hljs-keyword">type</span>}</span>`</span>, {
        <span class="hljs-attr">method</span>: form.method,
        <span class="hljs-attr">body</span>: <span class="hljs-keyword">new</span> URLSearchParams([...(formData <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>)]),
      })
  )
  .then(errorHandler)
  .then(<span class="hljs-function">(<span class="hljs-params">response: Response</span>) =&gt;</span> response.json())
  .then(<span class="hljs-function">(<span class="hljs-params">json</span>) =&gt;</span> json)
  .catch(errorLogger);

statusMsgElement!.innerText = result.message;
submitButton.disabled = <span class="hljs-literal">false</span>;
</code></pre><p>Since we need a CSRF token and the HTML form isn't rendered server-side (it is pre-rendered as is the case with a site built with the Jamstack) there is actually two fetch requests sent. The first one is a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET">GET</a> request to an endpoint that will provide the token, and then that token is appended to the formData object created earlier. The url pattern for this endpoint includes the "data-type" attribute from the form and a current timestamp. The timestamp is an extra step of validation that will occur in the serverless function created later. Additionally the formToken endpoint sends back the form data type sent to it, so that it can be passed to the second request.</p><p>After getting a valid token the next request is a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST">POST</a> request to the form "data-type" endpoint, and the body of the request includes the updated formData object with the "_csrf" token appended. This request is responsible for saving the data if it is sent with a valid CSRF token, and the form data is valid.</p><p>The last bit of code below the result is showing a message to the user after the Fetch request completes, showing whether the submission was successful, or an error occurred and they should try again. Additionally the submit button is no longer disabled so the form can be submitted again.</p><p>The entire form.ts file should look like this:</p><pre><code class="hljs"><span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">"load"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">new</span> FormHandler();
});

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FormHandler</span> </span>{
  <span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params"></span>)</span> {
    <span class="hljs-built_in">this</span>.formSubmitListener();
  }

  <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-title">formSubmitListener</span>(<span class="hljs-params"></span>)</span> {
    <span class="hljs-built_in">document</span>.body.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
      event.preventDefault();

      <span class="hljs-comment">// inform user form is submitting</span>
      <span class="hljs-keyword">const</span> submitButton = <span class="hljs-built_in">document</span>.querySelector(
        <span class="hljs-string">"button[type=submit]"</span>
      ) <span class="hljs-keyword">as</span> HTMLInputElement;

      submitButton.disabled = <span class="hljs-literal">true</span>;

      <span class="hljs-keyword">const</span> statusMsgElement = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"form-submit-msg"</span>);

      statusMsgElement!.innerText = <span class="hljs-string">"Submitting reply... Please wait."</span>;

      <span class="hljs-comment">// gather form element data</span>
      <span class="hljs-keyword">const</span> form = event.target <span class="hljs-keyword">as</span> HTMLFormElement;

      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(form);

      <span class="hljs-keyword">const</span> errorHandler = <span class="hljs-keyword">async</span> (response: Response) =&gt; {
        <span class="hljs-keyword">if</span> (!response.ok) {
          <span class="hljs-keyword">const</span> err = <span class="hljs-keyword">await</span> response.json().then(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> err);

          <span class="hljs-keyword">throw</span> <span class="hljs-built_in">Error</span>(
            <span class="hljs-built_in">JSON</span>.stringify({
              <span class="hljs-attr">status</span>: response.status,
              <span class="hljs-attr">statusText</span>: response.statusText,
              <span class="hljs-attr">error</span>: err,
            })
          );
        }

        <span class="hljs-keyword">return</span> response;
      };

      <span class="hljs-keyword">const</span> errorLogger = <span class="hljs-function">(<span class="hljs-params">error: <span class="hljs-built_in">Error</span></span>) =&gt;</span> {
        <span class="hljs-comment">// overwrite message to inform user</span>
        error.message = <span class="hljs-string">"An error occurred. Please try again."</span>;
        <span class="hljs-keyword">return</span> error;
      };

      <span class="hljs-comment">// submit formData with error handling and logging</span>
      <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> fetch(
        <span class="hljs-string">`<span class="hljs-subst">${form.action}</span>/formToken/<span class="hljs-subst">${<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
          <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString()
        ).getTime()}</span>/<span class="hljs-subst">${form.dataset.<span class="hljs-keyword">type</span>}</span>`</span>
      )
        .then(errorHandler)
        .then(<span class="hljs-function">(<span class="hljs-params">response: Response</span>) =&gt;</span> response.json())
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
          <span class="hljs-comment">// anti-forgery</span>
          formData.append(<span class="hljs-string">"_csrf"</span>, data.token);
          <span class="hljs-keyword">return</span> data.type;
        })
        .then(
          <span class="hljs-keyword">async</span> (<span class="hljs-keyword">type</span>) =&gt;
            <span class="hljs-comment">// casting to any here to satisfy tsc</span>
            <span class="hljs-comment">// sending body as x-www-form-url-encoded</span>
            <span class="hljs-comment">// formData convert to array for edge browser support</span>
            <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`<span class="hljs-subst">${form.action}</span>/<span class="hljs-subst">${<span class="hljs-keyword">type</span>}</span>`</span>, {
              <span class="hljs-attr">method</span>: form.method,
              <span class="hljs-attr">body</span>: <span class="hljs-keyword">new</span> URLSearchParams([...(formData <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>)]),
            })
        )
        .then(errorHandler)
        .then(<span class="hljs-function">(<span class="hljs-params">response: Response</span>) =&gt;</span> response.json())
        .then(<span class="hljs-function">(<span class="hljs-params">json</span>) =&gt;</span> json)
        .catch(errorLogger);

      statusMsgElement!.innerText = result.message;
      submitButton.disabled = <span class="hljs-literal">false</span>;
    });
  }
}
</code></pre><h3>CSRF Token Serverless Function</h3><p>The client-side code is now set up, so we can look at creating the Azure TypeScript Serverless Functions that will provide a server-side environment to generate the CSRF token and then validate the token to save the form submission data. Here is the <a href="https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-typescript">quickstart documentation for creating an Azure TypeScript function with Visual Studio code</a>. Once that is setup, we are going to create two functions. The first is the formToken endpoint.</p><p>In your functions package.json make sure to include the <a href="https://www.npmjs.com/package/csrf">csrf npm package</a> by running the command <kbd>npm install csrf --save</kbd></p><p>Here is the functions.json file associated with the index.ts formToken code that follows:</p><pre><code class="hljs">{
  <span class="hljs-attr">"bindings"</span>: [
    {
      <span class="hljs-attr">"authLevel"</span>: <span class="hljs-string">"anonymous"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"httpTrigger"</span>,
      <span class="hljs-attr">"direction"</span>: <span class="hljs-string">"in"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"req"</span>,
      <span class="hljs-attr">"methods"</span>: [<span class="hljs-string">"get"</span>],
      <span class="hljs-attr">"route"</span>: <span class="hljs-string">"formToken/{timeStamp:long}/{formType:alpha}"</span>
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"http"</span>,
      <span class="hljs-attr">"direction"</span>: <span class="hljs-string">"out"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"res"</span>
    }
  ],
  <span class="hljs-attr">"scriptFile"</span>: <span class="hljs-string">"../dist/formToken/index.js"</span>
}
</code></pre><p>This function only accepts GET requests, and requires two route parameters, timeStamp and formType. These are included in the client side script we created earlier.</p><p>Here is the formToken function code:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> csrf <span class="hljs-keyword">from</span> <span class="hljs-string">"csrf"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-keyword">const</span> utcTime = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString();

  <span class="hljs-keyword">const</span> submitTime = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
    <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(context.bindingData.timeStamp).toUTCString()
  ).getTime();

  <span class="hljs-comment">// add some skew</span>
  <span class="hljs-keyword">const</span> futureDateLimit = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(utcTime).getTime() + <span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">5</span>;

  <span class="hljs-keyword">const</span> pastDateLimit = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(utcTime).getTime() - <span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">5</span>;

  <span class="hljs-keyword">if</span> (submitTime &gt; futureDateLimit || submitTime &lt; pastDateLimit) {
    <span class="hljs-comment">// don't create token but also don't return error</span>
    context.res!.status = <span class="hljs-number">200</span>;
    context.res!.body = { <span class="hljs-attr">message</span>: <span class="hljs-string">"success"</span> };
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">const</span> tokens = <span class="hljs-keyword">new</span> csrf();

    <span class="hljs-keyword">const</span> token = tokens.create(process.env[<span class="hljs-string">"csrfSecret"</span>]);

    context.res!.status = <span class="hljs-number">200</span>;
    context.res!.body = { <span class="hljs-attr">token</span>: token, <span class="hljs-attr">type</span>: context.bindingData.formType };
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>This function first gathers the current time, and then the time submitted as the timeStamp route parameter. Then a past and future date limit are calculated based on the current time. If the submitted timeStamp is not within the date limit range then the request is ignored and a fake success message is sent back. This is to deter any bots from attempting to make anymore additional requests.</p><p>If the timestamp is valid, a new token is generated using the csrf npm package <a href="https://github.com/pillarjs/csrf#tokenscreatesecret">tokens.create()</a> function. In order to prevent the secret from being accessed publicly or accidentally stored in a git repository, a process environment variable is referenced to obtain the "csrfSecret" value. This is the documentation on how to <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal">add an application setting</a> in the Azure portal. With the generated token, the function returns the response object, including the token and the "formType" route parameter that was sent with the request.</p><p>In this example the same secret is used for all tokens that are generated. This may be useful as all tokens can be invalidated by changing the secret, and given the short length of the token date limit range this can work well. However, it may be advantageous to use the csrf npm package <a href="https://github.com/pillarjs/csrf#tokenssecret">token.secret()</a> function to dynamically create a new secret for each token that is generated. You could then store both the token and the secret in a database, or Azure Table Storage, and use the token to look up the stored secret, to later verify the token on the subsequent request.</p><h3>Contact Form Serverless Function</h3><p>The second serverless function is going to accept the contact form data with the csrf token appended. Additionally, it will verify the hidden decoy password form field, and the csrf token. If both validations pass then the data can be saved.</p><p>Here is the functions.json for the contact serverless function:</p><pre><code class="hljs">{
  <span class="hljs-attr">"bindings"</span>: [
    {
      <span class="hljs-attr">"authLevel"</span>: <span class="hljs-string">"anonymous"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"httpTrigger"</span>,
      <span class="hljs-attr">"direction"</span>: <span class="hljs-string">"in"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"req"</span>,
      <span class="hljs-attr">"methods"</span>: [<span class="hljs-string">"post"</span>]
    },
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"http"</span>,
      <span class="hljs-attr">"direction"</span>: <span class="hljs-string">"out"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"res"</span>
    }
  ],
  <span class="hljs-attr">"scriptFile"</span>: <span class="hljs-string">"../dist/contact/index.js"</span>
}
</code></pre><p>Note that the contact function is limited to only accept post requests.</p><p>Below is the index.ts function code:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> csrf <span class="hljs-keyword">from</span> <span class="hljs-string">"csrf"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-comment">//sent as x-www-form-url-encoded</span>
  <span class="hljs-keyword">const</span> body = querystring.parse(req.body);

  <span class="hljs-comment">// check hidden form field</span>
  <span class="hljs-keyword">const</span> verifiedHiddenFormField =
    body &amp;&amp; (body.password === <span class="hljs-literal">undefined</span> || body.password.length);

  <span class="hljs-comment">// verify token with secret</span>
  <span class="hljs-keyword">const</span> verifiedToken = <span class="hljs-keyword">new</span> csrf().verify(
    process.env[<span class="hljs-string">"csrfSecret"</span>],
    body._csrf
  );

  <span class="hljs-keyword">if</span> (!verifiedHiddenFormField || !verifiedToken) {
    <span class="hljs-comment">// failed verification</span>
    context.res!.status = <span class="hljs-number">200</span>;
    context.res!.body = { <span class="hljs-attr">message</span>: <span class="hljs-string">"success"</span> };
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-keyword">if</span> (
    !(body &amp;&amp; body.firstName &amp;&amp; body.lastName &amp;&amp; body.email &amp;&amp; body.message)
  ) {
    context.res!.status = <span class="hljs-number">400</span>;
    context.res!.body = {
      <span class="hljs-attr">message</span>: <span class="hljs-string">"Contact form is invalid. Please correct errors and try again."</span>,
    };
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">//todo: save the comment form data!</span>

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Thank you for contacting me! I will reply to you shortly."</span>,
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>The contact function first parses the request body using the <a href="https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options">querystring parse</a> method, which will create an object from the form data that was sent. Then the decoy password field is verified to exist, but also not have a value present. The csrf token appended to the form data is then verified using the process.env "csrfSecret" value. If both of these verifications are passing, then the function execution can proceed. Otherwise, like the formToken function, an empty success message is returned to deter further, possibly malicious requests.</p><p>After verification, the contact form info is checked to make sure all the fields have a value. If they don't an error message is returned and displayed to the viewer with the client side errorHandler and errorLogger functions created earlier.</p><p>At this point, with both verifications passing and valid form data, the data can be saved to the preferred data store. This could be a sql database or a nosql data store like azure storage. Once the save is complete the function will return a success message and the client side code will display that to the viewer.</p>]]></description></item><item><title>Render EJS File with Node.js</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/ejs-render-file/</link><guid isPermaLink="true">https://www.devextent.com/ejs-render-file/</guid><description><![CDATA[<p><a href="https://ejs.co/">EJS</a> is a templating language that uses JavaScript to generate HTML. This post will illustrate how to use Node.js with TypeScript to render an EJS file into HTML markup. Please make sure you have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed first. If you are unfamiliar with Typescript please read my post describing how to <a href="https://www.devextent.com/npm-compile-typescript/">compile TypeScript with npm</a>.</p><h3>EJS</h3><p>Begin by creating a new EJS file named index.ejs. This file will be the template used to generate index.html. If the model is passed into the template it will render the content as a paragraph.</p><pre><code class="hljs">&lt;!-- Sample Page --&gt;

&lt;h1&gt;Sample Page&lt;/h1&gt;

&lt;% if (model) { %&gt;
  &lt;%= model.content %&gt;
&lt;% } %&gt;
</code></pre><h3>package.json</h3><p>If you don't already have a package.json created you can create one by running the command <kbd>npm init</kbd> and following the prompts.</p><p>You will need your package.json to include these packages:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/ejs"</span>: <span class="hljs-string">"^2.6.2"</span>,
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^11.9.4"</span>,
    <span class="hljs-attr">"ejs"</span>: <span class="hljs-string">"^2.6.1"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^3.3.3333"</span>
  }
}
</code></pre><p>You can also copy the devDependencies section and run the command <kbd>npm install</kbd> instead of installing one at a time.</p><h3>Node.js</h3><p>Create a new TypeScript file named render.ts. Then add the following code to import the modules that we will use.</p><pre><code class="hljs"><span class="hljs-comment">//imports</span>
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> ejs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"ejs"</span>);
<span class="hljs-comment">//promisify</span>
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);
</code></pre><p>The first import is the <a href="https://nodejs.org/api/util.html">util</a> module so that we can use the <a href="https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original">promisify</a> function. Then import the <a href="https://nodejs.org/api/util.html">fs</a> module for file system access. Before using three of the functions from the fs module we can promisify them allowing for the use of async/await instead of nested callbacks. The last is for EJS, and since the render file function returns a promise by default we do not need to use promisify.</p><p>Below the import statements add an async function named render. This is where the HTML output will be generated and written to a file named index.html. It needs to be marked as an async function so that the keyword await can be used. Then make sure to call the function so the code that is about to be added will execute.</p><pre><code class="hljs"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">render</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }
}
render();
</code></pre><p>Before rendering our EJS file we will need a folder to put the output. So add the following to our render function:</p><pre><code class="hljs"><span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">"dist"</span>, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
</code></pre><p>This will create a new directory named dist where the html output will be saved. By passing the recursive property we can ensure parent folders are created even if none are necessary. After creating the dist folder we can use EJS to render the index.ejs template to HTML. The resulting HTML string is then written to a file named index.html in the dist folder.</p><p>At this point your index.ts file should look like this:</p><pre><code class="hljs"><span class="hljs-comment">//imports</span>
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> ejs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"ejs"</span>);
<span class="hljs-comment">//promisify</span>
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">render</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">//create output directory</span>
    <span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">"dist"</span>, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });

    <span class="hljs-comment">//render ejs template to html string</span>
    <span class="hljs-keyword">const</span> html = <span class="hljs-keyword">await</span> ejs
      .renderFile(<span class="hljs-string">"index.ejs"</span>, { <span class="hljs-attr">model</span>: <span class="hljs-literal">false</span> })
      .then(<span class="hljs-function">(<span class="hljs-params">output</span>) =&gt;</span> output);
    <span class="hljs-comment">//create file and write html</span>
    <span class="hljs-keyword">await</span> writeFile(<span class="hljs-string">"dist/index.html"</span>, html, <span class="hljs-string">"utf8"</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }
}
render();
</code></pre><p>In order to run this script we need to add a tsconfig.json file to configure the TypeScript compiler. This will compile the TypeScript into JavaScript so that it can be used by node.js. Add the tsconfig file to the same folder as the render.js script.</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"rootDir"</span>: <span class="hljs-string">"./"</span>,
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./dist"</span>,
    <span class="hljs-attr">"sourceMap"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"render.js"</span>]
}
</code></pre><p>We also need to add a script to the package.json file created earlier. This script will compile render.ts and then run it using node. Your package.json should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"render"</span>: <span class="hljs-string">"tsc &amp;&amp; node dist/render.js"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/ejs"</span>: <span class="hljs-string">"^2.6.2"</span>,
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^11.9.4"</span>,
    <span class="hljs-attr">"ejs"</span>: <span class="hljs-string">"^2.6.1"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^3.3.3333"</span>
  }
}
</code></pre><h3>EJS render HTML</h3><p>The render script can be run in a terminal window by typing the command <kbd>npm run render</kbd>. Make sure to run this command from the directory where your package.json is located. After running the render script you should now see a folder named dist containing a file named index.html.</p><p>The contents of index.html should look like this:</p><pre><code class="hljs">Sample Page
</code></pre><p>Notice that the conditional block containing the model content, in the index.ejs template, is not included in the html output. This is because in the render script the model was passed in as false. Now we'll create an object to pass in as the model with some sample content to the sample page.</p><p>In the render.ts file previously created, after the import statements, create an object and add a property to it called content with the value set to a sample of content.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> pageModel = {
  <span class="hljs-attr">content</span>: <span class="hljs-string">"This is some sample content. Located on the sample page."</span>,
};
</code></pre><p>Then pass this object in to the ejs.renderFile function instead of false. The render.ts file should look like this:</p><pre><code class="hljs"><span class="hljs-comment">//imports</span>
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> ejs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"ejs"</span>);
<span class="hljs-comment">//promisify</span>
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);

<span class="hljs-keyword">const</span> pageModel = {
  <span class="hljs-attr">content</span>: <span class="hljs-string">"&lt;p&gt;This is some sample content. Located on the sample page.&lt;/p&gt;"</span>,
};
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">render</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">//create output directory</span>
    <span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">"dist"</span>, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });

    <span class="hljs-comment">//render ejs template to html string</span>
    <span class="hljs-comment">//pass pageModel in to render content</span>
    <span class="hljs-keyword">const</span> html = <span class="hljs-keyword">await</span> ejs
      .renderFile(<span class="hljs-string">"index.ejs"</span>, { <span class="hljs-attr">model</span>: pageModel })
      .then(<span class="hljs-function">(<span class="hljs-params">output</span>) =&gt;</span> output);
    <span class="hljs-comment">//create file and write html</span>
    <span class="hljs-keyword">await</span> writeFile(<span class="hljs-string">"dist/index.html"</span>, html, <span class="hljs-string">"utf8"</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(error);
  }
}
render();
</code></pre><p>With the model object passed into the template we should now see the conditional block rendered in the index.html output file. Run the command <kbd>npm run render</kbd> once more.</p><p>The index.html file in the dist folder should now look like this:</p><pre><code class="hljs"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Sample Page<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This is some sample content. Located on the sample page.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
</code></pre><p>The index.ejs template can now render dynamic HTML content according to the model object configured in the render.ts file and by running <kbd>npm run render</kbd> after each change to generate an updated index.html file.</p>]]></description></item><item><title>Submit FormData Object Using the Fetch API</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/fetch-api-post-formdata-object/</link><guid isPermaLink="true">https://www.devextent.com/fetch-api-post-formdata-object/</guid><description><![CDATA[<p>The JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">Fetch</a> API provides a utility to make <a href="https://developer.mozilla.org/en-US/docs/Web/Guide/AJAX">AJAX</a> requests. This post will show how ES6 syntax can be used with Typescript and the Fetch API to submit an HTML form. Using the Fetch API in conjunction with other <a href="https://developer.mozilla.org/en-US/docs/Web/API">Web API's</a> a post request can be sent, containing <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects">FormData Objects</a> in the body of the request.</p><h3>HTML Form</h3><p>First we need to create an html file, let's call it index.html, with a form element to capture the input values we are going to submit using JavaScript.</p><pre><code class="hljs"><span class="hljs-comment">&lt;!-- index.html --&gt;</span>
<span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Example Form<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"myForm"</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"myFormAction"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- this input is hidden with a value already set --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"userId"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"userId"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"3"</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"firstName"</span>&gt;</span>first name<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"firstName"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"firstName"</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"form.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre><p>Take note that the value for the form action attribute is a placeholder. In real usage this would be replaced with url that you would like to submit the form to. One of the inputs is type=hidden to show that we can submit hidden elements using this technique. Additionally there is one input to capture first name and a button to submit the form using the HTTP post method.</p><h3>Typescript Form Submit</h3><p>Next we'll need to write form.ts so the TypeScript compiler can generate the JavaScript file, form.js, referenced in index.html. The code in form.ts will handle the form submit by making an AJAX request. If you haven't already now is a good time to read my other post <a href="https://www.devextent.com/npm-compile-typescript/">Compile Typescript with npm</a>. There you will find instructions on how to install and configure TypeScript to accommodate the usage below.</p><p>The code below assumes the endpoint we are submitting the form to, in the sample HTML action="myFormAction", will be returning a response that has the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type">Content-Type</a> header set to <a href="https://www.iana.org/assignments/media-types/application/json">application/json</a>.</p><p>To begin an event listener is created to listen for all form submit events. Notice that the callback function is marked as an async function. Using the async modifier allows for the use of the await keyword when executing the asynchronous Fetch request.</p><pre><code class="hljs"><span class="hljs-comment">// form.ts</span>

<span class="hljs-comment">// listen for any form submit event</span>
<span class="hljs-built_in">document</span>.body.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{});
</code></pre><p>Inside the callback the first line of code prevents the default action from occurring. Without preventing the default, the browser would attempt to navigate to the URL of the form action attribute when the form is submitted.</p><pre><code class="hljs"><span class="hljs-comment">// form.ts</span>

<span class="hljs-comment">// listen for any form submit event</span>
<span class="hljs-built_in">document</span>.body.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.preventDefault();
});
</code></pre><p>Next a variable for the form element is created and cast to an <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement">HTMLFormElement</a> to allow access to the action and method properties.</p><pre><code class="hljs"><span class="hljs-comment">// form.ts</span>

<span class="hljs-comment">// listen for any form submit event</span>
<span class="hljs-built_in">document</span>.body.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.preventDefault();

  <span class="hljs-keyword">const</span> form = event.target <span class="hljs-keyword">as</span> HTMLFormElement;
});
</code></pre><h3>Fetch API Post Form Data</h3><p>Then the result variable is created and it is used to store the response sent following the Fetch request. The Fetch request returns a promise and must be awaited so that the result can be obtained. The URL passed into the Fetch method is set to the action of the form, and the options contains keys for method and body values. The form method, like the action, is available from HTMLFormElement.</p><pre><code class="hljs"><span class="hljs-comment">// form.ts</span>

<span class="hljs-comment">// listen for any form submit event</span>
<span class="hljs-built_in">document</span>.body.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  event.preventDefault();
  <span class="hljs-keyword">const</span> form = event.target <span class="hljs-keyword">as</span> HTMLFormElement;

  <span class="hljs-comment">// casting to any here to satisfy tsc</span>
  <span class="hljs-comment">// sending body as x-www-form-url-encoded</span>
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> fetch(form.action, {
    <span class="hljs-attr">method</span>: form.method,
    <span class="hljs-attr">body</span>: <span class="hljs-keyword">new</span> URLSearchParams([...(<span class="hljs-keyword">new</span> FormData(form) <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>)]),
  })
    .then(<span class="hljs-function">(<span class="hljs-params">response: Response</span>) =&gt;</span> response.json())
    .then(<span class="hljs-function">(<span class="hljs-params">json</span>) =&gt;</span> json)
    .catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(error));
});
</code></pre><p>Notice the use of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">spread syntax</a> to transform the <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">FormData</a> into an array of key-value pairs. This may seem redundant, but the Edge browser cannot iterate over FormData objects. By transforming the object into an array, Edge is able to successfully construct the <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams">URLSearchParams</a> object.</p><p>Interestingly the current <a href="https://github.com/microsoft/TypeScript/blob/75301c8e2ce498359a6b33c3f9c9a6a1bd5980c0/lib/lib.dom.d.ts#L16109">TypeScript definition for URLSearchParams</a> does not permit a FormData object to be passed into the constructor, however this is valid JavaScript. To satisfy the TypeScript compiler the FormData object is cast to any. This allows a URLSearchParams object to be constructed from the FormData object which itself is constructed from the HTMLFormElement. Since the body of the Fetch request is of the type URLSearchParams (hint: it looks like a ?query=string) the resulting Content-Type of the request body will be x-www-form-url-encoded. This allows for the server to parse it as it would a normal form submission.</p><p>Now when the form in the index.html file is submitted the submit event listener will override the default browser behavior and submit the form using an AJAX request. Even without an actual endpoint to receive the request you should still be able to verify the code is working because the resulting error "Failed to fetch" will be logged to the console.</p>]]></description></item><item><title>Build a Serverless Comment System for a Jamstack Blog</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/jamstack-blog-serverless-comment-system/</link><guid isPermaLink="true">https://www.devextent.com/jamstack-blog-serverless-comment-system/</guid><description><![CDATA[<p><a href="https://jamstack.org/">Jamstack</a> blogs, or otherwise static sites that are built with prerendered markup can load quickly and cost less to run, however one potential drawback of a serverless approach for a blog can be the lack of a content management system. Without using a database or a headless content management system, blogs built with the Jamstack are most likely storing their content in a git repository, and this git-centric approach to development provides an interesting pathway for storing and managing blog comments. With some help from <a href="https://www.npmjs.com/package/@octokit/rest">Octokit</a>, the REST API client provided by GitHub, the <a href="https://www.npmjs.com/package/simple-git">Simple Git</a> npm package, <a href="https://www.npmjs.com/package/@sendgrid/mail">SendGrid</a> email service, and <a href="https://docs.microsoft.com/en-us/azure/azure-functions/">Azure Serverless Functions</a> comment system can be built that includes comment moderation and email notifications.</p><h3>Create GitHub Git Repository</h3><p>The first GitHub repo that we need to create will be public and is where our comments will ultimately end up. GitHub provides documentation for <a href="https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/create-a-repo">creating a repo</a>. After creating the public repository, a <a href="https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-repository-visibility">private repository</a> is also needed and is going to be used so that comments can be moderated through the creation of pull requests. The private repository also allows for any comment information, like emails, to be filtered out before merging into the public repository.</p><h3>HTML Comment Form</h3><p>With the git repositories set up we can now create a standard HTML form that will submit comments to our serverless function (not yet set up) endpoint.</p><pre><code class="hljs"><span class="hljs-comment">&lt;!-- form.html --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"commentForm"</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"FUNCTION_ENDPOINT"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"postId"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"postId"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"POST_ID"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"comment"</span>&gt;</span>comment<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span> <span class="hljs-attr">required</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">"5"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"comment"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"comment"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"authorName"</span>&gt;</span>name<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
      <span class="hljs-attr">required</span>
      <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
      <span class="hljs-attr">id</span>=<span class="hljs-string">"authorName"</span>
      <span class="hljs-attr">name</span>=<span class="hljs-string">"authorName"</span>
      <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"name"</span>
    /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"authorEmail"</span>&gt;</span>email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
      <span class="hljs-attr">required</span>
      <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
      <span class="hljs-attr">id</span>=<span class="hljs-string">"authorEmail"</span>
      <span class="hljs-attr">name</span>=<span class="hljs-string">"authorEmail"</span>
      <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"email"</span>
    /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre><p>In most cases a static site generator would be outputting this form from template files, but the important part is that the form action shown as "FUNCTION_ENDPOINT" will be replaced with the actual url that will be provided by the serverless function in the following section. There also needs to be a way to maintain the relationship between the comment submitted and the blog post it should reference. In this case a hidden field is added with a value of "POST_ID" to maintain this data during the form submit. This can be changed to anything that suits the build process in use, so that comments can be stored with this as a key to indicate which post they belong to.</p><h3>Azure Serverless Function</h3><p>Now that the client side HTML form is in place, we need an endpoint to submit the form to. Azure Javascript functions will be used to provide an endpoint configured to accept HTTP POST requests containing comment data, in the request body, that will be committed by our serverless function to the private git repository. Microsoft provides documentation to <a href="https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-typescript">set up a TypeScript function with Visual Studio Code</a>. Please make sure to reference their documentation before proceeding. Below is the starting code that we will build out TypeScript function with:</p><pre><code class="hljs"><span class="hljs-comment">// comment.ts</span>
<span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);
  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;
  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = { <span class="hljs-attr">message</span>: <span class="hljs-string">"Success!"</span> };
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>At this point all the function does is set the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type">Content-Type</a> response header and return an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200">HTTP 200 OK</a> success status response code with a success message. Next we will npm install the npm packages needed for the functions code.</p><h3>npm install</h3><p>We are going to want to use the following npm packages within the code of the serverless function we are creating:</p><ul><li><a href="https://www.npmjs.com/package/uuid">uuid</a></li><li><a href="https://www.npmjs.com/package/simple-git">simple-git</a></li><li><a href="https://www.npmjs.com/package/rimraf">rimraf</a></li><li><a href="https://www.npmjs.com/package/@sendgrid/mail">sendgrid/mail</a></li><li><a href="https://www.npmjs.com/package/@octokit/rest">octokit/rest</a></li></ul><p>To install these packages, all at the same time, and their corresponding types to use with Typescript, run the command: <kbd>npm install @sendgrid/mail @octokit/rest rimraf simple-git uuid @types/node @types/rimraf --save-dev</kbd>.</p><p>Then add these import states to the comment.ts file:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> uuidv4 = <span class="hljs-built_in">require</span>(<span class="hljs-string">"uuid/v4"</span>);
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SendGrid <span class="hljs-keyword">from</span> <span class="hljs-string">"@sendgrid/mail"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> simpleGit <span class="hljs-keyword">from</span> <span class="hljs-string">"simple-git/promise"</span>;
<span class="hljs-keyword">import</span> { formHelpers } <span class="hljs-keyword">from</span> <span class="hljs-string">"../common/formHelpers"</span>;
<span class="hljs-keyword">import</span> { Octokit } <span class="hljs-keyword">from</span> <span class="hljs-string">"@octokit/rest"</span>;
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> rimrafstd = <span class="hljs-built_in">require</span>(<span class="hljs-string">"rimraf"</span>);
<span class="hljs-keyword">import</span> { tmpdir } <span class="hljs-keyword">from</span> <span class="hljs-string">"os"</span>;
<span class="hljs-keyword">const</span> rimraf = util.promisify(rimrafstd);
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);
SendGrid.setApiKey(process.env[<span class="hljs-string">"SendGridApiKey"</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);
</code></pre><p>The last import statement uses and environment variable to securely access a SendGrid API key. In order to send out notification emails (this will be set up in a later section), create a SendGrid account and configure an API Key. Azure Serverless Functions support adding additional <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings#settings">application settings</a> where the API key can be saved as an environment variable. By using an environment variable we prevent the need to store the SendGrid API key directly in the serverless function source code.</p><h3>Validate POST request body</h3><p>Next add some basic validation to ensure that the comment form is submitted appropriately.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> body = querystring.parse(req.body);

<span class="hljs-keyword">if</span> (
  !(body &amp;&amp; body.comment &amp;&amp; body.postId &amp;&amp; body.authorEmail &amp;&amp; body.authorName)
) {
  context.res!.status = <span class="hljs-number">400</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Comment invalid. Please correct errors and try again."</span>,
  };
  <span class="hljs-keyword">return</span>;
}
</code></pre><p>After parsing the request body using the <a href="https://nodejs.org/api/querystring.html">querystring module</a> the validation code checks to make sure the form fields are filled out with data. In a production environment these checks would need to be much more strict, to ensure there is no <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF</a> attacks being attempted.</p><h3>Initialize Git Repository with Simple Git</h3><p>Next we will begin the process of creating a temporary repository in the serverless functions default directory for temporary files using the <a href="https://nodejs.org/api/os.html#os_os_tmpdir">os module</a> , adding a new branch, and committing the newly submitted comment so that, in a later step, a pull request for the new branch can be created programmatically.</p><pre><code class="hljs"><span class="hljs-comment">//Initialize Git Repository with Simple Git</span>

<span class="hljs-comment">// generate unique folder name for git repository</span>
<span class="hljs-keyword">const</span> tempRepo = uuidv4();

<span class="hljs-comment">// create empty directory to store comment file</span>
<span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments`</span>, {
  <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span>,
});

<span class="hljs-comment">// initialize simple-git</span>
<span class="hljs-keyword">const</span> git = simpleGit(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>`</span>);

<span class="hljs-comment">// initialize git repository in tempRepo</span>
<span class="hljs-keyword">await</span> git.init();

<span class="hljs-comment">// set up git config</span>
<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
  git.addConfig(<span class="hljs-string">"user.name"</span>, <span class="hljs-string">"GITHUB_USERNAME"</span>),
  git.addConfig(<span class="hljs-string">"user.email"</span>, <span class="hljs-string">"GITHUB_EMAIL"</span>),
]);

<span class="hljs-comment">// add the private remote</span>
<span class="hljs-keyword">await</span> git.addRemote(
  <span class="hljs-string">"private"</span>,
  <span class="hljs-string">`https://GITHUB_USERNAME:<span class="hljs-subst">${process.env[<span class="hljs-string">"GitHubUserPassword"</span>]}</span>@https://github.com/GITHUB_USERNAME/PRIVATE_REPOSITORY`</span>
);
</code></pre><p>Since this code resides within a serverless function there is no state that is saved in between requests. This requires creating a unique folder and initializing a new git repository every time the serverless function is activated. Once the git repo is initialized in a temp folder the user name and email are configured. These currently set to "GITHUB_USERNAME" and "GITHUB_EMAIL" should be updated to match your account information.</p><p>Once the git config is set, a remote is added to reference the private repository that was created earlier. For convenience the remote is named "private", although this can be changed to something more suitable in your case. GitHub requires authentication for private repositories, so the GitHub account password is accessed as an environment variable, similar to the SendGrid API key set up previously. When adding the password application setting it is also a good idea to use a <a href="https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token">GitHub personal access token</a> (PAT) instead of your main GitHub account password. The GitHub PAT can be included the same way a regular password would be.</p><h3>Checkout Git Branch with Simple Git</h3><pre><code class="hljs"><span class="hljs-comment">//Checkout git branch with Simple Git</span>

<span class="hljs-comment">// generate unique id for comment</span>
<span class="hljs-keyword">const</span> commentId = uuidv4();

<span class="hljs-comment">// create branch</span>
<span class="hljs-keyword">try</span> {
  <span class="hljs-comment">// fetch main branch to base of off</span>
  <span class="hljs-keyword">await</span> git.fetch(<span class="hljs-string">"private"</span>, <span class="hljs-string">"main"</span>);

  <span class="hljs-comment">// use postId to see if comments already are saved for this post</span>
  <span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"private/main"</span>, [<span class="hljs-string">"--"</span>, <span class="hljs-string">`comments/<span class="hljs-subst">${body.postId}</span>.json`</span>]);

  <span class="hljs-comment">// create new branch named with commentID based off main branch</span>
  <span class="hljs-keyword">await</span> git.checkoutBranch(<span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>, <span class="hljs-string">"private/main"</span>);
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">// no previous comments are saved for this post</span>
  <span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"private/main"</span>);
  <span class="hljs-keyword">await</span> git.checkoutLocalBranch(<span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>);
}
</code></pre><p>Each comment needs a unique identifier, and the uuid npm package is used to generate a GUID that we save the the commentId variable. The code that follows is contained in a try catch block, because in the case of a brand new comment there will not be a file corresponding to the post that contains the comments previously submitted. In this case the checkout of the JSON file with the name of the postId from the parsed request body will throw an error because git will indicate that this file does not exist.</p><p>In either case of appending a comment to an existing list or committing the first one, the end result of the try catch block will be a new branch checked out with the name of the commentId that was just generated. Be sure to note the difference between checkoutBranch and checkoutLocalBranch in the Simple Git <a href="https://github.com/steveukx/git-js#git-checkout">git checkout documentation</a>.</p><h3>Write JSON File</h3><pre><code class="hljs"><span class="hljs-comment">// Write JSON File with updated Comment data</span>

<span class="hljs-comment">// create comment object to store as JSON in git repository</span>
<span class="hljs-keyword">const</span> comment = {
  <span class="hljs-attr">id</span>: commentId,
  <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString()).getTime(),
  <span class="hljs-attr">authorEmail</span>: body.authorEmail,
  <span class="hljs-attr">authorName</span>: body.authorName,
  <span class="hljs-attr">bodyText</span>: body.comment,
};

<span class="hljs-comment">// list of all comments</span>
<span class="hljs-keyword">let</span> comments = [];

<span class="hljs-comment">// retrieve existing comments</span>
<span class="hljs-keyword">try</span> {
  comments = <span class="hljs-built_in">JSON</span>.parse(
    <span class="hljs-keyword">await</span> readFile(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/<span class="hljs-subst">${body.postId}</span>.json`</span>, <span class="hljs-string">"utf8"</span>)
  );
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">//no previous comments</span>
}

<span class="hljs-comment">// add newly submitted comment</span>
comments.push(comment);

<span class="hljs-comment">// update or create new comments file with new comment included</span>
<span class="hljs-keyword">await</span> writeFile(
  <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/<span class="hljs-subst">${body.postId}</span>.json`</span>,
  <span class="hljs-built_in">JSON</span>.stringify(comments, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
  <span class="hljs-string">"utf8"</span>
);
</code></pre><p>Now that the temporary git repository is configured and we have checked out a branch with the latest comments (if any exist), we can update the JSON file containing the comments to include the new one. First, an object is created that represents the new comment data. Then in the following try catch block we attempt to read and parse into JSON, the existing file with the name of the postId included in the request body, corresponding to the blog post commented on. In the event that this file does not exists there will be an error that is caught and the execution of the code can proceed. In this case when the file cannot be read, because it does not exist, it means that we have no comments saved previously similar to the try catch block used previously during the branch checkout.</p><p>Once the list of all comments is hydrated, or if it remains an empty array, the new comment can be added to it. Then the entire list of comments is written back to the same file corresponding the the postId, and the changes to this file are ready to be committed and pushed to the private git repository.</p><h3>Git Commit and Push to Private Repository</h3><pre><code class="hljs"><span class="hljs-comment">// stage file modifications, commit and push</span>

<span class="hljs-keyword">await</span> git.add(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/<span class="hljs-subst">${body.postId}</span>.json`</span>);

<span class="hljs-keyword">await</span> git.commit(<span class="hljs-string">`adding comment <span class="hljs-subst">${commentId}</span>`</span>);

<span class="hljs-keyword">await</span> git.push(<span class="hljs-string">"private"</span>, <span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>);

<span class="hljs-comment">// delete temporary repository</span>
<span class="hljs-keyword">await</span> rimraf(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/`</span>);
</code></pre><p>Here we are adding the modifications from the file we just wrote to, with the name of the postId, to the branch currently checked out with the name of the commentId, and then that branch is pushed to the private remote origin. Once the push is complete, the temporary directory we previously created is no longer needed, and the rimraf npm package is used to recursively delete the entire directory and its contents.</p><h3>Send Notification Emails and Create Pull Request with Octokit</h3><p>The last bit of code needed for the comment.ts function, will construct two emails, one to you and one to the reader that submitted the comment. It will also use the GitHub Octokit REST API client to create a pull request for the branch that was pushed with the new comment committed. This way the comment can be moderated before displaying publicly. To prevent the comment from being published the pull request can be declined and the branch with the comment can be deleted all within the GitHub interface.</p><pre><code class="hljs"><span class="hljs-comment">//send notifications and create pull request</span>

<span class="hljs-keyword">const</span> userEmail = {
  <span class="hljs-attr">to</span>: body.authorEmail,
  <span class="hljs-attr">from</span>: <span class="hljs-string">"YOUR_NAME@YOUR_WEBSITE"</span>,
  <span class="hljs-attr">subject</span>: <span class="hljs-string">"comment submitted"</span>,
  <span class="hljs-attr">text</span>: <span class="hljs-string">"Your comment will be visible when approved."</span>,
};

<span class="hljs-keyword">const</span> adminEmail = {
  <span class="hljs-attr">to</span>: <span class="hljs-string">"ADMIN_EMAIL"</span>,
  <span class="hljs-attr">from</span>: <span class="hljs-string">"ADMIN_EMAIL"</span>,
  <span class="hljs-attr">subject</span>: <span class="hljs-string">"comment submitted"</span>,
  <span class="hljs-attr">html</span>: <span class="hljs-string">`&lt;div&gt;from: <span class="hljs-subst">${body.authorName}</span>&lt;/div&gt;
         &lt;div&gt;email: <span class="hljs-subst">${body.authorEmail}</span>&lt;/div&gt;
         &lt;div&gt;comment: <span class="hljs-subst">${body.comment}</span>&lt;/div&gt;`</span>,
};

<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
  SendGrid.send(userEmail),
  SendGrid.send(adminEmail),
  <span class="hljs-keyword">new</span> Octokit({
    <span class="hljs-attr">auth</span>: process.env[<span class="hljs-string">"GitHubUserPassword"</span>],
  }).pulls.create({
    <span class="hljs-attr">owner</span>: <span class="hljs-string">"GITHUB_USERNAME"</span>,
    <span class="hljs-attr">repo</span>: <span class="hljs-string">"PRIVATE_REPOSITORY"</span>,
    <span class="hljs-attr">title</span>: <span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>,
    <span class="hljs-attr">head</span>: <span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>,
    <span class="hljs-attr">base</span>: <span class="hljs-string">"main"</span>,
  }),
]);
</code></pre><p>Both SendGrid.send() and Octokit.pulls.create() are asynchronous and return a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</a>. To take advantage of this we use Promise.all() to carry out all three actions: sending two emails and the HTTP Request to the GitHub REST API simultaneously. Using the await keyword ensures that all three promises are resolved before continuing.</p><p>When we put all these code sections together the result should look like this:</p><pre><code class="hljs"><span class="hljs-comment">// comment.ts</span>

<span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> uuidv4 = <span class="hljs-built_in">require</span>(<span class="hljs-string">"uuid/v4"</span>);
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SendGrid <span class="hljs-keyword">from</span> <span class="hljs-string">"@sendgrid/mail"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> simpleGit <span class="hljs-keyword">from</span> <span class="hljs-string">"simple-git/promise"</span>;
<span class="hljs-keyword">import</span> { formHelpers } <span class="hljs-keyword">from</span> <span class="hljs-string">"../common/formHelpers"</span>;
<span class="hljs-keyword">import</span> { Octokit } <span class="hljs-keyword">from</span> <span class="hljs-string">"@octokit/rest"</span>;
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> rimrafstd = <span class="hljs-built_in">require</span>(<span class="hljs-string">"rimraf"</span>);
<span class="hljs-keyword">import</span> { tmpdir } <span class="hljs-keyword">from</span> <span class="hljs-string">"os"</span>;
<span class="hljs-keyword">const</span> rimraf = util.promisify(rimrafstd);
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);
SendGrid.setApiKey(process.env[<span class="hljs-string">"SendGridApiKey"</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-keyword">const</span> body = querystring.parse(req.body);

  <span class="hljs-keyword">if</span> (
    !(
      body &amp;&amp;
      body.comment &amp;&amp;
      body.postGuid &amp;&amp;
      body.authorEmail &amp;&amp;
      body.authorName
    )
  ) {
    context.res!.status = <span class="hljs-number">400</span>;
    context.res!.body = {
      <span class="hljs-attr">message</span>: <span class="hljs-string">"Comment invalid. Please correct errors and try again."</span>,
    };
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">//Initialize Git Repository with Simple Git</span>

  <span class="hljs-comment">// generate unique folder name for git repository</span>
  <span class="hljs-keyword">const</span> tempRepo = uuidv4();

  <span class="hljs-comment">// create empty directory to store comment file</span>
  <span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments`</span>, {
    <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span>,
  });

  <span class="hljs-comment">// initialize simple-git</span>
  <span class="hljs-keyword">const</span> git = simpleGit(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>`</span>);

  <span class="hljs-comment">// initialize git repository in tempRepo</span>
  <span class="hljs-keyword">await</span> git.init();

  <span class="hljs-comment">// set up git config</span>
  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
    git.addConfig(<span class="hljs-string">"user.name"</span>, <span class="hljs-string">"GITHUB_USERNAME"</span>),
    git.addConfig(<span class="hljs-string">"user.email"</span>, <span class="hljs-string">"GITHUB_EMAIL"</span>),
  ]);

  <span class="hljs-comment">// add the private remote</span>
  <span class="hljs-keyword">await</span> git.addRemote(
    <span class="hljs-string">"private"</span>,
    <span class="hljs-string">`https://GITHUB_USERNAME:<span class="hljs-subst">${process.env[<span class="hljs-string">"GitHubUserPassword"</span>]}</span>@https://github.com/GITHUB_USERNAME/PRIVATE_REPOSITORY`</span>
  );

  <span class="hljs-comment">//Checkout git branch with Simple Git</span>

  <span class="hljs-comment">// generate unique id for comment</span>
  <span class="hljs-keyword">const</span> commentId = uuidv4();

  <span class="hljs-comment">// create branch</span>
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// fetch main branch to base of off</span>
    <span class="hljs-keyword">await</span> git.fetch(<span class="hljs-string">"private"</span>, <span class="hljs-string">"main"</span>);

    <span class="hljs-comment">// use postID to see if comments already are saved for this post</span>
    <span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"private/main"</span>, [<span class="hljs-string">"--"</span>, <span class="hljs-string">`comments/<span class="hljs-subst">${body.postId}</span>.json`</span>]);

    <span class="hljs-comment">// create new branch named with commentID based off main branch</span>
    <span class="hljs-keyword">await</span> git.checkoutBranch(<span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>, <span class="hljs-string">"private/main"</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-comment">// no previous comments are saved for this post</span>
    <span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"private/main"</span>);
    <span class="hljs-keyword">await</span> git.checkoutLocalBranch(<span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>);
  }

  <span class="hljs-comment">// Write JSON File with updated Comment data</span>

  <span class="hljs-comment">// create comment object to store as JSON in git repository</span>
  <span class="hljs-keyword">const</span> comment = {
    <span class="hljs-attr">id</span>: commentId,
    <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toUTCString()).getTime(),
    <span class="hljs-attr">authorEmail</span>: body.authorEmail,
    <span class="hljs-attr">authorName</span>: body.authorName,
    <span class="hljs-attr">bodyText</span>: body.comment,
  };

  <span class="hljs-comment">// list of all comments</span>
  <span class="hljs-keyword">let</span> comments = [];

  <span class="hljs-comment">// retrieve existing comments</span>
  <span class="hljs-keyword">try</span> {
    comments = <span class="hljs-built_in">JSON</span>.parse(
      <span class="hljs-keyword">await</span> readFile(
        <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/<span class="hljs-subst">${body.postId}</span>.json`</span>,
        <span class="hljs-string">"utf8"</span>
      )
    );
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-comment">//no previous comments</span>
  }

  <span class="hljs-comment">// add newly submitted comment</span>
  comments.push(comment);

  <span class="hljs-comment">// update or create new comments file with new comment included</span>
  <span class="hljs-keyword">await</span> writeFile(
    <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/<span class="hljs-subst">${body.postId}</span>.json`</span>,
    <span class="hljs-built_in">JSON</span>.stringify(comments, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
    <span class="hljs-string">"utf8"</span>
  );

  <span class="hljs-comment">// stage file modifications, commit and push</span>

  <span class="hljs-keyword">await</span> git.add(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/<span class="hljs-subst">${body.postId}</span>.json`</span>);

  <span class="hljs-keyword">await</span> git.commit(<span class="hljs-string">`adding comment <span class="hljs-subst">${commentId}</span>`</span>);

  <span class="hljs-keyword">await</span> git.push(<span class="hljs-string">"private"</span>, <span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>);

  <span class="hljs-comment">// delete temporary repository</span>
  <span class="hljs-keyword">await</span> rimraf(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/`</span>);

  <span class="hljs-comment">//send notifications and create pull request</span>

  <span class="hljs-keyword">const</span> userEmail = {
    <span class="hljs-attr">to</span>: body.authorEmail,
    <span class="hljs-attr">from</span>: <span class="hljs-string">"YOUR_NAME@YOUR_WEBSITE"</span>,
    <span class="hljs-attr">subject</span>: <span class="hljs-string">"comment submitted"</span>,
    <span class="hljs-attr">text</span>: <span class="hljs-string">"Your comment will be visible when approved."</span>,
  };

  <span class="hljs-keyword">const</span> adminEmail = {
    <span class="hljs-attr">to</span>: <span class="hljs-string">"ADMIN_EMAIL"</span>,
    <span class="hljs-attr">from</span>: <span class="hljs-string">"ADMIN_EMAIL"</span>,
    <span class="hljs-attr">subject</span>: <span class="hljs-string">"comment submitted"</span>,
    <span class="hljs-attr">html</span>: <span class="hljs-string">`&lt;div&gt;from: <span class="hljs-subst">${body.authorName}</span>&lt;/div&gt;
           &lt;div&gt;email: <span class="hljs-subst">${body.authorEmail}</span>&lt;/div&gt;
           &lt;div&gt;comment: <span class="hljs-subst">${body.comment}</span>&lt;/div&gt;`</span>,
  };

  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
    SendGrid.send(userEmail),
    SendGrid.send(adminEmail),
    <span class="hljs-keyword">new</span> Octokit({
      <span class="hljs-attr">auth</span>: process.env[<span class="hljs-string">"GitHubUserPassword"</span>],
    }).pulls.create({
      <span class="hljs-attr">owner</span>: <span class="hljs-string">"GITHUB_USERNAME"</span>,
      <span class="hljs-attr">repo</span>: <span class="hljs-string">"PRIVATE_REPOSITORY"</span>,
      <span class="hljs-attr">title</span>: <span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>,
      <span class="hljs-attr">head</span>: <span class="hljs-string">`<span class="hljs-subst">${commentId}</span>`</span>,
      <span class="hljs-attr">base</span>: <span class="hljs-string">"main"</span>,
    }),
  ]);

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = {
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Success!"</span>,
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>At this point, we have one of the two serverless functions completed! Next we will need a way to moderate comments that are submitted to the comment.ts function shown above. To do this another serverless function will be used, which we will name "comment-merge.ts". The goal of this function will be to integrate moderated comments into the public repo that was created initially, and to filter out any sensitive data that should not be publicly displayed.</p><h3>GitHub Webhook</h3><p>Before beginning the code of the comment-merge.ts function a <a href="https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/about-webhooks">GitHub webhook</a> needs to be created that will send a POST request on pull request events. In the private repository settings on GitHub <a href="https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/creating-webhooks#setting-up-a-webhook">add a webhook</a> that points to the serverless function url, and select only the pull request event rather than the default of activating for all of the event types. This will enable the comment-merge.ts function to be activated anytime we accept one of the pull requests created as a result of a new comment submission.</p><p>Now that the GitHub webhook is configured to listen for pull request events occurring in the private repository we can set up the second serverless function to act on these events. There is one additional npm package that will be needed for this function, and it can be installed by running the command <kbd>npm install glob @types/glob --save-dev</kbd>. This will install the glob npm package and the corresponding types.</p><p>The same beginning code from the first function can be used for the merge function, so we can skip ahead a bit and look at the imports that will be needed.</p><pre><code class="hljs"><span class="hljs-comment">// comment-merge.ts</span>
<span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> simpleGit <span class="hljs-keyword">from</span> <span class="hljs-string">"simple-git/promise"</span>;
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> { tmpdir } <span class="hljs-keyword">from</span> <span class="hljs-string">"os"</span>;
<span class="hljs-keyword">import</span> uuidv4 = <span class="hljs-built_in">require</span>(<span class="hljs-string">"uuid/v4"</span>);
<span class="hljs-keyword">import</span> globstd = <span class="hljs-built_in">require</span>(<span class="hljs-string">"glob"</span>);
<span class="hljs-keyword">import</span> rimrafstd = <span class="hljs-built_in">require</span>(<span class="hljs-string">"rimraf"</span>);
<span class="hljs-keyword">const</span> rimraf = util.promisify(rimrafstd);
<span class="hljs-keyword">const</span> glob = util.promisify(globstd);
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);
</code></pre><p>These should look similar to the first function, with the glob package also being imported.</p><h3>Validate GitHub Webhook Post Request</h3><p>Now we can add code that will parse the request body that is sent from the GitHub webhook. The webhook is sent with the data needed as the value of the payload property. Like the request body of our initial comment function the querystring package is used to parse the payload and then JSON.parse is used to create an object representing the data.</p><pre><code class="hljs"><span class="hljs-comment">// validate github webhook payload</span>

<span class="hljs-comment">//request content type is configured in GitHub webhook settings</span>
<span class="hljs-keyword">const</span> payload = req.body;

<span class="hljs-keyword">if</span> (
  payload.action != <span class="hljs-string">"closed"</span> ||
  payload.pull_request.base.ref != <span class="hljs-string">"main"</span> ||
  !payload.pull_request.merged_at
) {
  <span class="hljs-keyword">return</span>;
}
</code></pre><p>Since this webhook activates on any event regarding a pull request, whether that be opening or closing, we need to make sure that this code only runs when the pull request is closed. Secondly, the pull request branch needs to match the main branch so that pull requests from other branches are ignored. Lastly, the merged_at value is checked to make sure this pull request has been merged before closing. If the pull request is closed and not merged (the comment is spam) we can ignore the following post request sent by GitHub.</p><p>In addition to checking the payload properties shown above, it is a good idea to <a href="https://www.devextent.com/secure-github-webhook-nodejs/">secure the webhook</a> to make sure the serverless function is only activating when a request is sent from GitHub. This can prevent unwanted requests from being processed, and is a good idea to include when running this code in a production environment.</p><h3>Add Public and Private GitHub Remotes</h3><pre><code class="hljs"><span class="hljs-comment">// create temp repo and add remotes</span>

<span class="hljs-keyword">const</span> tempRepo = uuidv4();

<span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments`</span>, {
  <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">const</span> git = simpleGit(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>`</span>);

<span class="hljs-keyword">await</span> git.init();

<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
  git.addConfig(<span class="hljs-string">"user.name"</span>, <span class="hljs-string">"GITHUB_USERNAME"</span>),
  git.addConfig(<span class="hljs-string">"user.email"</span>, <span class="hljs-string">"GITHUB_EMAIL"</span>),
]);

<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
  git.addRemote(
    <span class="hljs-string">"private"</span>,
    <span class="hljs-string">`https://GITHUB_USERNAME:<span class="hljs-subst">${process.env[<span class="hljs-string">"GitHubUserPassword"</span>]}</span>@https://github.com/GITHUB_USERNAME/PRIVATE_REPOSITORY`</span>
  ),
  git.addRemote(
    <span class="hljs-string">"public"</span>,
    <span class="hljs-string">`https://GITHUB_USERNAME:<span class="hljs-subst">${process.env[<span class="hljs-string">"GitHubUserPassword"</span>]}</span>@https://github.com/GITHUB_USERNAME/PUBLIC_REPOSITORY`</span>
  ),
]);
</code></pre><p>This code is nearly the same as the temporary git repo creation and initialization that was needed for the first function. The main difference is that two remotes are being added this time, one being the private repository where the comment is stored, and the second is the public repository where moderated comments will be merged into.</p><p>Make sure to include the username and password in the remote url for both the private and public remotes, even though for public GitHub repositories this is usually not necessary. This is a result of the Azure serverless function configuration requiring authentication in order to work as expected. If it is not included, when trying to push to the public repository after merging the comment, the git push will fail silently and the function will timeout.</p><h3>Git Checkout and Fetch</h3><p>After configuring the remotes some additional git commands are required to checkout the correct branches and fetch the latest file modifications.</p><pre><code class="hljs"><span class="hljs-comment">// fetch public and integrate with latest modifications from private repo</span>

<span class="hljs-keyword">await</span> git.fetch(<span class="hljs-string">"public"</span>, <span class="hljs-string">"main"</span>);

<span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"main"</span>, [<span class="hljs-string">"--"</span>, <span class="hljs-string">"comments/"</span>]);

<span class="hljs-keyword">await</span> git.checkoutBranch(<span class="hljs-string">"main"</span>, <span class="hljs-string">"main"</span>);

<span class="hljs-keyword">await</span> git.fetch(<span class="hljs-string">"private"</span>, <span class="hljs-string">"main"</span>);

<span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"main"</span>, [<span class="hljs-string">"--"</span>, <span class="hljs-string">"comments/"</span>]);
</code></pre><p>This code first fetches the public remote so that the folder containing previously posted comments can be checked out. With the comment data from the main branch of the public repository now included in the temporary repository, the same fetch and checkout commands are used to integrate the private remote where the main branch includes comments that have passed moderation and their corresponding pull request has been merged.</p><h3>Filter Out Private Data</h3><p>Now that the temporary git repository has the newest comment, there may be information that should not be made public, like user emails. Before we commit and push the new comment to the public repository we can filter the comment data to remove any information that should not be public. This is also the point where the glob npm package will be utilized.</p><pre><code class="hljs"><span class="hljs-comment">// filter private data from comments</span>

<span class="hljs-comment">// retrieve comment file paths</span>
<span class="hljs-keyword">const</span> paths = <span class="hljs-keyword">await</span> glob(<span class="hljs-string">`comments/**/*.json`</span>, {
  <span class="hljs-attr">cwd</span>: <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/`</span>,
});

<span class="hljs-comment">// wait for all paths to process asynchronously</span>
<span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
  paths.map(<span class="hljs-keyword">async</span> (path) =&gt; {
    <span class="hljs-keyword">let</span> pathData = [];

    <span class="hljs-comment">//read JSON file with comment info</span>
    pathData = <span class="hljs-built_in">JSON</span>.parse(
      <span class="hljs-keyword">await</span> readFile(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/<span class="hljs-subst">${path}</span>`</span>, <span class="hljs-string">"utf8"</span>)
    );

    <span class="hljs-comment">// filter out private info</span>
    <span class="hljs-keyword">const</span> publicData = pathData.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> { authorEmail, ...store } = item;
      <span class="hljs-keyword">return</span> store;
    });

    <span class="hljs-comment">// write file back to original with private data removed</span>
    <span class="hljs-keyword">await</span> writeFile(
      <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/<span class="hljs-subst">${path}</span>`</span>,
      <span class="hljs-built_in">JSON</span>.stringify(publicData, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
      <span class="hljs-string">"utf8"</span>
    );
  })
);
</code></pre><p>This code gets all the paths for the files where comments are stored. Then each path is processed and the file in the temporary folder is read and JSON.parse is used to create an object that we can remove any private data from before publishing. In this case the authorEmail key/value pair is being removed from the comment object, using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment">destructuring assignment syntax</a>, and any remaining properties are kept in place. The filtered data is then written back to the file matching the path using JSON.stringify to retain the original formatting.</p><h3>Git Commit and Push to Public Repository</h3><pre><code class="hljs"><span class="hljs-comment">// add filtered comment file modifications, commit, and push</span>

<span class="hljs-keyword">await</span> git.add(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/*.json`</span>);

<span class="hljs-keyword">await</span> git.commit(<span class="hljs-string">"approving comment"</span>);

<span class="hljs-keyword">await</span> git.push(<span class="hljs-string">"public"</span>, <span class="hljs-string">"main"</span>);

<span class="hljs-keyword">await</span> rimraf(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/`</span>);
</code></pre><p>The last part of the comment merge function includes adding the modifications made the to the comment files to include the new comment with private data filtered out, and committing those changes to the main branch. Once the changes are committed the branch is pushed to the public repository and the comment can now be displayed.</p><p>In the case where a static site generator is being used for the blog, this push can trigger a new build and the comment can be included by the build process. The last thing to do, as done in the first function, is to delete the temporary git repository folder since it is no longer needed for the duration of this request.</p><p>The comment-merge.ts with all code added should look like this:</p><pre><code class="hljs"><span class="hljs-comment">// comment-merge.ts</span>
<span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> util = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> simpleGit <span class="hljs-keyword">from</span> <span class="hljs-string">"simple-git/promise"</span>;
<span class="hljs-keyword">import</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);
<span class="hljs-keyword">import</span> { tmpdir } <span class="hljs-keyword">from</span> <span class="hljs-string">"os"</span>;
<span class="hljs-keyword">import</span> uuidv4 = <span class="hljs-built_in">require</span>(<span class="hljs-string">"uuid/v4"</span>);
<span class="hljs-keyword">import</span> globstd = <span class="hljs-built_in">require</span>(<span class="hljs-string">"glob"</span>);
<span class="hljs-keyword">import</span> rimrafstd = <span class="hljs-built_in">require</span>(<span class="hljs-string">"rimraf"</span>);
<span class="hljs-keyword">const</span> rimraf = util.promisify(rimrafstd);
<span class="hljs-keyword">const</span> glob = util.promisify(globstd);
<span class="hljs-keyword">const</span> mkdir = util.promisify(fs.mkdir);
<span class="hljs-keyword">const</span> writeFile = util.promisify(fs.writeFile);
<span class="hljs-keyword">const</span> readFile = util.promisify(fs.readFile);

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  context.res!.headers[<span class="hljs-string">"Content-Type"</span>] = <span class="hljs-string">"application/json"</span>;

  <span class="hljs-comment">//request content type is configured in GitHub webhook settings</span>
  <span class="hljs-keyword">const</span> payload = req.body;

  <span class="hljs-keyword">if</span> (
    payload.action != <span class="hljs-string">"closed"</span> ||
    payload.pull_request.base.ref != <span class="hljs-string">"main"</span> ||
    !payload.pull_request.merged_at
  ) {
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">// create temp repo and add remotes</span>

  <span class="hljs-keyword">const</span> tempRepo = uuidv4();

  <span class="hljs-keyword">await</span> mkdir(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments`</span>, {
    <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span>,
  });

  <span class="hljs-keyword">const</span> git = simpleGit(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>`</span>);

  <span class="hljs-keyword">await</span> git.init();

  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
    git.addConfig(<span class="hljs-string">"user.name"</span>, <span class="hljs-string">"GITHUB_USERNAME"</span>),
    git.addConfig(<span class="hljs-string">"user.email"</span>, <span class="hljs-string">"GITHUB_EMAIL"</span>),
  ]);

  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
    git.addRemote(
      <span class="hljs-string">"private"</span>,
      <span class="hljs-string">`https://GITHUB_USERNAME:<span class="hljs-subst">${process.env[<span class="hljs-string">"GitHubUserPassword"</span>]}</span>@https://github.com/GITHUB_USERNAME/PRIVATE_REPOSITORY`</span>
    ),
    git.addRemote(
      <span class="hljs-string">"public"</span>,
      <span class="hljs-string">`https://GITHUB_USERNAME:<span class="hljs-subst">${process.env[<span class="hljs-string">"GitHubUserPassword"</span>]}</span>@https://github.com/GITHUB_USERNAME/PUBLIC_REPOSITORY`</span>
    ),
  ]);

  <span class="hljs-comment">// fetch public and integrate with latest modifications from private repo</span>

  <span class="hljs-keyword">await</span> git.fetch(<span class="hljs-string">"public"</span>, <span class="hljs-string">"main"</span>);

  <span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"main"</span>, [<span class="hljs-string">"--"</span>, <span class="hljs-string">"comments/"</span>]);

  <span class="hljs-keyword">await</span> git.checkoutBranch(<span class="hljs-string">"main"</span>, <span class="hljs-string">"main"</span>);

  <span class="hljs-keyword">await</span> git.fetch(<span class="hljs-string">"private"</span>, <span class="hljs-string">"main"</span>);

  <span class="hljs-keyword">await</span> git.checkout(<span class="hljs-string">"main"</span>, [<span class="hljs-string">"--"</span>, <span class="hljs-string">"comments/"</span>]);

  <span class="hljs-comment">// filter private data from comments</span>

  <span class="hljs-comment">// retrieve comment file paths</span>
  <span class="hljs-keyword">const</span> paths = <span class="hljs-keyword">await</span> glob(<span class="hljs-string">`comments/**/*.json`</span>, {
    <span class="hljs-attr">cwd</span>: <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/`</span>,
  });

  <span class="hljs-comment">// wait for all paths to process asynchronously</span>
  <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
    paths.map(<span class="hljs-keyword">async</span> (path) =&gt; {
      <span class="hljs-keyword">let</span> pathData = [];

      <span class="hljs-comment">//read JSON file with comment info</span>
      pathData = <span class="hljs-built_in">JSON</span>.parse(
        <span class="hljs-keyword">await</span> readFile(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/<span class="hljs-subst">${path}</span>`</span>, <span class="hljs-string">"utf8"</span>)
      );

      <span class="hljs-comment">// filter out private info</span>
      <span class="hljs-keyword">const</span> publicData = pathData.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { authorEmail, ...store } = item;
        <span class="hljs-keyword">return</span> store;
      });

      <span class="hljs-comment">// write file back to original with private data removed</span>
      <span class="hljs-keyword">await</span> writeFile(
        <span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/<span class="hljs-subst">${path}</span>`</span>,
        <span class="hljs-built_in">JSON</span>.stringify(publicData, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>),
        <span class="hljs-string">"utf8"</span>
      );
    })
  );

  <span class="hljs-comment">// add filtered comment file modifications, commit, and push</span>

  <span class="hljs-keyword">await</span> git.add(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/comments/*.json`</span>);

  <span class="hljs-keyword">await</span> git.commit(<span class="hljs-string">"approving comment"</span>);

  <span class="hljs-keyword">await</span> git.push(<span class="hljs-string">"public"</span>, <span class="hljs-string">"main"</span>);

  <span class="hljs-keyword">await</span> rimraf(<span class="hljs-string">`<span class="hljs-subst">${tmpdir}</span>/<span class="hljs-subst">${tempRepo}</span>/`</span>);

  context.res!.status = <span class="hljs-number">200</span>;
  context.res!.body = { <span class="hljs-attr">message</span>: <span class="hljs-string">"success"</span> };
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>A blog built with the Jamstack can now integrate comments in a way that is very cost effective and maintain a git-centric approach. The comments that readers submit can be moderated, filtered, and are stored right along side the blog content. This way the corresponding JSON files that are created can be integrated into an existing build process and dynamically pre-rendered with the content, eliminating the need to make client side requests to fetch data that would harm the user experience or affect page load time.</p><p>Azure serverless functions provide a cost effective way to have on demand cloud compute, without the need to have a server running all of the time, only to be used occasionally. One possible drawback of this approach is that sometimes, due to cold start delays of the the serverless function, when the user submits a comment it can be somewhat slow to process. This is a result of the comment.ts function, while asynchronous, initializing and checking out a git repository, sending two emails and utilizing the GitHub REST API to programmatically create a pull request. It may reduce processing times to remove the email notification component if not needed for your use case.</p>]]></description></item><item><title>Compile SASS with npm</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/npm-compile-sass/</link><guid isPermaLink="true">https://www.devextent.com/npm-compile-sass/</guid><description><![CDATA[<p><strong>UPDATE</strong>: <em>The steps in this post show how to compile sass using the <a href="https://www.npmjs.com/package/node-sass?activeTab=readme">node-sass</a> npm package, which is built on top of LibSass. <a href="https://sass-lang.com/blog/libsass-is-deprecated">LibSass is now deprecated</a> in favor of <a href="https://sass-lang.com/dart-sass">Dart Sass</a>, the new primary implementation of SASS. You will want to reference my other post that shows how to <a href="https://www.devextent.com/dart-sass-javascript-implementation-npm-compile-sass/">use the Dart Sass JavaScript implementation with npm</a> for the most current way of using npm to compile SASS.</em></p><hr><p>There are many different ways to compile <a href="https://stackoverflow.com/questions/5654447/whats-the-difference-between-scss-and-sass">SCSS</a>, one of the <a href="https://sass-lang.com/documentation/syntax">two syntaxes supported by SASS</a>. In this post we will explore the utilization of the <a href="https://www.npmjs.com/package/node-sass?activeTab=readme">node-sass</a> npm package. We'll also look at how we can use the <a href="https://www.npmjs.com/package/clean-css">clean-css</a> npm package to minify and optimize the generated output after compiling SCSS into CSS. Both of these techniques are similar to how Bootstrap handles the <a href="https://github.com/twbs/bootstrap/blob/622c914a3acc1ab933b3e89d8abfdd63feeb4016/package.json#L25">compilation</a> and <a href="https://github.com/twbs/bootstrap/blob/622c914a3acc1ab933b3e89d8abfdd63feeb4016/package.json#L29">minification</a> of its SCSS files. Please make sure you have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed first.</p><h3>SCSS</h3><p>First an SCSS file is needed, and it can be placed in the root of the project folder. To illustrate the preprocessing of our SCSS file into CSS let's add some style rules that are intentionally utilizing the SCSS syntax. We'll look to the <a href="https://sass-lang.com/guide">Sass Basics guide</a> for some code snippets.</p><pre><code class="hljs"><span class="hljs-comment">// some variables</span>
<span class="hljs-variable">$font-stack</span>: Helvetica, sans-serif;
<span class="hljs-variable">$primary-color</span>: <span class="hljs-number">#333</span>;

<span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">font</span>: <span class="hljs-number">100%</span> <span class="hljs-variable">$font-stack</span>;
  <span class="hljs-attribute">color</span>: <span class="hljs-variable">$primary-color</span>;
}

<span class="hljs-comment">// some nesting</span>
<span class="hljs-selector-tag">nav</span> {
  <span class="hljs-selector-tag">ul</span> {
    <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">list-style</span>: none;
  }

  <span class="hljs-selector-tag">li</span> {
    <span class="hljs-attribute">display</span>: inline-block;
  }

  <span class="hljs-selector-tag">a</span> {
    <span class="hljs-attribute">display</span>: block;
    <span class="hljs-attribute">padding</span>: <span class="hljs-number">6px</span> <span class="hljs-number">12px</span>;
    <span class="hljs-attribute">text-decoration</span>: none;
  }
}

<span class="hljs-comment">// a mixin</span>
<span class="hljs-keyword">@mixin</span> transform(<span class="hljs-variable">$property</span>) {
  -webkit-<span class="hljs-attribute">transform</span>: <span class="hljs-variable">$property</span>;
  -ms-<span class="hljs-attribute">transform</span>: <span class="hljs-variable">$property</span>;
  <span class="hljs-attribute">transform</span>: <span class="hljs-variable">$property</span>;
}

<span class="hljs-selector-class">.box</span> {
  <span class="hljs-keyword">@include</span> transform(rotate(<span class="hljs-number">30deg</span>));
}
</code></pre><p>Now that we have an SCSS file to process, the next step involves configuring the package.json to install the necessary dependencies and provide a way to compile SCSS with Node.js by adding custom scripts.</p><h3>package.json</h3><p>Using the scripts section of an <a href="https://docs.npmjs.com/files/package.json">npm package.json</a> file we can execute a series of commands to carry out the compilation of SCSS and optimize the resulting CSS output. A package.json file is required, and can be created running the command <kbd>npm init</kbd> in the project folder and following the prompts, or copying below.</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npmcompilesass"</span>,
  <span class="hljs-attr">"scripts"</span>: {}
}
</code></pre><p>Next we'll need to add two packages into the devDependencies of our project. To do so run the following command <kbd>npm install node-sass clean-css-cli --save-dev</kbd>. What will occur is that the node-sass and clean-css npm packages will be installed to the devDependencies of the project. You should also see a node modules folder appear in the root of the project, and there should also be a <a href="https://docs.npmjs.com/files/package-lock.json">package-lock.json</a> file that was created.</p><p>Your package.json file should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npmcompilesass"</span>,
  <span class="hljs-attr">"scripts"</span>: {},
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"clean-css-cli"</span>: <span class="hljs-string">"^4.3.0"</span>,
    <span class="hljs-attr">"node-sass"</span>: <span class="hljs-string">"^4.12.0"</span>
  }
}
</code></pre><p>If for some reason your file looks different, you can copy the above and run the command <kbd>npm install</kbd>. This will reinstall both packages.</p><h3>Compile Sass to CSS using node-sass</h3><p>With the dependencies available we can add a script to compile the SCSS file created earlier with the node-sass npm package.</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npmcompilesass"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-styles"</span>: <span class="hljs-string">"node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 styles.scss dist/styles.css"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"clean-css-cli"</span>: <span class="hljs-string">"^4.3.0"</span>,
    <span class="hljs-attr">"node-sass"</span>: <span class="hljs-string">"^4.12.0"</span>
  }
}
</code></pre><p>Unfortunately, <a href="https://stackoverflow.com/questions/36258456/how-can-i-write-multiline-scripts-in-npm-scripts">multi-line npm scripts are not supported</a> so the script is quite long, and there are quite a few parameters passed. Luckily the <a href="https://github.com/sass/node-sass#command-line-interface">node-sass command line documentation</a> can provided detailed info on all of the possible parameters that are supported.</p><p>In this case parameters are used to indicate source maps should be generated (for debugging purposes), the amount of decimal precision is capped at 6, and the scss source file to process is styles.scss, which will be processed and output to a file named styles.css in a new folder named dist, located in the root of the project. The name of the dist folder can be changed if needed, and it will be created when the script runs if it does not already exist.</p><p>At this point we can run the compile styles script by running the command <kbd>npm run compile-styles</kbd>. However, this is only running node-sass and isn't minifying the css output, so we need to add another script to carry out the css optimization.</p><h3>Minify CSS with clean-css</h3><p>Like the node-sass package, we installed the clean-css package in the first step. To utilize it we'll add an additional script to the package.json file.</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npmcompilesass"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-styles"</span>: <span class="hljs-string">"node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 styles.scss dist/styles.css"</span>,
    <span class="hljs-attr">"css-minify"</span>: <span class="hljs-string">"cleancss --level 1 --format breaksWith=lf --source-map --source-map-inline-sources --output dist/styles.min.css dist/styles.css"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"clean-css-cli"</span>: <span class="hljs-string">"^4.3.0"</span>,
    <span class="hljs-attr">"node-sass"</span>: <span class="hljs-string">"^4.12.0"</span>
  }
}
</code></pre><p>Similar to the compile-styles script, the css-minify script is also quite long and contains many parameters. More info on all the parameters can be found at the <a href="https://github.com/jakubpawlowicz/clean-css-cli#cli-options">clean-css-cli GitHub repo</a>. The parameters being passed in indicate to run clean-css with a certain level of optimization, line break formatting, and to include source maps with the optimized output. The file to optimize is the styles.css file located in the dist folder that was generated by the compile-styles command, and the optimized output will be written to styles.min.css in the same folder. Now that all the required scripts have been added to the package.json file we can first compile, and then minify the scss source, by running the command <kbd>npm run compile-styles</kbd> followed by the command <kbd>npm run css-minify</kbd>. Then looking in the dist folder that was created there should be four files:</p><ul><li>styles.css</li><li>styles.css.map</li><li>styles.min.css</li><li>styles.min.css.map</li></ul><p>The two files we are most interested in are styles.css and styles.min.css. These are the browser compatible style sheets that can now be linked in any HTML file.</p><h3>CSS</h3><p>To make sure everything worked correctly your styles.css file should look like this:</p><pre><code class="hljs"><span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">font</span>: <span class="hljs-number">100%</span> Helvetica, sans-serif;
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>;
}

<span class="hljs-selector-tag">nav</span> <span class="hljs-selector-tag">ul</span> {
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">list-style</span>: none;
}

<span class="hljs-selector-tag">nav</span> <span class="hljs-selector-tag">li</span> {
  <span class="hljs-attribute">display</span>: inline-block;
}

<span class="hljs-selector-tag">nav</span> <span class="hljs-selector-tag">a</span> {
  <span class="hljs-attribute">display</span>: block;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">6px</span> <span class="hljs-number">12px</span>;
  <span class="hljs-attribute">text-decoration</span>: none;
}

<span class="hljs-selector-class">.box</span> {
  -webkit-<span class="hljs-attribute">transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">30deg</span>);
  -ms-<span class="hljs-attribute">transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">30deg</span>);
  <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">rotate</span>(<span class="hljs-number">30deg</span>);
}

<span class="hljs-comment">/*# sourceMappingURL=styles.css.map */</span>
</code></pre><p>You can also verify the styles.min.css file because it should have identical content with all of the whitespace removed. Also take note that a comment is included at the bottom for the source map file. This can be left as is and allows for seeing the style rules in the original SCSS file from the browser's dev tools.</p><h3>Run npm Scripts Sequentially</h3><p>With the output being generated correctly, there is one additional step we can do to simplify the SCSS processing into one command. Looking back to the scripts section of the package.json file, let's add one more script to combine the other two.</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"npmcompilesass"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-styles"</span>: <span class="hljs-string">"node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 styles.scss dist/styles.css"</span>,
    <span class="hljs-attr">"css-minify"</span>: <span class="hljs-string">"cleancss --level 1 --format breaksWith=lf --source-map --source-map-inline-sources --output dist/styles.min.css dist/styles.css"</span>,
    <span class="hljs-attr">"process-styles"</span>: <span class="hljs-string">"npm run compile-styles &amp;&amp; npm run css-minify"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"clean-css-cli"</span>: <span class="hljs-string">"^4.3.0"</span>,
    <span class="hljs-attr">"node-sass"</span>: <span class="hljs-string">"^4.12.0"</span>
  }
}
</code></pre><p>Now by running the command <kbd>npm run process-styles</kbd>, the compile-styles and css-minify scripts will run in series, and it is no longer necessary to execute both scripts separately. The process-styles script is responsible for both compiling the SCSS into css output and minifying it for optimal use.</p>]]></description></item><item><title>Compile TypeScript with npm</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/npm-compile-typescript/</link><guid isPermaLink="true">https://www.devextent.com/npm-compile-typescript/</guid><description><![CDATA[<p>Npm package.json scripts can be used to run various commands. Here, we will learn how to run the TypeScript compiler to generate JavaScript output from TypeScript source files. Before we start, make sure you have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>TypeScript</h3><p>In a new folder, create a file named script.ts. Then, add some sample code so we can test whether the JavaScript output is being generated properly.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> msg: <span class="hljs-built_in">string</span> = <span class="hljs-string">"Hello World!"</span>;
<span class="hljs-built_in">console</span>.log(msg);
</code></pre><h3>TypeScript Compiler</h3><p>In the same folder, create a new file named tsconfig.json. Here is the TypeScript official documentation for configuring <a href="https://www.typescriptlang.org/docs/handbook/tsconfig-json.html">tsconfig.json</a>.</p><p>Your tsconfig.json file should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"output"</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"./*"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>]
}
</code></pre><p>This configuration tells the TypeScript compiler to look for source files in the root of your project, where your tsconfig.json is located. For any TypeScript files it finds there, it will output the compiled JavaScript to a new folder named output.</p><h3>package.json</h3><p>In the same folder create a package.json file. Here is the npm official documentation on creating a <a href="https://docs.npmjs.com/creating-a-package-json-file">package.json</a> file.</p><p>Then, add the name and version properties required. You will also need to add a property called scripts. This property contains the script instructions that we will use to compile the TypeScript we created. In this case, our compilation script is named compile-typescript, and it runs the command tsc. This is the default TypeScript command, and it will utilize the tsconfig.json we created.</p><p>Your package.json file should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-typescript"</span>: <span class="hljs-string">"tsc"</span>
  }
}
</code></pre><p>Now that package.json is created and the TypeScript compilation step is listed, we must save TypeScript as a dev dependency. This will give the npm task access.</p><h3>npm Install TypeScript</h3><p>To install TypeScript for this project in a terminal window, run the command: <kbd>npm install typescript --save-dev</kbd></p><p>After installing TypeScript, your package.json should look like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-typescript"</span>: <span class="hljs-string">"tsc"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^3.5.3"</span>
  }
}
</code></pre><h3>JavaScript</h3><p>In a terminal window, navigate to the source code folder you created. Then, run the following command: <kbd>npm run compile-typescript</kbd></p><p>Now, you should now see a new folder created named output, that contains one file named script.js. Notice how the output has defaulted to ES5 JavaScript, which is compatible with all major browsers.</p><p>Your script.js file should look like this:</p><pre><code class="hljs"><span class="hljs-keyword">var</span> msg = <span class="hljs-string">"Hello World"</span>;
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"msg"</span>);
</code></pre><h3>Run Node.js Script</h3><p>The script.js created as a result of running the "compile-typescript" command can now be run with Node.js. To do this another package.json script is added, which is named "start". The "start" script will run the node cli command which the path of the script.ts file is passed.</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-typescript"</span>: <span class="hljs-string">"tsc"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node ./output/script.js"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^3.5.3"</span>
  }
}
</code></pre><p>Run the start command by entering <kbd>npm run start</kbd> in a terminal window, and you should see the output "Hello World!" printed to the console.</p><h3>Run npm Scripts Sequentially</h3><p>To save time the "compile-typescript" and "start" commands can be combined into one command by modifying the start command to include this functionality.</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-typescript"</span>: <span class="hljs-string">"tsc"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run compile-typescript &amp;&amp; node ./output/script.js"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^3.5.3"</span>
  }
}
</code></pre><p>Now running the command <kbd>npm run start</kbd> will first run the "compile-typescript" command and then use node to run the script.js file that is output.</p>]]></description></item><item><title>Minify HTML with npm</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/npm-minify-html/</link><guid isPermaLink="true">https://www.devextent.com/npm-minify-html/</guid><description><![CDATA[<p>The <a href="https://www.npmjs.com/package/html-minifier">html-minifier</a> npm package provides a command line interface that makes it possible to minify HTML. This can be useful when working with a site built with the <a href="https://jamstack.org/">Jamstack</a>. One example of this scenario could be a site that uses a static site generator to output prerendered HTML files at build time. In this case, and especially when serving lots of content, minifying the HTML output can result in cost savings as well as performance improvements.</p><p>Before following the steps below make sure to have <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>HTML</h3><p>To demonstrate the features provided by the html-minifier package we will start out with a sample html file. We can name this file index.html, and save it to a folder called "src". The name of the file and containing folder will be needed in the following steps. For this example, the sample file contains different types of elements to highlight the effect of minification, especially in regard to how white space is maintained when using preformatted elements.</p><pre><code class="hljs"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>This is our sample html content<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Here is some paragraph text.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span>This text is preformatted.

There is more than one line in this text block.

    <span class="hljs-tag">&lt;<span class="hljs-name">code</span>&gt;</span>console.log("code block inside preformatted block.");<span class="hljs-tag">&lt;/<span class="hljs-name">code</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>some more text at the bottom<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre><p><strong>Note</strong>: A more common scenario than starting with a sample file might be applying the minification step to the output of a build process. If you are interested in seeing an example of how to generate HTML output, here is some info on how to <a href="https://www.devextent.com/ejs-render-file/">render EJS files with Node.js</a>. The steps in that article can be extended to create a static site generator, and the html-minifier package can be included and used as part of the build process.</p><h3>package.json</h3><p>Next we will want to set up the <a href="https://docs.npmjs.com/creating-a-package-json-file">package.json</a> file so that we can npm install the html-minifier package. If one is not already created, running the command <kbd>npm init</kbd> and following the prompts will create one. Once the package.json file is in place we can run the command <kbd>npm install html-minifier --save-dev</kbd> to install the html-minifier npm package.</p><p>Your package.json file should look similar to this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"your-package-name-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"html-minifier"</span>: <span class="hljs-string">"^4.0.0"</span>
  }
}
</code></pre><p>There may be some additional properties created if using the npm init command, and the default values can be left in place. If you did not use the npm init command you can copy the contents above and run the command <kbd>npm install</kbd>, which will install all the required dependencies.</p><p>Now that the html-minifier package is installed we need a way to utilize it from the command line. To do so, an npm scripts property can be added to the package.json file just created. We will need to add one script that will pass option arguments to the html-minifier package command line interface, and we can name this script "html-minify".</p><p>Here is what the package.json file should look like with the script added:</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"your-package-name-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"html-minify"</span>: <span class="hljs-string">"html-minifier --input-dir src --output-dir dist --file-ext html --remove-comments --collapse-whitespace --minify-js true --minify-css true"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"html-minifier"</span>: <span class="hljs-string">"^4.0.0"</span>
  }
}
</code></pre><p>Let's look at each of the options being passed in to the html-minifier cli, and see what each is specifying.</p><h3>html-minifier options</h3><p>The first option --input-dir is specifying the folder that our source html file is located. In this case the folder name is "src", which was created during the initial step. Following that, --output-dir is specifying the output directory where the minified html file will be added. In this case it is set to "dist", although this can be changed to any folder name.</p><p>The --file-ext option is set to html (in this example it is not needed), however if the input directory contains file types other than "html", errors may occur as a result of the attempted minification of those files. In the html-minifier github repository there is open issue to <a href="https://github.com/kangax/html-minifier/pull/1026">support multiple file extensions</a>. A possible workaround for the time being is to add multiple package.json scripts, with each one running a separate command for each of the individual file types that will be minified. Additionally there are many other minifier packages available on npm and one of those may be better suited for file types other than html.</p><p>The next two options: --remove-comments and --collapse-whitespace do exactly as they are named, and there is no value to pass to them. If comments need to be retained or white space non-collapsed, these options can be deleted and html-minifier will not alter these properties of the original file.</p><p>Depending on whether set to true or false (or not included as the default value is false), the last two options, --minify-js and --minify-css will minify the corresponding source of their type, only if included as inline style or script tags in the html being minified. It may also be good to know that the html-minifier options information states that <a href="https://www.npmjs.com/package/clean-css">clean-css</a> and <a href="https://www.npmjs.com/package/uglify-js">uglify-js</a> are used for the minification when these options are included.</p><p>To get a full list of all the options supported, it can be helpful to globally install the html-minifier package by running the command <kbd>npm install html-minifier -g</kbd> (this may require administrator access). With the package installed globally, running the command <kbd>html-minifier --help</kbd> will list all of the command line options, their value if applicable, and a short help text description.</p><h3>Minify HTML</h3><p>Now that the html-minify script is added and the options are configured, to use it run the command <kbd>npm run html-minify</kbd>. As a result a new folder called "dist" should have been created where the src folder is located. Within that folder should be the minified version of the index.html file initially created.</p><p>Here is what the minified html file should look like:</p><pre><code class="hljs">&lt;h1&gt;This is our sample html content&lt;/h1&gt;&lt;p&gt;Here is some paragraph text.&lt;/p&gt;&lt;pre&gt;This text is preformatted.

There is more than one line in this text block.

    &lt;code&gt;console.log("code block inside preformatted block.");&lt;/code&gt;
&lt;/pre&gt;&lt;div&gt;some more text at the bottom&lt;/div&gt;
</code></pre><p>Notice that the whitespace within the preformatted element is maintained. This is important as those sections need to keep their whitespace as originally formatted, so the html-minifier does not change the desired formatting. For other elements not intended to maintain whitespace they can be reduced to a single line, and the comment at the top has been removed as well. There is no inline Javascript of CSS in this example, but you can add some in and see the effect.</p><p>Using the html-minifier package can be helpful to reduce file size and increase performance for users, especially when using a slower internet connection. Even with the small file used for this example, the response size has decreased from 303 bytes to 275 bytes. This is a small amount, but the savings can add up in a typical scenario where file sizes are much larger.</p><p>There is also a <a href="http://kangax.github.io/html-minifier/">web based html minifier</a> made by the same package author.</p>]]></description></item><item><title>Run Git Commands with Node.js</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/run-git-command-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/run-git-command-nodejs/</guid><description><![CDATA[<p>If you're building a blog with the <a href="https://jamstack.org/">Jamstack</a> your content might be stored in a git repository. This can help to reduce overhead, since a database is no longer required, but presents other interesting challenges like displaying post metadata. This may include the date the post was created or the date it was last updated, information that can be helpful to readers and enhances the display of the post in search engine results. We can use Node.js to retrieve the information that is stored in each commit, as well as run other git commands with the help of the <a href="https://www.npmjs.com/package/simple-git">Simple Git</a> npm package.</p><p>Before getting started it may be helpful to checkout how to <a href="https://www.devextent.com/ejs-render-file/">render EJS files with Node.js</a>. The code below assumes a static build process and that the source is tracked in a git repository, as well as having <a href="https://nodejs.org/en/">Node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>Setup TypeScript</h3><p>If you're interested in more information about setting up Typescript, checkout this post that shows how to <a href="https://www.devextent.com/npm-compile-typescript/">compile TypeScript with npm</a>. There you can see how to create a package.json file and add a tsconfig.json to configure the TypeScript compiler.</p><p>Since we are using Node.js with TypeScript there are some modifications needed to the tsconfig.json file from the previous post.</p><p>Here is what your tsconfig.json file should look like, in order for the code that follows to work correctly.</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./output"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES6"</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"/*"</span>],
  <span class="hljs-attr">"exclude"</span>: []
}
</code></pre><p>This configuration instructs the TypeScript compiler to use commonjs modules and output code that targets the ES6 specification, which is needed since an async function will be needed to utilize the npm package we'll use to gather git file metadata.</p><h3>npm install Simple Git</h3><p>Next, the Simple Git npm package is needed so that it can be used to access the git metadata. Run the command <kbd>npm install simple-git --save-dev</kbd> in the terminal, and that will install the Simple Git package to the node_modules folder.</p><p>At this point the package.json file should look similar to this (the package versions might be slightly different):</p><pre><code class="hljs">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"package-name-goes-here"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"0.0.0"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"compile-typescript"</span>: <span class="hljs-string">"tsc"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"simple-git"</span>: <span class="hljs-string">"^1.129.0"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^3.7.5"</span>
  }
}
</code></pre><p><strong>Note</strong>: Since we are using TypeScript for this example, usually a type definition package is also required to be "npm installed" just like the actual package. In this case the Simple Git package includes the @types type declarations, so downloading a separate package is not needed.</p><h3>Use Simple Git with TypeScript</h3><p>With TypeScript and the npm package.json configured we can now create a TypeScript file, let's call it index.ts. This will contain the code that will access the git metadata of our post file. To get started the Simple Git npm package will be imported, and an async function will be needed to utilize the Simple Git package, immediately following the async build function is called so the result can be output.</p><pre><code class="hljs"><span class="hljs-comment">// index.ts</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> simpleGit <span class="hljs-keyword">from</span> <span class="hljs-string">"simple-git/promise"</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">build</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> git = simpleGit();
}

build();
</code></pre><p>Since we are using TypeScript the import declaration might look slightly different than expected. This approach is consistent with the Simple Git documentation. Additionally, make sure to import the promise and async compatible version simple-git/promise. Inside of the build function Simple Git is initialized, and the functions provided by the Simple Git API are ready for use. Simple Git may not provide all of the git functionality available from the command line, but it works well for more common usages. We can add some code that will retrieve the created date of a file (based on the first commit) and the last modified date (based on latest commit).</p><pre><code class="hljs"><span class="hljs-comment">// index.ts</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> simpleGit <span class="hljs-keyword">from</span> <span class="hljs-string">"simple-git/promise"</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">build</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> git = simpleGit();

  <span class="hljs-comment">//list commits</span>
  <span class="hljs-comment">// git log accepts an options object - from ts definition</span>
  <span class="hljs-comment">/*
    format?: T;
    file?: string;
    from?: string;
    multiLine?: boolean;
    symmetric?: boolean;
    to?: string;
  */</span>
  <span class="hljs-keyword">const</span> log = <span class="hljs-keyword">await</span> git.log({ <span class="hljs-attr">file</span>: <span class="hljs-string">`sample-post-page.html`</span> });

  <span class="hljs-comment">// get first commit date of file</span>
  <span class="hljs-keyword">const</span> createdDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(log.all.slice(-<span class="hljs-number">1</span>)[<span class="hljs-number">0</span>].date);

  <span class="hljs-comment">// get latest modified date of file</span>
  <span class="hljs-keyword">const</span> modifiedDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(log.latest.date);

  <span class="hljs-comment">// output formatted time stamps</span>
  <span class="hljs-built_in">console</span>.log(createdDate.toLocaleDateString());
  <span class="hljs-built_in">console</span>.log(modifiedDate.toLocaleDateString());
}

build();
</code></pre><p>Instead of just outputting this git metadata to the console it can be assigned to a variable and then included in rendered content output for the reader to view.</p><p>The Simple Git API provides a lot of other examples for the functionality it provides. In this example the focus was on how we can gather the created and last modified dates of a file representing post content that is included in a static build process like one might find in use for a site built with the Jamstack.</p>]]></description></item><item><title>Send Email with Node.js</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/send-email-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/send-email-nodejs/</guid><description><![CDATA[<p>There are a variety of ways to send an email with Node.js. One way is to utilize the email service offered by <a href="https://sendgrid.com/">SendGrid</a>. The <a href="https://sendgrid.com/solutions/email-api/">email API</a> has a <a href="https://sendgrid.com/pricing/">free plan</a>, which does have a usage limit, specified on their website, but it should be enough for example purposes. To use the <a href="https://www.npmjs.com/package/@sendgrid/mail">SendGrid Mail Service npm package</a>, an API key is required which can be obtained by <a href="https://signup.sendgrid.com/">creating a new SendGrid account</a>.</p><h3>SendGrid API Key</h3><p>If you are having trouble creating an API key, please view the <a href="https://sendgrid.com/docs/ui/account-and-settings/api-keys/">API Keys documentation</a> provided by SendGrid. With the API key obtained, we can begin writing code that will utilize the free SendGrid service. You should not "hardcode" your API key into your application code. A more secure way of granting the application access to your account API key is to store it as an environment variable.</p><h3>Azure Serverless Function</h3><p>In order to send the email we can use a serverless function, for this example we will use a <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node">JavaScript Azure Function</a>. The serverless function will accept an HTTP post request and trigger the sending of an email to the address provided in the form submission. In order to do this we will need an HTML form. Before using the following code, it's a good idea to check out this other post on <a href="https://www.devextent.com/fetch-api-post-formdata-object/">Submitting Form Data with the Fetch API</a>.</p><p>Once the client side code is setup to post a form with the email address and email message, we can set up the serverless function to handle sending the email with the information from the form submission.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  <span class="hljs-comment">// form data submitted as query string in request body</span>
  <span class="hljs-keyword">const</span> body = querystring.parse(req.body);
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>This is the base code needed for the Azure serverless function. Below we will look to see how the data contained in the request body will be used to generate the email.</p><h3>Send Email with SendGrid and Node.js</h3><p><strong>Note</strong>: This code is setup as if the request body contains two keys, one for emailAddress and one for emailMessage. Additionally, the SendGrid API key obtained earlier is accessed here from an environment variable. See the Azure documentation to <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings">add an application setting</a>. Application settings are accessed as environment variables in the serverless function code.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> { AzureFunction, Context, HttpRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"@azure/functions"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> querystring <span class="hljs-keyword">from</span> <span class="hljs-string">"querystring"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SendGrid <span class="hljs-keyword">from</span> <span class="hljs-string">"@sendgrid/mail"</span>;
SendGrid.setApiKey(process.env[<span class="hljs-string">"SendGridApiKey"</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);

<span class="hljs-keyword">const</span> httpTrigger: AzureFunction = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">
  context: Context,
  req: HttpRequest
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  context.log(<span class="hljs-string">"HTTP trigger function processed a request."</span>);

  <span class="hljs-comment">// form data submitted as query string in request body</span>
  <span class="hljs-keyword">const</span> body = querystring.parse(req.body);

  <span class="hljs-comment">// check to make sure form was submitted with field data entered</span>
  <span class="hljs-keyword">if</span> (body &amp;&amp; body.emailAddress &amp;&amp; body.emailMessage) {
    <span class="hljs-comment">// create an email options object</span>
    <span class="hljs-keyword">const</span> email = {
      <span class="hljs-attr">to</span>: process.env[<span class="hljs-string">"SendGridApiKey"</span>],
      <span class="hljs-attr">from</span>: <span class="hljs-string">"noreply@yourdomain.com"</span>,
      <span class="hljs-attr">subject</span>: <span class="hljs-string">"Hello! This email was sent with Node.js"</span>,
      <span class="hljs-attr">html</span>: <span class="hljs-string">`&lt;div&gt;This email is from: <span class="hljs-subst">${body.emailAddress}</span>&lt;/div&gt;
      &lt;div&gt;message: <span class="hljs-subst">${body.emailMessage}</span>&lt;/div&gt;`</span>,
    };

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> SendGrid.send(email);

      context.res!.status = <span class="hljs-number">200</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"Email successful! Check email for the message."</span>,
      };
    } <span class="hljs-keyword">catch</span> (error) {
      context.res!.status = <span class="hljs-number">400</span>;
      context.res!.body = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred."</span>,
      };
    }
  } <span class="hljs-keyword">else</span> {
    context.res!.status = <span class="hljs-number">400</span>;
    context.res!.body = {
      <span class="hljs-attr">message</span>: <span class="hljs-string">"Form submission is invalid. Please try again."</span>,
    };
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> httpTrigger;
</code></pre><p>Prior to the function code the SendGrid Mail Service package is imported. Immediately following the setApiKey method is called and the environment variable, stored as an application setting, is passed in. The SendGrid package is now initialized and ready to be used in the code that sends the email. The api key is typecast as a string here, because in this example <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node#typescript">TypeScript</a> has been selected as the language. If you are not using TypeScript this typecast should be removed as it is not needed.</p><p>The serverless function code first checks to make sure the form was submit with the field data entered. If the form submission was valid and an email options object is created with the form data. ES6 template literals are used here to, instead of standard string concatenation, build the email message that is saved as the email object html key. It is called html because SendGrid permits the sending of html emails. The result of using ES6 template literals are a concise and readable block of code can be easily adjusted in the future if needed.</p><p>The email object is then passed to the SendGrid send method provided by the SendGrid Mail Service npm package. Notice, that since this is an async method it must be awaited before allowing the code execution to proceed. The send method call is also wrapped in a try catch block. This way if the email service fails the serverless function will return a server error notifying the client.</p><p>Using Sendgrid makes it even easier to manage and prevents potential issues with spam filters. This approach can be useful if we are building a site with the <a href="https://jamstack.org/">Jamstack</a>, since a server is not required. Additionally, if the email usage limits are within the free plan of SendGrid, the cost savings can be quite substantial. It is also worth noting that when using Azure Serverless Functions, we can use the same Azure account to create and link to a Sendgrid account, which includes tens of thousands of free emails per month. To find it search for SendGrid in the Azure Portal dashboard, and follow the setup instructions from there.</p>]]></description></item><item><title>Generate an RSS Feed with Node.js</title><pubDate>Wed, 13 Jan 2021 00:00:00 GMT</pubDate><link>https://www.devextent.com/xml-rss-feed-nodejs/</link><guid isPermaLink="true">https://www.devextent.com/xml-rss-feed-nodejs/</guid><description><![CDATA[<p>An <a href="https://en.wikipedia.org/wiki/RSS">RSS</a> feed is a convenient way to allow access to syndicated content in a standardized format that is easily shareable and discoverable. Recently I've been using <a href="https://feedly.com/">feedly</a> to stay up to date with a variety of web development blogs. This got me interested in how to add an rss feed to a static website built with the <a href="https://jamstack.org/">Jamstack</a>, specifically how to generate an rss feed from blog post data with node.js and <a href="https://www.typescriptlang.org/">TypeScript</a>.</p><p>Before proceeding make sure to have <a href="https://nodejs.org/en/">node.js</a> and <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm">npm</a> installed.</p><h3>Run npm init</h3><p>There are some npm packages that will be used to create the rss feed, so first run the command <kbd>npm init</kbd>, which will create a package.json file that we can add dependencies to. After creating the package.json these are the npm packages that we will add:</p><ul><li><a href="https://www.npmjs.com/package/fs-extra">fs-extra</a></li><li><a href="https://www.npmjs.com/package/xml">xml</a></li><li><a href="https://www.npmjs.com/package/cheerio">cheerio</a></li><li><a href="https://www.npmjs.com/package/typescript">typescript</a></li></ul><p>To install these run the command <kbd>npm install fs-extra cheerio xml typescript --save</kbd>, and since we are using TypeScript for this example we need the corresponding type definitions. To install the type definitions run the command: <kbd>npm install @types/xml @types/cheerio @types/fs-extra --save-dev</kbd>.</p><p>There is one extra field that needs to be added to the package.json file and that is the <a href="https://nodejs.org/api/packages.html#packages_type">type</a> field. This permits the use of <a href="https://nodejs.org/api/esm.html">ECMAScript modules</a>, rather than <a href="https://nodejs.org/api/modules.html">CommonJS modules</a>.</p><p>Your package.json should look similar to this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"xmlrssfeed"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"cheerio"</span>: <span class="hljs-string">"^1.0.0-rc.5"</span>,
    <span class="hljs-attr">"fs-extra"</span>: <span class="hljs-string">"^9.0.1"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.1.3"</span>,
    <span class="hljs-attr">"xml"</span>: <span class="hljs-string">"^1.0.1"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/cheerio"</span>: <span class="hljs-string">"^0.22.23"</span>,
    <span class="hljs-attr">"@types/fs-extra"</span>: <span class="hljs-string">"^9.0.6"</span>,
    <span class="hljs-attr">"@types/xml"</span>: <span class="hljs-string">"^1.0.5"</span>
  }
}
</code></pre><h3>Configure tsconfig.json</h3><p>Typescript is used in this example so tsconfig.json file is also required. You can read more about the tsconfig.json settings in the <a href="https://www.typescriptlang.org/docs/handbook/tsconfig-json.html">TypeScript documentation</a>. For our case, create a file named tsconfig.json and copy the code below into it.</p><pre><code class="hljs">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"allowSyntheticDefaultImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"isolatedModules"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2019"</span>],
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"*.ts"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules/**/*"</span>]
}
</code></pre><p>The module field is set to "esnext" to match the addition of the "type" field in the package.json. This setting instructs the TypeScript compiler to generate <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">es modules</a>, and allows us to use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import">import</a> in the TypeScript code.</p><h3>npm package.json script</h3><p>After configuring TypeScript, we need a way to transpile and then execute the generated JavaScript with node.js. To do this, an npm package.json script can be added to carry out both steps. In the package.json file, add a new scripts property "createRssFeed", so that it looks like this:</p><pre><code class="hljs">{
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"xmlrssfeed"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"createRssFeed"</span>: <span class="hljs-string">"tsc &amp;&amp; node index.js"</span>,
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"cheerio"</span>: <span class="hljs-string">"^1.0.0-rc.5"</span>,
    <span class="hljs-attr">"fs-extra"</span>: <span class="hljs-string">"^9.0.1"</span>,
    <span class="hljs-attr">"typescript"</span>: <span class="hljs-string">"^4.1.3"</span>,
    <span class="hljs-attr">"xml"</span>: <span class="hljs-string">"^1.0.1"</span>
  },
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/cheerio"</span>: <span class="hljs-string">"^0.22.23"</span>,
    <span class="hljs-attr">"@types/fs-extra"</span>: <span class="hljs-string">"^9.0.6"</span>,
    <span class="hljs-attr">"@types/xml"</span>: <span class="hljs-string">"^1.0.5"</span>
  }
}
</code></pre><p>The createRssFeed script will sequentially compile the TypeScript source file (index.ts) and then use node to execute the JavaScript output. If you try running the command <kbd>npm run createRssFeed</kbd> you will get an error, because the index.ts doesn't exist yet. Let's create that now.</p><h3>Node.js Script</h3><p>In the same folder as the package.json file create a new file named index.ts, and add the code below to make sure the setup is working.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;
<span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;
<span class="hljs-keyword">import</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">"cheerio"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createRssFeed</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"creating feed"</span>);
})();
</code></pre><p>Then run the createRssFeed command <kbd>npm run createRssFeed</kbd> and the output should print to the console the text "creating feed".</p><h3>Generate RSS Feed</h3><p>With the setup working we can now begin to use the npm packages that we imported. The xml package accepts a feed object as it's configuration so we can add that to the createRssFeed function. The feedObject will be processed into an xml string and then the fs-extra package will be used to write the output to a file named feed.rss.</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;
<span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;
<span class="hljs-keyword">import</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">"cheerio"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createRssFeed</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"creating feed"</span>);
  <span class="hljs-keyword">const</span> feedObject = {
    <span class="hljs-attr">rss</span>: [
      {
        <span class="hljs-attr">_attr</span>: {
          <span class="hljs-attr">version</span>: <span class="hljs-string">"2.0"</span>,
          <span class="hljs-string">"xmlns:atom"</span>: <span class="hljs-string">"http://www.w3.org/2005/Atom"</span>,
        },
      },
      {
        <span class="hljs-attr">channel</span>: [
          {
            <span class="hljs-string">"atom:link"</span>: {
              <span class="hljs-attr">_attr</span>: {
                <span class="hljs-attr">href</span>: <span class="hljs-string">"YOUR-WEBSITE/feed.rss"</span>,
                <span class="hljs-attr">rel</span>: <span class="hljs-string">"self"</span>,
                <span class="hljs-attr">type</span>: <span class="hljs-string">"application/rss+xml"</span>,
              },
            },
          },
          {
            <span class="hljs-attr">title</span>: <span class="hljs-string">"YOUR-WEBSITE-TITLE"</span>,
          },
          {
            <span class="hljs-attr">link</span>: <span class="hljs-string">"YOUR-WEBSITE/"</span>,
          },
          { <span class="hljs-attr">description</span>: <span class="hljs-string">"YOUR-WEBSITE-DESCRIPTION"</span> },
          { <span class="hljs-attr">language</span>: <span class="hljs-string">"en-US"</span> },
          <span class="hljs-comment">// todo: add the feed items here</span>
        ],
      },
    ],
  };

  <span class="hljs-keyword">const</span> feed = <span class="hljs-string">'&lt;?xml version="1.0" encoding="UTF-8"?&gt;'</span> + xml(feedObject);

  <span class="hljs-keyword">await</span> fs.writeFile(<span class="hljs-string">"/feed.rss"</span>, feed, <span class="hljs-string">"utf8"</span>);
})();
</code></pre><p>Make sure to replace "YOUR-WEBSITE", "YOUR-WEBSITE-TITLE", and "YOUR-WEBSITE-DESCRIPTION" with the actual values from the website you are generating the RSS feed for.</p><p>At this point the createRssFeed npm package.json script should generate a new file named feed.rss in the project folder, although it will be an empty feed. So in the feed object we can replace the todo comment with code that will use some sample post data to generate the feed.</p><p>In this case we'll create an array of objects for our sample post data, but a more likely scenario is that they would be dynamically sourced from a content store, like markdown files or a content management system.</p><p>Add the sample posts below directly above the feedObject variable.</p><pre><code class="hljs"><span class="hljs-keyword">const</span> posts = [
  {
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Post One"</span>,
    <span class="hljs-attr">date</span>: <span class="hljs-string">"1/1/2020"</span>,
    <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-one"</span>,
    <span class="hljs-attr">content</span>: <span class="hljs-string">"This is some content for post one."</span>,
  },
  {
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Post Two"</span>,
    <span class="hljs-attr">date</span>: <span class="hljs-string">"1/2/2020"</span>,
    <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-two"</span>,
    <span class="hljs-attr">content</span>: <span class="hljs-string">"This is some content for post two."</span>,
  },
  {
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Post Three"</span>,
    <span class="hljs-attr">date</span>: <span class="hljs-string">"1/3/2020"</span>,
    <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-three"</span>,
    <span class="hljs-attr">content</span>: <span class="hljs-string">"This is some content for post three."</span>,
  },
  {
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Post Four"</span>,
    <span class="hljs-attr">date</span>: <span class="hljs-string">"1/4/2020"</span>,
    <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-four"</span>,
    <span class="hljs-attr">content</span>: <span class="hljs-string">"This is some content for post four."</span>,
  },
];
</code></pre><p>Now that we some posts to include, replace the todo with this function call:</p><pre><code class="hljs">...(buildFeed(posts));
</code></pre><p>This will take the result of the buildFeed function (we will write this next), which will be an array and <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">spread</a> the results into the feedObject.</p><p>Now the index.ts file should look like this:</p><pre><code class="hljs"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs-extra"</span>;
<span class="hljs-keyword">import</span> xml <span class="hljs-keyword">from</span> <span class="hljs-string">"xml"</span>;
<span class="hljs-keyword">import</span> cheerio <span class="hljs-keyword">from</span> <span class="hljs-string">"cheerio"</span>;

(<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createRssFeed</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"creating feed"</span>);
  <span class="hljs-keyword">const</span> posts = [
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Post One"</span>,
      <span class="hljs-attr">date</span>: <span class="hljs-string">"1/1/2020"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-one"</span>,
      <span class="hljs-attr">content</span>: <span class="hljs-string">"&lt;p&gt;This is some content for post one.&lt;/p&gt;"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Post Two"</span>,
      <span class="hljs-attr">date</span>: <span class="hljs-string">"1/2/2020"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-two"</span>,
      <span class="hljs-attr">content</span>: <span class="hljs-string">"&lt;p&gt;This is some content for post two.&lt;/p&gt;"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Post Three"</span>,
      <span class="hljs-attr">date</span>: <span class="hljs-string">"1/3/2020"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-three"</span>,
      <span class="hljs-attr">content</span>:
        <span class="hljs-string">"&lt;p&gt;This is some content for post three. This is a relative &lt;a href='/relative-link/'&gt;link&lt;/a&gt;&lt;/p&gt;"</span>,
    },
    {
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Post Four"</span>,
      <span class="hljs-attr">date</span>: <span class="hljs-string">"1/4/2020"</span>,
      <span class="hljs-attr">slug</span>: <span class="hljs-string">"post-four"</span>,
      <span class="hljs-attr">content</span>: <span class="hljs-string">"&lt;p&gt;This is some content for post four.&lt;/p&gt;"</span>,
    },
  ];

  <span class="hljs-keyword">const</span> feedObject = {
    <span class="hljs-attr">rss</span>: [
      {
        <span class="hljs-attr">_attr</span>: {
          <span class="hljs-attr">version</span>: <span class="hljs-string">"2.0"</span>,
          <span class="hljs-string">"xmlns:atom"</span>: <span class="hljs-string">"http://www.w3.org/2005/Atom"</span>,
        },
      },
      {
        <span class="hljs-attr">channel</span>: [
          {
            <span class="hljs-string">"atom:link"</span>: {
              <span class="hljs-attr">_attr</span>: {
                <span class="hljs-attr">href</span>: <span class="hljs-string">"YOUR-WEBSITE/feed.rss"</span>,
                <span class="hljs-attr">rel</span>: <span class="hljs-string">"self"</span>,
                <span class="hljs-attr">type</span>: <span class="hljs-string">"application/rss+xml"</span>,
              },
            },
          },
          {
            <span class="hljs-attr">title</span>: <span class="hljs-string">"YOUR-WEBSITE-TITLE"</span>,
          },
          {
            <span class="hljs-attr">link</span>: <span class="hljs-string">"YOUR-WEBSITE/"</span>,
          },
          { <span class="hljs-attr">description</span>: <span class="hljs-string">"YOUR-WEBSITE-DESCRIPTION"</span> },
          { <span class="hljs-attr">language</span>: <span class="hljs-string">"en-US"</span> },
          ...buildFeed(posts),
        ],
      },
    ],
  };

  <span class="hljs-keyword">const</span> feed = <span class="hljs-string">'&lt;?xml version="1.0" encoding="UTF-8"?&gt;'</span> + xml(feedObject);

  <span class="hljs-keyword">await</span> fs.writeFile(<span class="hljs-string">"./feed.rss"</span>, feed);
})();
</code></pre><p>The feedObject now includes the buildFeed function, which can be added below the createRssFeed function. As the name suggests this is where the feed items will be created and sorted by most recent date. Additionally the cheerio npm package will be used here.</p><pre><code class="hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">buildFeed</span>(<span class="hljs-params">
  posts: { title: <span class="hljs-built_in">string</span>; date: <span class="hljs-built_in">string</span>; slug: <span class="hljs-built_in">string</span>; content: <span class="hljs-built_in">string</span> }[]
</span>) </span>{
  <span class="hljs-keyword">const</span> sortedPosts = posts.sort(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">first, second</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(second.date).getTime() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(first.date).getTime();
  });

  <span class="hljs-keyword">const</span> feedItems = [];

  feedItems.push(
    ...sortedPosts.map(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">post</span>) </span>{
      <span class="hljs-keyword">const</span> feedItem = {
        <span class="hljs-attr">item</span>: [
          { <span class="hljs-attr">title</span>: post.title },
          {
            <span class="hljs-attr">pubDate</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(post.date <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>).toUTCString(),
          },
          {
            <span class="hljs-attr">guid</span>: [
              { <span class="hljs-attr">_attr</span>: { <span class="hljs-attr">isPermaLink</span>: <span class="hljs-literal">true</span> } },
              <span class="hljs-string">`YOUR-WEBSITE/<span class="hljs-subst">${post.slug}</span>/`</span>,
            ],
          },
          {
            <span class="hljs-attr">description</span>: {
              <span class="hljs-attr">_cdata</span>: post.content,
            },
          },
        ],
      };
      <span class="hljs-keyword">return</span> feedItem;
    })
  );

  <span class="hljs-keyword">return</span> feedItems;
}
</code></pre><p>This code can now generate the RSS feed by re-running the command <kbd>npm run createRssFeed</kbd>, however any relative links in the post content will not link to the correct website, since RSS feeds require absolute links. We can convert them to absolute links using the cheerio npm package.</p><h3>Convert relative links to absolute links</h3><p>Directly above the feed object add the following code:</p><pre><code class="hljs"><span class="hljs-keyword">const</span> $ = cheerio.load(post.content <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>, {
  <span class="hljs-attr">decodeEntities</span>: <span class="hljs-literal">false</span>,
});

<span class="hljs-comment">// replace relative links with absolute</span>
$(<span class="hljs-string">"a[href^='/'], img[src^='/']"</span>).each(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"><span class="hljs-built_in">this</span>: cheerio.Element</span>) </span>{
  <span class="hljs-keyword">const</span> $this = $(<span class="hljs-built_in">this</span>);
  <span class="hljs-keyword">if</span> ($this.attr(<span class="hljs-string">"href"</span>)) {
    $this.attr(<span class="hljs-string">"href"</span>, <span class="hljs-string">`YOUR-WEBSITE/<span class="hljs-subst">${$<span class="hljs-built_in">this</span>.attr(<span class="hljs-string">"href"</span>)}</span>`</span>);
  }
  <span class="hljs-keyword">if</span> ($this.attr(<span class="hljs-string">"src"</span>)) {
    $this.attr(<span class="hljs-string">"src"</span>, <span class="hljs-string">`YOUR-WEBSITE/<span class="hljs-subst">${$<span class="hljs-built_in">this</span>.attr(<span class="hljs-string">"src"</span>)}</span>`</span>);
  }
});

<span class="hljs-keyword">const</span> postContent = $(<span class="hljs-string">"body"</span>).html() <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;
</code></pre><p>Here is some more info on this technique to <a href="https://www.devextent.com/relative-url-to-absolute-url-nodejs/">convert relative urls to absolute urls</a>. Make sure to also replace the description property of the feedItem with the postContent variable. The buildFeed function should now look like this:</p><pre><code class="hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">buildFeed</span>(<span class="hljs-params">
  posts: { title: <span class="hljs-built_in">string</span>; date: <span class="hljs-built_in">string</span>; slug: <span class="hljs-built_in">string</span>; content: <span class="hljs-built_in">string</span> }[]
</span>) </span>{
  <span class="hljs-keyword">const</span> sortedPosts = posts.sort(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">first, second</span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(second.date).getTime() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(first.date).getTime();
  });

  <span class="hljs-keyword">const</span> feedItems = [];

  feedItems.push(
    ...sortedPosts.map(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">post</span>) </span>{
      <span class="hljs-keyword">const</span> $ = cheerio.load(post.content <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>, {
        <span class="hljs-attr">decodeEntities</span>: <span class="hljs-literal">false</span>,
      });

      <span class="hljs-comment">// replace relative links with absolute</span>
      $(<span class="hljs-string">"a[href^='/'], img[src^='/']"</span>).each(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"><span class="hljs-built_in">this</span>: cheerio.Element</span>) </span>{
        <span class="hljs-keyword">const</span> $this = $(<span class="hljs-built_in">this</span>);
        <span class="hljs-keyword">if</span> ($this.attr(<span class="hljs-string">"href"</span>)) {
          $this.attr(<span class="hljs-string">"href"</span>, <span class="hljs-string">`YOUR-WEBSITE/<span class="hljs-subst">${$<span class="hljs-built_in">this</span>.attr(<span class="hljs-string">"href"</span>)}</span>`</span>);
        }
        <span class="hljs-keyword">if</span> ($this.attr(<span class="hljs-string">"src"</span>)) {
          $this.attr(<span class="hljs-string">"src"</span>, <span class="hljs-string">`YOUR-WEBSITE/<span class="hljs-subst">${$<span class="hljs-built_in">this</span>.attr(<span class="hljs-string">"src"</span>)}</span>`</span>);
        }
      });

      <span class="hljs-keyword">const</span> postContent = $(<span class="hljs-string">"body"</span>).html() <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;

      <span class="hljs-keyword">const</span> feedItem = {
        <span class="hljs-attr">item</span>: [
          { <span class="hljs-attr">title</span>: post.title },
          {
            <span class="hljs-attr">pubDate</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(post.date <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>).toUTCString(),
          },
          {
            <span class="hljs-attr">guid</span>: [
              { <span class="hljs-attr">_attr</span>: { <span class="hljs-attr">isPermaLink</span>: <span class="hljs-literal">true</span> } },
              <span class="hljs-string">`YOUR-WEBSITE/<span class="hljs-subst">${post.slug}</span>/`</span>,
            ],
          },
          {
            <span class="hljs-attr">description</span>: {
              <span class="hljs-attr">_cdata</span>: postContent,
            },
          },
        ],
      };

      <span class="hljs-keyword">return</span> feedItem;
    })
  );

  <span class="hljs-keyword">return</span> feedItems;
}
</code></pre><p>The buildFeed function, first sorts all the posts by most recent date and then maps over the sorted posts to assign post data properties to the corresponding xml fields in the RSS feed. For each of the posts the content is modified, by using the cheerio npm package, to convert all the relative links to absolute links. That way when the RSS feed is shared the in-article links will link back to the correct website. As in the sections above make sure to replace "YOUR-WEBSITE" with the actual domain of your website. Additionally the date is formatted to <a href="https://www.w3.org/Protocols/rfc822/">RFC 822 format</a>, in order to match the <a href="https://validator.w3.org/feed/docs/rss2.html">RSS specification</a>.</p><p>Re-run the command <kbd>npm run createRssFeed</kbd>, and the feed.rss file that is generated should reflect the changes we made. You can verify that this file is a valid rss feed by checking it with the <a href="https://validator.w3.org/feed/">w3c Feed Validation Service</a>.</p><p>To permit auto discovery of the RSS feed make sure to include the following html in the head tag of your website.</p><pre><code class="hljs"><span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"alternate"</span>
  <span class="hljs-attr">type</span>=<span class="hljs-string">"application/rss+xml"</span>
  <span class="hljs-attr">title</span>=<span class="hljs-string">"RSS 2.0"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"/feed.rss"</span>
/&gt;</span>
</code></pre>]]></description></item></channel></rss>