All files / src/lib/nunjucks-extensions set-external.ts

89.74% Statements 35/39
77.77% Branches 7/9
100% Functions 9/9
89.18% Lines 33/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93                        9x   9x             88x   88x         3x         3x       3x 3x   3x 5x 2x     3x   3x 3x 3x 3x 3x 3x 6x 3x     3x 3x     3x 3x   3x           2x     3x 3x   3x               3x 3x 3x     3x      
import fs from 'fs-extra';
import path from 'path';
import csvParse from 'csv-parse/lib/sync.js';
import { Environment, Extension } from 'nunjucks';
// Type definitions are undefined / documented and in flux for these. See the source.
// @ts-ignore
import parserPkg from 'nunjucks/src/parser.js';
// @ts-ignore
import { lex } from 'nunjucks/src/lexer.js';
 
import * as logger from '../../utils/logger.js';
 
const { Parser } = parserPkg;
 
const acceptedFileTypes = ['json', 'csv'];
 
/**
 * Nunjucks extension for sourcing in variables from external sources.
 * Supports only .json and .csv sources for now.
 */
export class SetExternalExtension implements Extension {
  tags = ['ext'];
 
  constructor(private rootPath: string, private env: Environment) {}
 
  emitLoad(fullPath: string) {
    // Emit the nunjucks load event for the listener in {@link VariableRenderer}. No type defs.
    // @ts-ignore
    this.env.emit('load', '', { path: fullPath });
  }
 
  parse(parser: any, nodes: any, lexer: any) {
    // get the tag token
    const setExtTagToken = parser.nextToken();
 
    // parse the args and move after the block end. passing true
    // as the second arg is required if there are no parentheses
    const args = parser.parseSignature(null, true);
    parser.advanceAfterBlockEnd(setExtTagToken.value);
 
    const options = args.children
      .filter((child: any) => !(child instanceof nodes.KeywordArgs))
      .map((child: any) => child.value as string);
 
    // last child contains the kvp containing the path to the external variable source
    const lastChild = args.children[args.children.length - 1];
 
    const buffer: string[] = [];
    if (lastChild instanceof nodes.KeywordArgs) {
      lastChild.children.forEach((pair: any) => {
        const variableName = pair.key.value;
        const resourcePath = pair.value.value;
        acceptedFileTypes.forEach((fileType) => {
          if (!resourcePath.endsWith(fileType)) {
            return;
          }
 
          const fullResourcePath = path.resolve(this.rootPath, resourcePath);
          Iif (fileType === 'json') {
            const resourceRaw = fs.readFileSync(fullResourcePath);
            buffer.push(`{% set ${variableName} = ${resourceRaw} %}`);
          } else if (fileType === 'csv') {
            const hasNoHeader = options.includes('noHeader');
 
            const csvResourceRaw = csvParse(
              fs.readFileSync(fullResourcePath), {
                bom: true, // strip the byte order mark (BOM) from the input string or buffer.
                columns: (
                  hasNoHeader
                    ? false // if noHeader is present, first row is not header row
                    : (header: string[]) => header.map((col: string) => col)
                ),
              });
            const resourceRaw = JSON.stringify(csvResourceRaw);
            buffer.push(`{% set ${variableName} = ${resourceRaw} %}`);
          }
          this.emitLoad(fullResourcePath);
        });
      });
    } else E{
      logger.error(`Invalid {% ext %} tag at line ${setExtTagToken.lineno}.`);
      return new nodes.NodeList(setExtTagToken.lineno, setExtTagToken.colno, []);
    }
 
    const newParser = new Parser(lex(buffer.join('\n'), lexer.opts));
    if (parser.extensions !== undefined) {
      newParser.extensions = parser.extensions;
    }
 
    return newParser.parse();
  }
}