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

91.11% Statements 41/45
77.77% Branches 7/9
100% Functions 9/9
90.69% Lines 39/43

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
    919x
    9x
    9x

    9x

    9x

    9x

    9x

    9x
    95x

    95x

    6x

    6x

    6x
    6x

    6x
    10x
    4x

    6x

    6x
    6x
    6x
    6x
    6x
    6x
    12x
    6x

    6x
    6x

    6x
    6x

    6x

    4x

    6x
    6x

    6x

    6x
    6x
    6x

    6x

     import fs from 'fs-extra';
    import path from 'path';
    import csvParse from 'csv-parse/lib/sync';
    import { Environment, Extension } from 'nunjucks';
    // Type definitions are undefined / documented and in flux for these. See the source.
    // @ts-ignore
    import { Parser } from 'nunjucks/src/parser';
    // @ts-ignore
    import { lex } from 'nunjucks/src/lexer';

    import * as logger from '../../utils/logger';

    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 => 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();
      }
    }