Skip to content

Commit

Permalink
v8.0.0 (#53)
Browse files Browse the repository at this point in the history
* updated the minor deps

* replace tslint with eslint and update a few deps

* put the vscode ruler at 100 ch

* improve type annotations (no functionality change)

* make eslint and rollup work again

* add the dist-test to the build process

* improve linting or silence it when tricky

* update the examples and run them

* add a few .github files

* add a note to the changelog

* update the performance comparison

* improve perf messages

* run perf tests more times to get a better picture

* doc: add a note about security support

* fix: format the rollup config

* refactor: small syntax improvement

* refactor: use util type checks whenever possible

* chore: some basic formatting

* refactor: move the template type checking to tokenize

* feat: add isInt() util function

* refactor: remove the extra confusing param

* fix: Renderer should check that it gets an object option

* refactor: pass options object to tokenize()

* update a few deps and fix broken changes

add nvm support
addressed some known security issues
refactored the dist-test
clean up tests

* update packages

* update some more deps

* add a barebone playground

* improve playground

* improve the playground

* add a bit more info to the changelog

* get() takes an options object

* applied prettier and made eslint happy

* rename a perf file

* fix a broken path

* add the depth option. Close #34

* make travis happy

* validate the depth option

* make it slightly shorter to minify

* add a few empty lines to improve readability

* increase the cache size

* refactor the perf test script

* refactor the perf script further

* improve the output of the perf comparison

* improve the deep perf test

* fix a bug in long template perf test

* improve typedoc and annotations

* trigger github page build

* change master to update (github pages)

* set local_dir

* remove the markdown docs in favour of gh-pages docs

* refer to examples dir in README

* slightly improve the playground

* travis only deploys on lts/*

* travis deploy docs only on master

* travis deploy only on node version specified in .nvm

* 8.0.0
  • Loading branch information
userpixel authored Aug 8, 2020
1 parent 4499803 commit c111ef7
Show file tree
Hide file tree
Showing 61 changed files with 4,190 additions and 2,242 deletions.
4 changes: 1 addition & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.js]
max_line_length = 80
max_line_length = 100

[*.md]
max_line_length = 0
Expand Down
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
dist-test/
examples/
perf/
68 changes: 68 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"warn",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"warn",
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"semi": [
"warn",
"never"
],
"no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": true
}
}
],
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/restrict-template-expressions": "off"
}
}
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Current behavior

# Expected behavior
7 changes: 7 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Summary

# Checklist

- [ ] The work was discussed in an issue prior to making a PR
- [ ] All new functionality is tested
- [ ] Any documentation that will be affected is updated
1 change: 1 addition & 0 deletions .github/SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If you have questions or suggestions, [create an issue](https://github.com/userpixel/micromustache/issues/new).
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ node_js:
script: npm t
before_deploy: npm run docs
deploy:
local_dir: docs
provider: pages
skip_cleanup: true
github_token: $GITHUB_TOKEN
keep_history: true
on:
branch: master
node_js: node
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.rulers": [80]
"editor.rulers": [100],
"editor.formatOnSave": true
}
5 changes: 5 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
"kind": "test",
"isDefault": true
}
},
{
"type": "npm",
"script": "lint",
"problemMatcher": "$eslint-stylish"
}
]
}
22 changes: 17 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## Unreleased

- Support [optional chaining syntax](https://github.com/tc39/proposal-optional-chaining)
- Support [optional chaining syntax](https://github.com/tc39/proposal-optional-chaining). But the default behaviour of mustache is like we use `?.` everywhere we write `.`.
- Support comments: `{{! ...}}` like MustacheJS
- Reenable tests/ for mustache compatibility and add relevant options
- Add a string literal tag function (generic render)
- Add the possibility to process variable names before and after they are resolved using `get()`. This can allow HTML escaping for example.
- Add the possibility to process variable names before and after they are resolved using `get()`. This can allow HTML escaping for example. Also see #50

## 8.0.0

- The CommonJS file has changed name: V7=`dist/micromustache.js` v8=`dist/micromustache.cjs`. If you just use `require('micromustache')` it sould work without any change.
- The `depth` option is added
- Updated the dependencies
- Addressed known security issues

### BREAKING CHANGES

- The `scope` could be a `function` as well, but with this release we only accept `Object`.
- Previously string characters could be accessed with array-index syntax, but now it is not possible (eg. `render('{{str[0]}}', { str: 'Hello' })` will not return `'H'` anymore)
- Drop support for Safari10

## 7.0.0

BREAKING CHANGES:
### BREAKING CHANGES:

- **The CLI is removed**
- Variable names cannot be longer than 1000 characters
Expand All @@ -30,7 +42,7 @@ BREAKING CHANGES:
- A change in terminology to better reflect JavaScript terms: What Mustache and the previous version of the lib called `view` is not called `scope`.
- Expose a CommonJS build for the browser limited to ECMAScript 5 features.

BREAKING CHANGES:
### BREAKING CHANGES:
- **The biggest change is that if you used `compile()` in version 5, it returned a function but since version 6, it returns an object that _has_ a `render()` function**
- The behaviour of the resolver function has changed: In v5 if the resolver threw an error we fell back to the standard `.get()` functionality but v6 just throws that error in an effort to make debugging easier.
- We don't use default exports anymore so `const render = require('micromustache/render')`
Expand Down
111 changes: 9 additions & 102 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

A **secure**, fast and lightweight template engine with some handy additions.

**Think of it as a sweet spot between plain text interpolation and [mustache.js](https://github.com/janl/mustache.js); Certainly not as logic-ful as [Handlebars](http://handlebarsjs.com/)! Sometimes a stricter syntax is the right boundary to reduce potential errors and improve performance.**
**Think of it as a sweet spot between plain text interpolation and [mustache.js](https://github.com/janl/mustache.js); Certainly not as logic-ful as [Handlebars](http://handlebarsjs.com/)! Sometimes a stricter syntax is the right boundary to reduce potential errors and improve performance. This tool has a limited scope that doesn't attempt to solve everybody's use case, and instead do a specific thing well.**

* 🏃 **2x-3x** faster than MustacheJS (_Micromustache is the fastest template engine that doesn't need pre-compilation and still works in CSP environments_)
* 🔒 **Secure** has limits for variable length, number of interpolations, nesting debth and common Javascript pitfalls (`__proto__`, `constructor`, getters/etc). Works in CSP environments (no usage of `eval()` or `new Function()`). Published only with 2FA. [No regexp](https://medium.com/@liran.tal/node-js-pitfalls-how-a-regex-can-bring-your-system-down-cbf1dc6c4e02).
Expand Down Expand Up @@ -197,109 +197,12 @@ console.log($({ name })`Hello {name}!`)

# API

## `render(template, scope, options)`
[On Github pages](https://userpixel.github.io/micromustache/)

Replaces every {{varName}} inside the template with values from the scope parameter.
# Examples

###### Params

* `template` The template string containing zero or more `{{varName}}` as placeholders for looking up values from the `scope` parameter.
* `scope?: object` An object containing values for variable names from the the template. If it's omitted, we default to an empty object. Since functions are objects in javascript, the `scope` can technically be a function too but it won't be called. It'll be treated as an object and its properties will be used for the lookup.
* `options?: object` see below 👇

###### Returns

The template string where its variable names replaced with corresponding values

###### Throws

* `TypeError` if the `template` is not a string, `scope` is not an object, or `options` is invalid
* `SyntaxError` if the template does not comply with the syntax

## `renderFn(template, resolveFn, scope, options)`

Same as render but accepts a function that allows you to resolve the variable name to a value as you choose. _Tip: you may do some extra processing and use the `get()` function underneath but that's up to you._

**WARNING: When dealing with user input, always make sure to validate it.**

###### New params

* `resolveFn: (varName, scope) => any` a function that takes a variable name and resolves it to a value. The value can be a number, string or boolean. If it is not, it'll be "stringified".

**WARNING: When dealing with user input, always make sure to validate it.**

## `renderFnAsync(template, resolveFnAsync, scope, options)`

Same as `renderFn()` but expects a resolver function that always returns a promise.

###### New params

* `resolveFnAsync: (varName, scope) => Promise<any>` a function that takes a variable name and returns a promise that resolves to a value. The value can be a number, string or boolean. If it is not, it'll be "stringified".

###### Returns

A promise that resolves to the final output once all the `resolveFnAsync` functions are resolved

## `compile(template, options)`

Compiles a template and returns an object that has the render functions. This drammatically improves the interpolation speed (2x-3x) compared to `render()`.

###### Params

* `template` same template that is passed to `render()`
* `options?: object` see below 👇

###### Returns

An object with 3 methods:
* `render(scope)` same as the `render()` function above but without the `template` parameter
* `renderFn(resolveFn, scope)` same as the `renderFn()` function above but without the `template` parameter
* `renderFnAsync(resolveFnAsync, scope)` same as the `renderFnAsync()` function above but without the `template` parameter

###### Throws

* `TypeError` if the `template` is not a string or `options` is invalid.
* `SyntaxError` if the template does not comply with the syntax.

## `get(scope, varName, propExists)`

A useful utility function that is used internally to lookup a variable name as a path to a property in an object.

###### Params

* `scope` same as the `scope` parameter to the `render()` function above.
* `varName: string | string[]` a string like `a.b.c` or `a['b'].c`. It can also be a path array like `['a', 'b', 'c']` (same as [lodash's get()](https://lodash.com/docs/4.17.11#get)). _Tip: the array version is a whole lot faster because we skips parsing it._
* `propExists?: boolean = false` see the meaning of this param under the `options` below.


> Differences with JavaScript:
> * No support for keys that include `[` or `]`. ex. `a['[']`
> * No support for keys that include `'` or `"`. ex. `a['"']`
> * `foo[bar]` is allowed and treated as `foo['bar']` (this behaviour is similar to how lodash `get()` works). But JavaScript treats `bar` as a variable and tries to lookup its value or throws a `ReferenceError` if there is no variable called `bar`.
###### Returns

The value or `undefined`.
If the scope is `undefined` or `null` the result is always `undefined`.

###### Throws

* `ReferenceError` if it cannot find a value in the specified path and `propExists` is set.


## `options`

All the functions that can take an option, expect it as an object with these properties:

* `explicit?: boolean = false` When set to a truthy value, rendering literally puts a `'null'` or `'undefined'` for values that are `null` or `undefined`. By default it swallows those values to be compatible with Mustache.
* `propsExist?: boolean = false` When set to a truthy value, we throw a `ReferenceError` for invalid varNames. Invalid varNames are the ones that do not exist in the scope. By default, invalid varNames will be resolved to an empty string.
If a value does not exist in the scope, two things can happen:
- if `propsExist` is falsy (default), the value will be resolved to an empty string
- if `propsExist` is truthy, a `ReferenceError` will be thrown
* `validateVarNames?: boolean = false` When set to a truthy value, validates the variable names
* `tags?: string[2] = ['{{', '}}']` The string symbols that mark the opening and closing of a variable name in the template.

[Full API docs](https://userpixel.github.io/micromustache) _(beta)_
Check out the [`examples`](./examples) directory.
_Note that they need you to build the project locally._

# FAQ

Expand All @@ -309,6 +212,10 @@ If a value does not exist in the scope, two things can happen:

[On wiki](https://github.com/userpixel/micromustache/wiki/Known-issues)

# License

[MIT](./LICENSE.md)

---

_Made in Sweden 🇸🇪 by [@alexewerlof](https://mobile.twitter.com/alexewerlof)_
Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

## Supported Versions

Due to available resources and differences in code, only version 6+ is supported.
Due to available resources and differences in code, only the recent versions are supported:

| Version | Supported |
| ------- | ------------------ |
| 8.x.x | :white_check_mark: |
| 7.x.x | :white_check_mark: |
| 6.x.x | :white_check_mark: |
| 6.x.x | :x: |
| < 5.x | :x: |

## Reporting a Vulnerability
Expand Down
28 changes: 28 additions & 0 deletions dist-test/file-names.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const am = require('am')
const assert = require('assert')
const {
promises: { readdir, access },
} = require('fs')
const { resolve, extname } = require('path')
const pkgJson = require('../package.json')

const rootDir = resolve(__dirname, '..')
const distDir = resolve(rootDir, './dist')

async function main() {
const validFileExtentions = ['.mjs', '.js', '.cjs', '.map']
for await (let file of await readdir(distDir, { withFileTypes: true })) {
console.log(`Checking ${file.name}...`)
// only inspect files
if (file.isFile()) {
// does not contain any unexpected file extension
const fileExtension = extname(file.name)
assert(validFileExtentions.includes(fileExtension))
// Has the package name in its file name
assert(file.name.includes(pkgJson.name))
}
}
// pkg-ok checks "package.json:main", "package.json:module" and "package.json:types"
}

am(main)
8 changes: 0 additions & 8 deletions dist-test/node-cjs-as-esm.mjs

This file was deleted.

5 changes: 3 additions & 2 deletions dist-test/node-cjs-ext.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { render } = require('../dist/micromustache.js')
// This is a "normal" Node js file which tries to load the CommonJS version specifying its extension
const { render } = require('../dist/micromustache.cjs')

const result = render('Hello {{ name }}!', { name: 'Alex' })
if (result !== 'Hello Alex!') {
throw new Error(`The result does not match ${result}`)
throw new Error(`The result does not match ${result}`)
}
console.log('✔', __filename)
9 changes: 9 additions & 0 deletions dist-test/node-cjs-no-ext-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This is a Node CommonJS file which tries to use the CommonJS export as specified in the package.json
const mm = require('..')

const { render } = mm
const result = render('Hello {{ name }}!', { name: 'Alex' })
if (result !== 'Hello Alex!') {
throw new Error(`The result does not match ${result}`)
}
console.log('✔', __filename)
Loading

0 comments on commit c111ef7

Please sign in to comment.