Skip to content

SharepointPlus example with Node environment

Aymeric edited this page Dec 26, 2019 · 1 revision

I'm using SharepointPlus in all my projects with Vue + Webpack (vue-cli). Based on issue #125 I'll provide a few snippets/explanations on how I deal with Sharepoint 2013 On-Promise and SharepointPlus/Vue/Webpack.

All the below steps are automatically set/configured when I create a new project, using a custom-made vue-cli presents.

DISCLAIMER: Please, note that I'm working alone (I don't have any dev co-workers), and my environment is a Sharepoint 2013 On-Promise where I'm a Site Collection admin (without Farm permissions). The way I deal with Webpack and Sharepoint might be unconventional!

Webpack configuration

I have configured several things in my Webpack, some because of the Sharepoint environment, and some others that are more specific for SharepointPlus.

  1. (optional) I self-signed a HTTPS certificate for my dev server because I had HTTPS errors.
  2. I change optimization.splitChunks.automaticNameDelimiter to use the delimiter ., because the default ~ will cause issues with Sharepoint document libraries :
{
  [...]
  optimization:{
    splitChunks:{
      automaticNameDelimiter:'.'
    }
  }
}
  1. I use sharepointplus-loader to optimize the bundle's size

component.vue

In my Vue component, I'll import sharepointplus:

import $SP from 'sharepointplus'

Then I can use it:

export default {
  data () {
    return {
      titles:[]
    }
  },
  created () {
    $SP().list("My List").get({
      fields:"Title",
      where:"ID = "+this.urlParams.ID,
      json:true
    })
    .then(data => {
      this.titles = data.map(d => d.Title);
    })
  }
}

main.js

In my main JS file, I use the below trick to make sure I have window.L_Menu_BaseUrl available, even in my dev environment:

// `window.L_Menu_BaseUrl` is one of the ways for SharepointPlus to find the URL of the current website
// sometimes this information could be missing, so I just use the below code
if (typeof window.L_Menu_BaseUrl==="undefined") {
  // `process.env.VUE_APP_SHAREPOINT` is an environment variable defined when I create my project
  window.L_Menu_BaseUrl=process.env.VUE_APP_SHAREPOINT;
}

Debug.aspx

I create a Debug.aspx page as a placeholder for a switch button that will add a "dev mode" cookie when it's turned on. Capture of the switch button

I'll use the "dev mode" cookie to get the resources from the local dev server instead of getting them from the Sharepoint server.

index.html

My index.html template looks like the below in my Vue project:

<!-- I display a "development mode" message at the top to know when the "dev mode" is active -->
<small id="devmode" style="display:none;position:fixed;top:9px;color:white;z-index:1;">development mode</small>
<!-- my app is loaded in div#app -->
<div id="app"></div>
<script>
// I use a function `loadExt` to load my JS/CSS resources
function loadExt(e,t){var s=this;s.files=e,s.js=[],s.head=document.getElementsByTagName("head")[0],s.after=t||function(){},s.loadStyle=function(e){var t=document.createElement("link");t.rel="stylesheet",t.type="text/css",t.href=e,s.head.appendChild(t)},s.loadScript=function(e){var t=document.createElement("script");t.type="text/javascript",t.src=s.js[e];var a=function(){++e<s.js.length?s.loadScript(e):s.after()};t.onload=function(){a()},s.head.appendChild(t)};for(var a=0;a<s.files.length;a++)/\.js$|\.js\?/.test(s.files[a])&&s.js.push(s.files[a]),/\.css$|\.css\?/.test(s.files[a])&&s.loadStyle(s.files[a]);s.js.length>0?s.loadScript(0):s.after()}

// I check if the "dev mode" cookie is active for the current user
// `<%= process.env.VUE_APP_PACKAGE_NAME %>` is automatically replaced by my package name
var devMode = (document.cookie.indexOf("<%= process.env.VUE_APP_PACKAGE_NAME %>DevMode=true") !== -1);
// if the devMode is active, then I load the resources from the local server
if (devMode) {
  document.querySelector('#devmode').style.display='block';
  loadExt([
    // `<%= process.env.VUE_APP_LOCALHOST %>` is automatically replaced by https://localhost:8080 or
    // whatever that has been defined as the dev server host and port in my configuration
    "<%= process.env.VUE_APP_LOCALHOST %>/js/chunk-common.js",
    "<%= process.env.VUE_APP_LOCALHOST %>/js/chunk-index-vendors.js",
    "<%= process.env.VUE_APP_LOCALHOST %>/js/index.js",
    ])
} else {
  // otherwise, we let htmlWebpackPlugin add the last JS/CSS builds that have been published
  loadExt([
  <% for (var css in htmlWebpackPlugin.files.css) { %>"<%= htmlWebpackPlugin.files.css[css] %>",<% } %>
  <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>"<%= htmlWebpackPlugin.files.chunks[chunk].entry %>",<% } %>
  ])
}
</script>

sync.js

I created a script that will synchronize my local dist/ folder with a folder in a Sharepoint Document Library. I automatically call it when I publish a build.

// config
const distLocalPath = "./dist/"; // we need the '/' at the end
const $SP = require("sharepointplus");
const fs = require("fs");
const path = require("path");
const prompt = require('prompt');
// because I'm using Node server, I'll need to provide credentials to `auth`
const credentials = require('../credentials');
const sp = $SP().auth(credentials);
// when I create my project, I store the root URL to my Sharepoint website in my package.json
// e.g. "https://intranet.com/sites/DemoSite"
const url = require('./package.json').sharepoint.url;
// as well as the path to the document library/folder where I want the files to be sync'
// e.g. "SitePages/Dev/"
const rootFolder = require('./package.json').sharepoint.path.split('/')[0];
// the internal name for "SitePages" is actually "Site Pages"
const listName = (rootFolder==='SitePages'?'Site Pages':rootFolder);
// ignore some folders in the distant Sharepoint Document Library
const ignoreInDist = [rootFolder+'/Old/',rootFolder+'/Forms/',rootFolder+'/_layouts/'];
const filesize = require("filesize");

// keep the pathname of the URL + path defined in package.json
const distPathName = url.split('/').slice(3).join('/');

const regexLocalPath = new RegExp("^" + (distLocalPath.startsWith('./') ? distLocalPath.slice(2) : distLocalPath));
const walkSync = (dir, filelist = []) => {
  fs.readdirSync(dir).forEach(file => {
    let filePath = path.join(dir, file);
    let fileStat = fs.statSync(filePath);
    let isDirectory = fileStat.isDirectory();
    filelist.push({
      path:filePath.replace(/\\/g,"/").replace(regexLocalPath,"") + (isDirectory ? '/' : ''),
      // our Sharepoint timezone is "America/Monterrey", so I make sure to have the correct time on my local dev env and on the remote server
      modified:new Date(fileStat.mtime.toLocaleString("en-US", {timeZone: "America/Monterrey"})),
      size:fileStat.size,
      folder:isDirectory
    });
    if (isDirectory) filelist = walkSync(filePath, filelist)
  });
  return filelist;
}
const hasHash = name => {
  return /\.[0-9a-f]{8}\.[a-z]+$/.test(name.replace(/\.map$/,""))
}
function PromiseChain(arr, fct) {
  var dfd = Promise.resolve();
  var res = arr.map(function(item,idx) {
    dfd = dfd.then(function() {
      return fct(item,idx)
    });
    return dfd
  });
  return Promise.all(res)
}

let localFiles = []; // {path, modified, size, folder(boolean)}
let remoteFiles = new Map(); // path => {path, modified, size, folder(boolean)}
let toUpload = [];
let toRemove = [];

// retrieve remote files
sp.list(listName, url).get({
  fields:"ID,BaseName,FileRef,FSObjType,File_x0020_Size,Modified",
  folderOptions:{
    show:"FilesAndFolders_Recursive"
  }
})
.then(data => {
  let regex = new RegExp("^" + distPathName.replace(/%20/g," "));
  data.forEach(d => {
    let isFolder = ($SP().cleanResult(d.getAttribute("FSObjType")) == "1");
    let path = $SP().cleanResult(d.getAttribute("FileRef")).replace(regex, "") + (isFolder ? '/' : '');
    // remove the first '/'
    path = path.replace(/^\//,"");
    // remember the remote files
    remoteFiles.set(path, {
      path:path,
      size:$SP().cleanResult(d.getAttribute("File_x0020_Size"))*1,
      modified:$SP().toDate(d.getAttribute("Modified")),
      folder:isFolder,
      fileref:d.getAttribute("FileRef")
    })
  })

  // retrieve local files
  localFiles = walkSync(distLocalPath);

  // compare local versus remote files
  localFiles.forEach(lfile => {
    let rfile = remoteFiles.get(lfile.path);
    // if the file exists locally but not remotelly
    if (!rfile && lfile.path!==rootFolder+'/') {
      console.log('[\x1b[32mNEW\x1b[0m] ' + lfile.path+ ' ('+filesize(lfile.size)+')');
      toUpload.push(lfile.path);
    } else {
      if (lfile.folder) remoteFiles.delete(lfile.path);
      else {
        // if it exists in both side
        // then we check the file size
        // however, we always want to update index.html
        if (lfile.path.endsWith('index.html')) {
          console.log('[\x1b[36mUPDATE\x1b[0m] '+ lfile.path+ ' ('+filesize(lfile.size)+')');
          toUpload.push(lfile.path);
          remoteFiles.delete(lfile.path);
        }
        else if (lfile.size !== rfile.size) {
          console.log('[\x1b[36mUPDATE\x1b[0m] '+ lfile.path+ ' ('+filesize(lfile.size)+')');
          toUpload.push(lfile.path);
          remoteFiles.delete(lfile.path);
        } else {
          // if the files have the same size, so check if we have a hash in the name
          if (!hasHash(lfile.path)) {
            // if not, then check the modified date
            if (rfile.modified < lfile.modified) {
              console.log('[\x1b[36mUPDATE\x1b[0m] '+ lfile.path+ ' ('+filesize(lfile.size)+')');
              toUpload.push(lfile.path);
              remoteFiles.delete(lfile.path)
            } else {
              remoteFiles.delete(lfile.path)
            }
          } else {
            remoteFiles.delete(lfile.path)
          }
        }
      }
    }
  })
  // find the remote files that are not present locally
  const minimumPath = require('./package.json').sharepoint.path;
  remoteFiles.forEach(rfile => {
    let ignore = false;
    for (let i=0; i<ignoreInDist.length; i++) {
      if (rfile.path.startsWith(ignoreInDist[i]) || !rfile.path.startsWith(minimumPath) || rfile.path === minimumPath) {
        ignore=true;
        break;
      }
    }
    if (!ignore) {
      toRemove.push(rfile.fileref);
      console.log('[\x1b[31mDELETE\x1b[0m] '+rfile.path)
    }
  });

  // we want the *.html files to be uploaded at the end
  // because index.html will have the new references to the new chunk files, so we want to first upload everything
  toUpload.sort((a,b) => {
    if (a.endsWith('.html')) return 1;
    if (b.endsWith('.html')) return -1;
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
  })
  console.log("[\x1b[35mSHAREPOINT\x1b[0m] "+url);
  if (toUpload.length > 0) {
    return new Promise((prom_res, prom_rej) => {
      process.stderr.write("\x07"); // beep
      prompt.get({
        properties:{
          'proceed':{
            message:'Do you want to proceed? (Y/n)'
          }
        }
      }, function (err, result) {
        if (result.proceed !== 'n') {
          prom_res()
        } else {
          prom_rej('Process stopped by the user.')
        }
      })
    })
  } else return '';
})
.then(() => {
  // upload new/update content
  return PromiseChain(toUpload, filePath => {
    if (filePath.endsWith('/')) {
      console.log("[\x1b[36mCREATE FOLDER\x1b[0m] "+filePath);
      return sp.list(listName, url).createFolder(filePath.split('/').slice(1).join('/'))
    } else {
      console.log("[\x1b[32mUPLOAD FILE\x1b[0m] "+filePath);
      return sp.list(listName, url).createFile({
        content:fs.readFileSync(path.join(distLocalPath,filePath)),
        filename:filePath.split('/').slice(1).join('/')
      })
    }
  })
})
.then(() => {
  if (toRemove.length > 0) {
    return PromiseChain(toRemove, file => {
      file = $SP().getLookup(file);
      console.log("[\x1b[31mDELETE FILE\x1b[0m] "+file.value);
      return sp.list(listName, url).remove({ID:file.id, FileRef:file.value})
    })
  }
})
.then(() => {
  console.log("Synchronization done.")
})
.catch(err => {
  console.log("ERROR => ",err);
})

App.aspx

Finally, on my Sharepoint website, I create a new empty page (e.g. App.aspx), then I add a Content Editor webpart. In the "Content Link" of the Content Editor webpart properties, I put a link to my index.html file (e.g. /sites/DemoSite/SitePages/Dev/index.html).

My app is loaded, either from my local server (if the "dev mode" cookie is active), or from Sharepoint site library.

Clone this wiki locally