Search code examples
polymerproduction

How do you go into production with polymer project?


Is it common sense to use polymer build and then deploy the Application on your Web Server used for production? Or does it make sense to actutally use polymer serve / polyserve as the Web Server?


Solution

  • The problem with polymer serve is that if it falls over it doesn't restart, leaving you with no web site. Its real use is in development because it maps directories for you when you are developing a single element.

    Also, how will you be handling ajax calls?

    IN the past I have previously run my code (a bespoke node web server) in PM2. These days I run using docker, and in particular docker-compose which also restarts the application if it fails.

    EDIT The following is how I transpile on the fly code is copied (and then altered by me) from Google Polymer Teams "Polymer Server" and is therefore subject to the licence conditions given in that project.

     * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
     * This code may only be used under the BSD style license found at
     * http://polymer.github.io/LICENSE.txt
     * The complete set of authors may be found at
     * http://polymer.github.io/AUTHORS.txt
     * The complete set of contributors may be found at
     * http://polymer.github.io/CONTRIBUTORS.txt
     * Code distributed by Google as part of the polymer project is also
     * subject to an additional IP rights grant found at
     * http://polymer.github.io/PATENTS.txt
    

    The code consists of some supporting functions like these

      const parse5 = require('parse5');
      const dom5 = require('dom5');
      const LRU = require('lru-cache');
      const babelCore = require('babel-core');
      const transformLog = require('debug')('web:transform');
    
    
      const babelTransformers = [
        'babel-plugin-transform-es2015-arrow-functions',
        'babel-plugin-transform-es2015-block-scoped-functions',
        'babel-plugin-transform-es2015-block-scoping',
        'babel-plugin-transform-es2015-classes',
        'babel-plugin-transform-es2015-computed-properties',
        'babel-plugin-transform-es2015-destructuring',
        'babel-plugin-transform-es2015-duplicate-keys',
        'babel-plugin-transform-es2015-for-of',
        'babel-plugin-transform-es2015-function-name',
        'babel-plugin-transform-es2015-literals',
        'babel-plugin-transform-es2015-object-super',
        'babel-plugin-transform-es2015-parameters',
        'babel-plugin-transform-es2015-shorthand-properties',
        'babel-plugin-transform-es2015-spread',
        'babel-plugin-transform-es2015-sticky-regex',
        'babel-plugin-transform-es2015-template-literals',
        'babel-plugin-transform-es2015-typeof-symbol',
        'babel-plugin-transform-es2015-unicode-regex',
        'babel-plugin-transform-regenerator',
      ].map((name) => require(name));
    
      const isInlineJavaScript = dom5.predicates.AND(
        dom5.predicates.hasTagName('script'),
        dom5.predicates.NOT(dom5.predicates.hasAttr('src')));
    
      const babelCompileCache = LRU({
        length: (n, key) => n.length + key.length
      });
    
      function compileHtml(source, location) {
        const document = parse5.parse(source);
        const scriptTags = dom5.queryAll(document, isInlineJavaScript);
        for (const scriptTag of scriptTags) {
          try {
            const script = dom5.getTextContent(scriptTag);
            const compiledScriptResult = compileScript(script);
            dom5.setTextContent(scriptTag, compiledScriptResult);
          } catch (e) {
            // By not setting textContent we keep the original script, which
            // might work. We may want to fail the request so a better error
            // shows up in the network panel of dev tools. If this is the main
            // page we could also render a message in the browser.
            //eslint-disable-next-line no-console
            console.warn(`Error compiling script in ${location}: ${e.message}`);
          }
        }
        return parse5.serialize(document);
      }
      function compileScript(script) {
        return babelCore
          .transform(script, {
            plugins: babelTransformers,
          }).code;
      }
    
      function transform(request, body, isHtml) {
        const source = body;
        const cached = babelCompileCache.get(source);
        if (cached !== undefined) {
          transformLog('using the cache');
          return cached;
        }
        if (isHtml) {
          transformLog('compiling html');
          body = compileHtml(source, request.path);
        } else {
          transformLog('compiling js');
          body = compileScript(source);
        }
        babelCompileCache.set(source, body);
        return body;
      }
    

    The meat though is the middleware which effectively inserts itself in the outgoing stream captures all the chunks of outgoing html and js files and transforms them if necessary.

      function transformResponse(transformNeeded) {
        return (req, res, next) => {
          let ended = false;
    
          let _shouldTransform = null;
          let isHtml = true;
    
          // Note: this function memorizes its result.
          function shouldTransform() {
            if (_shouldTransform == null) {
              const successful = res.statusCode >= 200 && res.statusCode < 300;
              if (successful) {
                const result = transformNeeded(req);
                isHtml = result.isHtml;
                _shouldTransform = !!result.transform;
              } else {
                _shouldTransform = false;
              }
            }
            return _shouldTransform;
          }
    
          const chunks = [];
    
          const _write = res.write;
    
          res.write = function( chunk, enc, cb) {
            if (ended) {
              _write.call(this, chunk, enc, cb);
              return false;
            }
            if (shouldTransform()) {
              const buffer = (typeof chunk === 'string') ? new Buffer(chunk,enc) : chunk;
              chunks.push(buffer);
              return true;
            }
            return _write.call(this, chunk, enc, cb);
          }.bind(res);
    
          const _end = res.end;
          res.end = function (chunk, enc, cb) {
            if (ended)
              return false;
            ended = true;
            if (shouldTransform()) {
              if (chunk) {
                const buffer = (typeof chunk === 'string') ? new Buffer(chunk,enc) : chunk;
                chunks.push(buffer);
              }
              const body = Buffer.concat(chunks).toString('utf8');
              let newBody = body;
              try {
                newBody = transform(req, body, isHtml);
              } catch (e) {
                //eslint-disable-next-line no-console
                console.warn('Error', e);
              }
              // TODO(justinfagnani): re-enable setting of content-length when we know
              // why it was causing truncated files. Could be multi-byte characters.
              // Assumes single-byte code points!
              // res.setHeader('Content-Length', `${newBody.length}`);
              this.removeHeader('Content-Length');
              return _end.call(this, newBody);
            }
            return _end.call(this,chunk, enc, cb);
          }.bind(res);
          next();
        };
      }
    

    This routine called transformNeeded which is as follows (this is the bit that detects the brower)

    function transformNeeded(req) {
      const pathname = url.parse(req.url).pathname;
      const isHtml = pathname === '/' || pathname.slice(-5) === '.html';
      if (isHtml || pathname.slice(-3) === '.js') {
        //see if we need to compile as we have a .html or .js file
        const splitPathName = pathname.split('/');
        const isPolyfill = splitPathName.includes('webcomponentsjs') ||
          splitPathName.includes('promise-polyfill');
        if (!isPolyfill) {
          const browser = new UAParser(req.headers['user-agent']).getBrowser();
          const versionSplit = (browser.version || '').split('.');
          const [majorVersion, minorVersion] = versionSplit.map((v) => v ? parseInt(v, 10) : -1);
          const supportsES2015 = (browser.name === 'Chrome' && majorVersion >= 49) ||
            (browser.name === 'Chromium' && majorVersion >= 49) ||
            (browser.name === 'OPR' && majorVersion >= 36) ||
            (browser.name === 'Mobile Safari' && majorVersion >= 10) ||
            (browser.name === 'Safari' && majorVersion >= 10) ||
            // Note: The Edge user agent uses the EdgeHTML version, not the main
            // release version (e.g. EdgeHTML 15 corresponds to Edge 40). See
            // https://en.wikipedia.org/wiki/Microsoft_Edge#Release_history.
            //
            // Versions before 15.15063 may contain a JIT bug affecting ES6
            // constructors (see #161).
            (browser.name === 'Edge' &&
              (majorVersion > 15 || (majorVersion === 15 && minorVersion >= 15063))) ||
            (browser.name === 'Firefox' && majorVersion >= 51);
          requestLog(
            'Browser is %s version %d,%d - supports ES2015? ',
            browser.name,
            majorVersion,
            minorVersion,
            supportsES2015
          );
          return {transform: !supportsES2015, isHtml: isHtml};
        }
      }
      return {transform: false, isHtml: isHtml};
    }
    

    Finally, I have to set up the routes before I establish the web server and then tell the web server to use the routes I have set up.

    const Router = require('router');
    //sets up my API routes
    manager.setRoutes(router);
    router.use('/', transformResponse(this.transformNeeded));
    router.use('/', staticFiles(clientPath));
    this._start(router);