Skip to content

Instantly share code, notes, and snippets.

@bkoopman
Created December 15, 2014 14:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bkoopman/987ef712539dd8e669fe to your computer and use it in GitHub Desktop.
Save bkoopman/987ef712539dd8e669fe to your computer and use it in GitHub Desktop.
Publish HTML Design TBB
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using Sdl.Web.Tridion.Common;
using Tridion.ContentManager;
using Tridion.ContentManager.CommunicationManagement;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.Templating;
using Tridion.ContentManager.Templating.Assembly;
namespace Sdl.Web.Tridion.Templates
{
/// <summary>
/// Publish HTML design by unpacking the templates and less variables and running grunt to build it.
/// </summary>
[TcmTemplateTitle("Publish HTML Design")]
[TcmTemplateParameterSchema("resource:Sdl.Web.Tridion.Resources.PublishHtmlDesignParameters.xsd")]
public class PublishHtmlDesign : TemplateBase
{
// template builder log
private static readonly TemplatingLogger Log = TemplatingLogger.GetLogger(typeof(PublishHtmlDesign));
// name of system structure group
private const string SystemSgName = "_System";
// json content in page
private const string JsonOutputFormat = "{{\"status\":\"Success\",\"files\":[{0}]}}";
// set of files to merge across modules
private readonly Dictionary<string, List<string>> _mergeFileLines = new Dictionary<string, List<string>>();
// work folder for unzipping, merging and building design files
private string _tempFolder;
public override void Transform(Engine engine, Package package)
{
Initialize(engine, package);
_mergeFileLines.Add("src\\system\\assets\\less\\_custom.less", new List<string>());
_mergeFileLines.Add("src\\system\\assets\\less\\_modules.less", new List<string>());
_mergeFileLines.Add("src\\templates\\partials\\module-scripts-header.hbs", new List<string>());
_mergeFileLines.Add("src\\templates\\partials\\module-scripts-footer.hbs", new List<string>());
StringBuilder publishedFiles = new StringBuilder();
string drive = package.GetValue("drive") ?? String.Empty;
string cleanup = package.GetValue("cleanup") ?? String.Empty;
if (!String.IsNullOrEmpty(drive) && Char.IsLetter(drive.First()))
{
_tempFolder = drive.First() + @":\t" + DateTime.Now.ToString("yyyyMMddHHmmssfff");
}
else
{
// not using System.IO.Path.GetTempPath() because the paths in our zip are already quite long,
// so we need a very short temp path for the extract of our zipfile to succeed
// using drive from tridion cm homedir for temp folder
_tempFolder = ConfigurationSettings.GetTcmHomeDirectory().Substring(0, 3) + "t" + DateTime.Now.ToString("yyyyMMddHHmmssfff");
}
try
{
// read values from Component
var config = GetComponent();
var fields = new ItemFields(config.Content, config.Schema);
var favicon = fields.GetMultimediaLink("favicon");
var version = fields.GetTextValue("version");
PublishJson(String.Format("{{\"version\":{0}}}", JsonEncode(version)), config, GetPublication().RootStructureGroup, "version", "version");
// create temp folder
Directory.CreateDirectory(_tempFolder);
Log.Debug("Created " + _tempFolder);
// unzip and merge files
ProcessModules();
// build html design
string user = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
ProcessStartInfo info = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c npm start --color=false",
WorkingDirectory = _tempFolder,
CreateNoWindow = true,
ErrorDialog = false,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardErrorEncoding = Encoding.UTF8,
StandardOutputEncoding = Encoding.UTF8
};
using (Process cmd = new Process { StartInfo = info })
{
cmd.Start();
using (StreamReader reader = cmd.StandardOutput)
{
string output = reader.ReadToEnd();
if (!String.IsNullOrEmpty(output))
{
Log.Info(output);
// TODO: check for errors in standard output and throw exception
}
}
using (StreamReader reader = cmd.StandardError)
{
string error = reader.ReadToEnd();
if (!String.IsNullOrEmpty(error))
{
Exception ex = new Exception(error);
ex.Data.Add("Filename", info.FileName);
ex.Data.Add("Arguments", info.Arguments);
ex.Data.Add("User", user);
// TODO: check for known errors and throw exception with a user friendly message
//if (error.ToLower().Contains("something"))
//{
// throw new Exception(String.Format("Something went wrong for user {0}.", user), ex);
//}
throw ex;
}
}
cmd.WaitForExit();
}
// publish all binaries from dist folder
string dist = Path.Combine(_tempFolder, "dist");
if (Directory.Exists(dist))
{
// save favicon to disk (if available)
if (favicon != null)
{
string ico = Path.Combine(dist, "favicon.ico");
File.WriteAllBytes(ico, favicon.BinaryContent.GetByteArray());
Log.Debug("Saved " + ico);
}
string[] files = Directory.GetFiles(dist, "*.*", SearchOption.AllDirectories);
foreach (var file in files)
{
string filename = Path.GetFileName(file);
string extension = Path.GetExtension(file);
Log.Debug("Found " + file);
// determine correct structure group
Publication pub = (Publication)config.ContextRepository;
string relativeFolderPath = file.Substring(dist.Length, file.LastIndexOf('\\') - dist.Length);
Log.Debug("Relative path: " + relativeFolderPath);
relativeFolderPath = relativeFolderPath.Replace("system", SystemSgName).Replace('\\', '/');
string pubSgWebDavUrl = pub.RootStructureGroup.WebDavUrl;
string publishSgWebDavUrl = pubSgWebDavUrl + relativeFolderPath;
Log.Debug("Structure Group WebDAV URL: " + publishSgWebDavUrl);
StructureGroup sg = engine.GetObject(publishSgWebDavUrl) as StructureGroup;
if (sg == null)
{
throw new Exception("Missing Structure Group " + publishSgWebDavUrl);
}
// add binary to package and publish
using (FileStream fs = File.OpenRead(file))
{
Item binaryItem = Package.CreateStreamItem(GetContentType(extension), fs);
var binary = engine.PublishingContext.RenderedItem.AddBinary(binaryItem.GetAsStream(), filename, sg, "dist-" + filename, config, GetMimeType(extension));
binaryItem.Properties[Item.ItemPropertyPublishedPath] = binary.Url;
package.PushItem(filename, binaryItem);
if (publishedFiles.Length > 0)
{
publishedFiles.Append(",");
}
publishedFiles.AppendFormat("\"{0}\"", binary.Url);
Log.Info("Published " + binary.Url);
}
}
}
else
{
throw new Exception("Grunt build failed, dist folder is missing.");
}
}
finally
{
if (String.IsNullOrEmpty(cleanup) || !cleanup.ToLower().Equals("false"))
{
// cleanup workfolder
Directory.Delete(_tempFolder, true);
Log.Debug("Removed " + _tempFolder);
}
else
{
Log.Debug("Did not cleanup " + _tempFolder);
}
}
// output json result
package.PushItem(Package.OutputName, package.CreateStringItem(ContentType.Text, String.Format(JsonOutputFormat, publishedFiles)));
}
protected void ProcessModules()
{
var modules = GetActiveModules();
foreach (var module in modules)
{
var moduleName = module.Key;
if (moduleName != "core")
{
ProcessModule(moduleName, module.Value);
}
}
ProcessModule("core", modules["core"]);
// overwrite all merged files
foreach (var mergeFile in _mergeFileLines)
{
string file = Path.Combine(_tempFolder, mergeFile.Key);
File.WriteAllText(file, String.Join(Environment.NewLine, mergeFile.Value));
Log.Debug("Saved " + file);
}
}
protected void ProcessModule(string moduleName, Component moduleComponent)
{
var designConfig = GetDesignConfigComponent(moduleComponent);
if (designConfig != null)
{
var fields = new ItemFields(designConfig.Content, designConfig.Schema);
var zip = fields.GetComponentValue("design");
var variables = fields.GetComponentValue("variables");
var code = fields.GetTextValue("code");
var customLess = GetModuleCustomLess(variables, code);
if (zip != null)
{
// write binary contents as zipfile to disk
string zipfile = Path.Combine(_tempFolder, moduleName + "-html-design.zip");
File.WriteAllBytes(zipfile, zip.BinaryContent.GetByteArray());
// unzip
using (var archive = ZipFile.OpenRead(zipfile))
{
archive.ExtractToDirectory(_tempFolder, true);
}
// add content from merge files if available
List<string> files = _mergeFileLines.Keys.Select(s => s).ToList();
foreach (var mergeFile in files)
{
var path = Path.Combine(_tempFolder, mergeFile);
if (File.Exists(path))
{
foreach (var line in File.ReadAllLines(path))
{
if (!_mergeFileLines[mergeFile].Contains(line.Trim()))
{
_mergeFileLines[mergeFile].Add(line.Trim());
}
}
}
}
// add custom less code block
if (!String.IsNullOrEmpty(customLess.Trim()))
{
_mergeFileLines["src\\system\\assets\\less\\_custom.less"].Add(customLess);
}
}
if (moduleName.Equals("core"))
{
// unzip build files (nodejs, npm and grunt etc.)
var buildFiles = fields.GetComponentValue("build");
if (buildFiles != null)
{
ProcessBuildFiles(buildFiles);
}
}
}
}
protected void ProcessBuildFiles(Component zip)
{
// write binary contents as zipfile to disk
string zipfile = Path.Combine(_tempFolder, "build-files.zip");
File.WriteAllBytes(zipfile, zip.BinaryContent.GetByteArray());
// unzip
using (var archive = ZipFile.OpenRead(zipfile))
{
archive.ExtractToDirectory(_tempFolder, true);
}
}
private static Component GetDesignConfigComponent(Component moduleComponent)
{
var fields = new ItemFields(moduleComponent.Content, moduleComponent.Schema);
return fields.GetComponentValue("designConfiguration");
}
private static string GetModuleCustomLess(Component variables, string code)
{
const string line = "@{0}: {1};";
StringBuilder content = new StringBuilder();
// save less variables to disk (if available) in unpacked zip structure
if (variables != null)
{
// assuming all fields are text fields with a single value
var itemFields = new ItemFields(variables.Content, variables.Schema);
foreach (var itemField in itemFields)
{
string value = ((TextField)itemField).Value;
if (!String.IsNullOrEmpty(value))
{
content.AppendFormat(line, itemField.Name, ((TextField)itemField).Value);
}
}
}
if (code != null)
{
content.Append(code);
}
return content.ToString();
}
private static ContentType GetContentType(string extension)
{
// remove dot if extension starts with it
if (extension.StartsWith("."))
{
extension = extension.Substring(1);
}
switch (extension)
{
case "css":
case "js":
case "htc":
return ContentType.Text;
case "gif":
return ContentType.Gif;
case "jpg":
case "jpeg":
case "jpe":
return ContentType.Jpeg;
case "ico":
case "png":
return ContentType.Png;
default:
return ContentType.Unknown;
}
}
private static string GetMimeType(string extension)
{
// remove dot if extension starts with it
if (extension.StartsWith("."))
{
extension = extension.Substring(1);
}
switch (extension)
{
case "css":
return "text/css";
case "js":
return "application/x-javascript";
case "htc":
return "text/x-component";
case "gif":
return "image/gif";
case "jpg":
case "jpeg":
case "jpe":
return "image/jpeg";
case "ico":
return "image/x-icon";
case "png":
return "image/png";
case "svg":
return "image/svg+xml";
case "eot":
return "application/vnd.ms-fontobject";
case "woff":
return "application/x-woff";
case "otf":
return "application/x-font-opentype";
case "ttf":
return "application/x-font-ttf";
default:
return "application/octet-stream";
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment