diff --git a/.pnp.js b/.pnp.js index e2d6cef3c..6dc20f103 100755 --- a/.pnp.js +++ b/.pnp.js @@ -50,6 +50,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "name": "@lowdefy/layout", "reference": "workspace:packages/layout" }, + { + "name": "@lowdefy/nunjucks", + "reference": "workspace:packages/nunjucks" + }, { "name": "@lowdefy/renderer", "reference": "workspace:packages/renderer" @@ -77,6 +81,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@lowdefy/helpers", ["workspace:packages/helpers"]], ["@lowdefy/layout", ["workspace:packages/layout"]], ["@lowdefy/lowdefy", ["workspace:."]], + ["@lowdefy/nunjucks", ["workspace:packages/nunjucks"]], ["@lowdefy/poc-express", ["workspace:packages/express"]], ["@lowdefy/renderer", ["workspace:packages/renderer"]], ["@lowdefy/serializer", ["workspace:packages/serializer"]], @@ -3751,6 +3756,23 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "SOFT", }] ]], + ["@lowdefy/nunjucks", [ + ["workspace:packages/nunjucks", { + "packageLocation": "./packages/nunjucks/", + "packageDependencies": [ + ["@lowdefy/nunjucks", "workspace:packages/nunjucks"], + ["@babel/cli", "virtual:10c70c57b8c041994c932fbc6065790a1801b95ea6f8bb47c7334c5a9a57ca5035df7a515298306323a03d95e4c0d72d9488454f45837690b0620ca3a249c7f5#npm:7.11.6"], + ["@babel/core", "npm:7.11.6"], + ["@babel/preset-env", "virtual:10c70c57b8c041994c932fbc6065790a1801b95ea6f8bb47c7334c5a9a57ca5035df7a515298306323a03d95e4c0d72d9488454f45837690b0620ca3a249c7f5#npm:7.11.5"], + ["@lowdefy/type", "workspace:packages/type"], + ["babel-jest", "virtual:dc086c2784eb23a46dc07fa9ec3a3b376e693ea14441b30b9d9ab2ee39c177169123288242a40e296e2dd681a1cce35ee32919388c9b6f1a58c0a512bc095c93#npm:26.5.2"], + ["jest", "npm:26.5.2"], + ["nunjucks", "npm:3.2.2"], + ["nunjucks-date-filter", "npm:0.1.1"] + ], + "linkType": "SOFT", + }] + ]], ["@lowdefy/poc-express", [ ["workspace:packages/express", { "packageLocation": "./packages/express/", @@ -5257,6 +5279,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD", }] ]], + ["a-sync-waterfall", [ + ["npm:1.0.1", { + "packageLocation": "./.yarn/cache/a-sync-waterfall-npm-1.0.1-f6b6b49568-da11585ce7.zip/node_modules/a-sync-waterfall/", + "packageDependencies": [ + ["a-sync-waterfall", "npm:1.0.1"] + ], + "linkType": "HARD", + }] + ]], ["abab", [ ["npm:2.0.5", { "packageLocation": "./.yarn/cache/abab-npm-2.0.5-ae8d5b629e-a42b91bd9d.zip/node_modules/abab/", @@ -7597,6 +7628,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["commander", "npm:4.1.1"] ], "linkType": "HARD", + }], + ["npm:5.1.0", { + "packageLocation": "./.yarn/cache/commander-npm-5.1.0-7e939e7832-d16141ea7f.zip/node_modules/commander/", + "packageDependencies": [ + ["commander", "npm:5.1.0"] + ], + "linkType": "HARD", }] ]], ["comment-json", [ @@ -14773,6 +14811,30 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD", }] ]], + ["nunjucks", [ + ["npm:3.2.2", { + "packageLocation": "./.yarn/cache/nunjucks-npm-3.2.2-810aaf7ac6-ad7ac5ab62.zip/node_modules/nunjucks/", + "packageDependencies": [ + ["nunjucks", "npm:3.2.2"], + ["a-sync-waterfall", "npm:1.0.1"], + ["asap", "npm:2.0.6"], + ["chokidar", "npm:3.4.2"], + ["commander", "npm:5.1.0"] + ], + "linkType": "HARD", + }] + ]], + ["nunjucks-date-filter", [ + ["npm:0.1.1", { + "packageLocation": "./.yarn/cache/nunjucks-date-filter-npm-0.1.1-434e2afa95-45663784cf.zip/node_modules/nunjucks-date-filter/", + "packageDependencies": [ + ["nunjucks-date-filter", "npm:0.1.1"], + ["moment", "npm:2.29.1"], + ["nunjucks", "npm:3.2.2"] + ], + "linkType": "HARD", + }] + ]], ["nwsapi", [ ["npm:2.2.0", { "packageLocation": "./.yarn/cache/nwsapi-npm-2.2.0-8f05590043-fb0f05113a.zip/node_modules/nwsapi/", diff --git a/.yarn/cache/a-sync-waterfall-npm-1.0.1-f6b6b49568-da11585ce7.zip b/.yarn/cache/a-sync-waterfall-npm-1.0.1-f6b6b49568-da11585ce7.zip new file mode 100644 index 000000000..494d8a6b7 Binary files /dev/null and b/.yarn/cache/a-sync-waterfall-npm-1.0.1-f6b6b49568-da11585ce7.zip differ diff --git a/.yarn/cache/commander-npm-5.1.0-7e939e7832-d16141ea7f.zip b/.yarn/cache/commander-npm-5.1.0-7e939e7832-d16141ea7f.zip new file mode 100644 index 000000000..33d3efeb2 Binary files /dev/null and b/.yarn/cache/commander-npm-5.1.0-7e939e7832-d16141ea7f.zip differ diff --git a/.yarn/cache/nunjucks-date-filter-npm-0.1.1-434e2afa95-45663784cf.zip b/.yarn/cache/nunjucks-date-filter-npm-0.1.1-434e2afa95-45663784cf.zip new file mode 100644 index 000000000..c7c5e7277 Binary files /dev/null and b/.yarn/cache/nunjucks-date-filter-npm-0.1.1-434e2afa95-45663784cf.zip differ diff --git a/.yarn/cache/nunjucks-npm-3.2.2-810aaf7ac6-ad7ac5ab62.zip b/.yarn/cache/nunjucks-npm-3.2.2-810aaf7ac6-ad7ac5ab62.zip new file mode 100644 index 000000000..394cc15fa Binary files /dev/null and b/.yarn/cache/nunjucks-npm-3.2.2-810aaf7ac6-ad7ac5ab62.zip differ diff --git a/.yarnrc.yml b/.yarnrc.yml index 569eaa46f..8cfbdd737 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -5,6 +5,9 @@ packageExtensions: '@apollographql/apollo-tools@*': dependencies: graphql: '*' + 'nunjucks-date-filter@*': + dependencies: + 'nunjucks': '*' 'rc-animate@*': dependencies: 'react-dom': '*' diff --git a/packages/nunjucks/.babelrc b/packages/nunjucks/.babelrc new file mode 100644 index 000000000..cd680e14b --- /dev/null +++ b/packages/nunjucks/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "12", + "esmodules": true + } + } + ] + ] +} diff --git a/packages/nunjucks/jest.config.js b/packages/nunjucks/jest.config.js new file mode 100644 index 000000000..b3db30fb9 --- /dev/null +++ b/packages/nunjucks/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + + // A list of reporter names that Jest uses when writing coverage reports + coverageReporters: ['text'], + + // The test environment that will be used for testing + testEnvironment: 'node', + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ['/node_modules/', '/dist/'], +}; diff --git a/packages/nunjucks/package.json b/packages/nunjucks/package.json new file mode 100644 index 000000000..24dfbe51a --- /dev/null +++ b/packages/nunjucks/package.json @@ -0,0 +1,44 @@ +{ + "name": "@lowdefy/nunjucks", + "version": "1.0.0", + "licence": "Apache-2.0", + "description": "", + "homepage": "https://lowdefy.com", + "bugs": { + "url": "https://github.com/lowdefy/lowdefy/issues" + }, + "contributors": [ + { + "name": "Sam Tolmay", + "url": "https://github.com/SamTolmay" + }, + { + "name": "Gerrie van Wyk", + "url": "https://github.com/Gervwyk" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/lowdefy/lowdefy.git" + }, + "main": "dist/nunjucks.js", + "scripts": { + "build": "babel src --out-dir dist", + "test": "jest --coverage", + "prepare": "yarn build", + "prepublishOnly": "yarn build", + "npm-publish": "npm publish --access public" + }, + "dependencies": { + "@lowdefy/type": "1.0.1", + "nunjucks": "3.2.2", + "nunjucks-date-filter": "0.1.1" + }, + "devDependencies": { + "@babel/cli": "7.11.6", + "@babel/core": "7.11.6", + "@babel/preset-env": "7.11.5", + "babel-jest": "26.5.2", + "jest": "26.5.2" + } +} diff --git a/packages/nunjucks/src/nunjucks.js b/packages/nunjucks/src/nunjucks.js new file mode 100644 index 000000000..2ab406f58 --- /dev/null +++ b/packages/nunjucks/src/nunjucks.js @@ -0,0 +1,54 @@ +import nunjucks from 'nunjucks'; +import dateFilter from 'nunjucks-date-filter'; +import type from '@lowdefy/type'; + +// dateFilter.setDefaultFormat('YYYY-MM-DD'); +export const nunjucksEnv = new nunjucks.Environment(); + +nunjucksEnv.addFilter('date', dateFilter); + +const nunjucksTemplates = {}; +// slow +export const nunjucksString = (templateString, value) => { + if (type.isPrimitive(value)) { + return nunjucksEnv.renderString(templateString, { value }); + } + return nunjucksEnv.renderString(templateString, value); +}; + +export const validNunjucksString = (templateString, returnError = false) => { + try { + nunjucksString(templateString, {}); + return true; + } catch (e) { + if (returnError) { + return { name: e.name, message: e.message }; + } + return false; + } +}; + +// fast +// test with memoization +// this method compiles a nunjucks string only if the client has not compiled the same string before. +export const nunjucksFunction = (templateString) => { + // template was already compiled + if (type.isFunction(nunjucksTemplates[templateString])) { + return nunjucksTemplates[templateString]; + } + if (type.isString(templateString)) { + const template = nunjucks.compile(templateString, nunjucksEnv); + // execute once to throw catch template errors + template.render({}); + nunjucksTemplates[templateString] = (value) => { + if (type.isPrimitive(value)) { + return template.render({ value }); + } + return template.render(value); + }; + } else { + // for non string types like booleans or objects + nunjucksTemplates[templateString] = () => templateString; + } + return nunjucksTemplates[templateString]; +}; diff --git a/packages/nunjucks/test/nunjucks.test.js b/packages/nunjucks/test/nunjucks.test.js new file mode 100644 index 000000000..157aa86af --- /dev/null +++ b/packages/nunjucks/test/nunjucks.test.js @@ -0,0 +1,72 @@ +import { nunjucksString, nunjucksFunction, validNunjucksString } from '../src/nunjucks'; + +test('nunjucksString - string parsing', () => { + expect(nunjucksString('$ {{value}}', '100')).toEqual('$ 100'); + expect(nunjucksString('$ {{value}}', 46.6)).toEqual('$ 46.6'); + expect(nunjucksString('{{value}} is a boolean', true)).toEqual('true is a boolean'); +}); + +test('nunjucksFunction - string parsing, string value', () => { + const temp = nunjucksFunction('$ {{value}}'); + expect(temp('100')).toEqual('$ 100'); + expect(temp(100)).toEqual('$ 100'); + expect(temp(true)).toEqual('$ true'); +}); + +test('nunjucksFunction - string parsing, object value', () => { + const temp = nunjucksFunction('$ {{ cost }}'); + expect(temp({ cost: '100' })).toEqual('$ 100'); + expect(temp({ cost: 100 })).toEqual('$ 100'); + expect(temp({ cost: true })).toEqual('$ true'); +}); + +test('nunjucksString - errors', () => { + expect(() => { + nunjucksString('{% if a %}', {}); + }).toThrowErrorMatchingInlineSnapshot(` +"(unknown path) + parseIf: expected elif, else, or endif, got end of file" +`); +}); + +test('nunjucksFunction - errors', () => { + expect(() => { + nunjucksFunction('{% if a %}'); + }).toThrowErrorMatchingInlineSnapshot(` +"(unknown path) + parseIf: expected elif, else, or endif, got end of file" +`); +}); + +test('validNunjucksString - {% if %} single line', () => { + expect(validNunjucksString('{% if $state.name %} true {% endif %}')).toEqual(true); + expect(validNunjucksString('{% if $state.name %}')).toEqual(false); +}); + +test('validNunjucksString - {% if %} return error', () => { + expect(validNunjucksString('{% if $state.name %} true {% endif %}', true)).toEqual(true); + expect(validNunjucksString('{% if $state.name %}', true)).toMatchInlineSnapshot(` + Object { + "message": "(unknown path) + parseIf: expected elif, else, or endif, got end of file", + "name": "Template render error", + } + `); +}); + +test('nunjucksFunction - non-string template', () => { + const bool = nunjucksFunction(true); + expect(bool('100')).toEqual(true); + const number = nunjucksFunction(100); + expect(number('100')).toEqual(100); + const obj = nunjucksFunction({ x: 1 }); + expect(obj('100')).toEqual({ x: 1 }); +}); + +test('nunjucksFunction - memoization', () => { + const func1 = nunjucksFunction('$ {{value}}'); + expect(func1('100')).toEqual('$ 100'); + const memo = nunjucksFunction('$ {{value}}'); + expect(memo('100')).toEqual('$ 100'); + expect(memo).toBe(func1); +}); diff --git a/yarn.lock b/yarn.lock index f492955ea..20dbb362f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2963,6 +2963,21 @@ __metadata: languageName: unknown linkType: soft +"@lowdefy/nunjucks@workspace:packages/nunjucks": + version: 0.0.0-use.local + resolution: "@lowdefy/nunjucks@workspace:packages/nunjucks" + dependencies: + "@babel/cli": 7.11.6 + "@babel/core": 7.11.6 + "@babel/preset-env": 7.11.5 + "@lowdefy/type": 1.0.1 + babel-jest: 26.5.2 + jest: 26.5.2 + nunjucks: 3.2.2 + nunjucks-date-filter: 0.1.1 + languageName: unknown + linkType: soft + "@lowdefy/poc-express@workspace:packages/express": version: 0.0.0-use.local resolution: "@lowdefy/poc-express@workspace:packages/express" @@ -4295,6 +4310,13 @@ __metadata: languageName: node linkType: hard +"a-sync-waterfall@npm:^1.0.0": + version: 1.0.1 + resolution: "a-sync-waterfall@npm:1.0.1" + checksum: da11585ce7067691fa27ce2c4b731dfb632b090cfe32decb8a6f6e9b2cd6150353a5d78f1eb4555d4a9f8849eb66f443b34f3324dd0099badf208aea335be7f4 + languageName: node + linkType: hard + "abab@npm:^2.0.3": version: 2.0.5 resolution: "abab@npm:2.0.5" @@ -5034,7 +5056,7 @@ __metadata: languageName: node linkType: hard -"asap@npm:^2.0.0, asap@npm:~2.0.3": +"asap@npm:^2.0.0, asap@npm:^2.0.3, asap@npm:~2.0.3": version: 2.0.6 resolution: "asap@npm:2.0.6" checksum: 3d314f8c598b625a98347bacdba609d4c889c616ca5d8ea65acaae8050ab8b7aa6630df2cfe9856c20b260b432adf2ee7a65a1021f268ef70408c70f809e3a39 @@ -5991,7 +6013,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.2.2": +"chokidar@npm:^3.2.2, chokidar@npm:^3.3.0": version: 3.4.2 resolution: "chokidar@npm:3.4.2" dependencies: @@ -6274,6 +6296,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^5.1.0": + version: 5.1.0 + resolution: "commander@npm:5.1.0" + checksum: d16141ea7f580945156fb8a06de2834c4647c7d9d3732ebd4534ab8e0b7c64747db301e18f2b840f28ea8fef51f7a8d6178e674b45a21931f0b65ff1c7f476b3 + languageName: node + linkType: hard + "comment-json@npm:^2.2.0": version: 2.4.2 resolution: "comment-json@npm:2.4.2" @@ -12247,7 +12276,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"moment@npm:^2.24.0, moment@npm:^2.25.3": +"moment@npm:^2.24.0, moment@npm:^2.25.3, moment@npm:^2.9.0": version: 2.29.1 resolution: "moment@npm:2.29.1" checksum: 86729013febf7160de5b93da69273dd304d674b0224f9544b3abd09a87671ddd2cdd57598261ce57588910d63747ffd5590965e83c790d8bf327083c0e0a06e0 @@ -12726,6 +12755,32 @@ fsevents@^1.2.7: languageName: node linkType: hard +"nunjucks-date-filter@npm:0.1.1": + version: 0.1.1 + resolution: "nunjucks-date-filter@npm:0.1.1" + dependencies: + moment: ^2.9.0 + checksum: 45663784cf45758ac05bcd0e796e332b96601e934f95782cdf0a712e1ace14712b49dcbed750a986bd5706608d08edbad3588d66a9d436a036c7e0032ebb30e2 + languageName: node + linkType: hard + +"nunjucks@npm:*, nunjucks@npm:3.2.2": + version: 3.2.2 + resolution: "nunjucks@npm:3.2.2" + dependencies: + a-sync-waterfall: ^1.0.0 + asap: ^2.0.3 + chokidar: ^3.3.0 + commander: ^5.1.0 + dependenciesMeta: + chokidar: + optional: true + bin: + nunjucks-precompile: bin/precompile + checksum: ad7ac5ab621d30375ae8bd5e2f1fa4c87e652079179b63a8dc6d0fdb483ab283ba042f38675553a56db606a1accec19c1aade37b2e437520ab4e398b5f1dfbdd + languageName: node + linkType: hard + "nwsapi@npm:^2.2.0": version: 2.2.0 resolution: "nwsapi@npm:2.2.0"