Files
liquidjs/docs/source/tutorials/options.md
T
964a63b362 fix: v11 scope security and ownPropertyOnly hardening (#898) (#938)
* feat: block dangerous scope keys and harden findScope (#898)

Co-authored-by: Cursor <[email protected]>

* docs: fix ownPropertyOnly default in security model

Co-authored-by: Cursor <[email protected]>

* feat: harden scope writes, iteration, and readSize (#898)

Block writes to dangerous keys in assign/capture/increment/decrement, use own-property Symbol.iterator for plain objects when ownPropertyOnly is true, fix inherited size reads, and sanitize filter iteration scopes.

Co-authored-by: Cursor <[email protected]>

* fix: tie proto key blocking to ownPropertyOnly policy

Block __proto__, constructor, and prototype only when ownPropertyOnly
is true or when access would traverse the prototype chain. Allow own
properties with those names when ownPropertyOnly is false.

Co-authored-by: Cursor <[email protected]>

* fix: revert ownPropertyOnly iteration hardening

Iteration is documented as an ownPropertyOnly exception; restore
isIterable/toEnumerable and document inherited Symbol.iterator behavior.

Co-authored-by: Cursor <[email protected]>

* docs: fix ownPropertyOnly blocked-keys wording in options

Co-authored-by: Cursor <[email protected]>

* fix: unify blocked-key checks in findScope

Use shouldBlockScopeKeyRead in findScope hasKey so inherited
constructor/__proto__/prototype do not falsely match environments.
Remove redundant globals hasKey check; globals remains the fallback scope.

Co-authored-by: Cursor <[email protected]>

* test: trim redundant scope-security integration tests

Co-authored-by: Cursor <[email protected]>

* refactor: move readSize to Context methods

Move readSize, readFirst, and readLast to private Context methods using this.ownPropertyOnly. Remove redundant shouldBlockScopeKeyRead from findScope.

Co-authored-by: Cursor <[email protected]>

* refactor: wrap plain scopes in Context.push()

Centralize null-prototype scope creation in push() so callers pass plain objects; Drop instances and existing null-proto frames are pushed as-is. Remove sanitizeScope in favor of createScope via Object.assign.

* refactor: drop redundant tag write-path blocking

Write blocking on assign/capture/increment/decrement duplicated read-side
protection in readJSProperty; null-proto scopes from push already prevent
prototype pollution on managed writes.

Co-authored-by: Cursor <[email protected]>

* fix: address scope-security review findings

Restore null-prototype hardening for Jekyll include bindings, colocate blocked-key checks with readJSProperty, align ownPropertyOnly JSDoc with security docs, and drop integration tests duplicated in context.spec.

Co-authored-by: Cursor <[email protected]>

* refactor: simplify scope-security MR

Drop null-prototype passthrough in push(), inline blocked-key checks,
remove redundant createScope at include tag, trim verbose docs, and
drop implementation-detail unit tests.

Co-authored-by: Cursor <[email protected]>

* refactor: trim scope-security helpers and docs

Inline findScope and blocked-key checks, shorten ownPropertyOnly docs,
and drop implementation-detail push() unit tests.

Co-authored-by: Cursor <[email protected]>

* refactor: encapsulate Drop passthrough in createScope

* refactor: drop redundant typeof in blocked key check

Set.has already returns false for non-string PropertyKey values; widen
BLOCKED_SCOPE_KEYS type so TypeScript accepts the direct has(key) call.

Co-authored-by: Cursor <[email protected]>

* docs: shorten ownPropertyOnly proto-key wording

Co-authored-by: Cursor <[email protected]>

* fix: clarify blocked key checks in readJSProperty

Split the OR condition into two explicit checks so inherited proto keys are always blocked and own proto keys are blocked only when ownPropertyOnly is true.

Co-authored-by: Cursor <[email protected]>

* fix: apply ownPropertyOnly uniformly in readJSProperty

Proto keys block inherited access only; ownPropertyOnly is checked once before return for all keys. Own __proto__/constructor/prototype properties are readable—sanitize untrusted scope input.

Co-authored-by: Cursor <[email protected]>

* fix: remove BLOCKED_SCOPE_KEYS; ownPropertyOnly is the sole read policy

Proto keys were incorrectly blocked even when ownPropertyOnly=false.
Inherited access is now gated only by ownPropertyOnly; docs updated.

Co-authored-by: Cursor <[email protected]>

* fix: restore BLOCKED_SCOPE_KEYS gated by ownPropertyOnly

Dangerous keys (__proto__, constructor, prototype) are blocked only when
ownPropertyOnly is true (default). With false, full prototype access is
allowed as an explicit opt-out; use bourne for untrusted input.

Co-authored-by: Cursor <[email protected]>

* docs: shorten ownPropertyOnly entry in options tutorial

Details live in Security Model; keep options.md consistent with strictFilters/strictVariables tone.

Co-authored-by: Cursor <[email protected]>

* docs: simplify ownPropertyOnly JSDoc in LiquidOptions

Co-authored-by: Cursor <[email protected]>

* test: cover readSize branches in Context

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-07-24 00:53:21 +08:00

8.8 KiB

title
title
Options

The Liquid constructor accepts a plain object as options to define the behavior of LiquidJS. All of these options are optional thus we can specify any of them, for example the cache option:

const { Liquid } = require('liquidjs')
const engine = new Liquid({
    cache: true
})

{% note info API documentation %} Following is an overview for all the options. For exact types and signatures, see LiquidOptions | API. {% endnote %}

cache

cache is used to improve performance by caching previously parsed template structures, especially in cases when we repeatedly parse or render files.

It defaults to false. When set to true, a default LRU cache of size 1024 will be enabled. It can also be a number indicating the cache size you want.

Additionally, it can also be a custom cache implementation. See Caching for details.

Partials/Layouts

root is used to specify template directories for LiquidJS to look up and read template files. Can be a single string or an array of strings. See Render Files for details.

layouts is used to specify template directories for LiquidJS to look up files for {% layout %}. Same format as root and will default to root if not specified.

partials is used to specify template directories for LiquidJS to look up files for {% render %} and {% include %}. Same format as root and will default to root if not specified.

relativeReference is set to true by default to allow relative filenames. Note that relatively referenced files also need to be within the corresponding root. For example you can reference another file like {% render ../foo/bar %} as long as ../foo/bar is also within partials directory.

dynamicPartials

Note: for historical reasons, it's named dynamicPartials but it also works for layouts.

dynamicPartials indicates whether or not to treat filename arguments in include, render, layout tags as a variable. Defaults to true. For example, render the following snippet with scope { file: 'foo.html' } will include the foo.html:

{% include file %}

Setting dynamicPartials: false, LiquidJS will try to include the file named file, which is weird but allows simpler syntax if your template relations are static:

{% liquid foo.html %}

{% note warn Common Pitfall %} LiquidJS defaults this option to true to be compatible with shopify/liquid, but if you're from eleventy it's set to false by default (see Quoted Include Paths) which I believe is trying to be compatible with Jekyll.{% endnote %}

Jekyll include

{% since %}v9.33.0{% endsince %}

jekyllInclude is used to enable Jekyll-like include syntax. Defaults to false, when set to true:

  • Filename will be static: dynamicPartials now defaults to false (instead of true). And you can set dynamicPartials back to true.
  • Use = instead of : to separate parameter key-values.
  • Parameters are under include variable instead of current scope.

For example in the following template, name.html is not quoted, header and "HEADER" are separated by =, and the header parameter is referenced by include.header. For more details, see include.

// entry template
{% include article.html header="HEADER" content="CONTENT" %}

// article.html
<article>
  <header>{{include.header}}</header>
  {{include.content}}
</article>

extname

extname defines the default extension name to be appended into filenames if the filename has no extension name. Defaults to '' which means it's disabled by default. By setting it to .liquid:

{% render "foo" %}  there's no extname, adds `.liquid` and loads foo.liquid
{% render "foo.html" %}  there is an extname already, loads foo.html directly

{% note info Legacy Versions %} Before 2.0.1, extname is set to .liquid by default. To change that you need to set extname: '' explicitly. See #41 for details. {% endnote %}

fs

fs is used to define a custom file system implementation which will be used by LiquidJS to look up and read template files. See Abstract File System for details.

globals

globals is used to define global variables available to all templates even in cases of render tag. See 3185 for details.

jsTruthy

jsTruthy is used to use standard JavaScript truthiness rather than Shopify's.

It defaults to false. For example, when set to true, a blank string would evaluate to false with jsTruthy. With Shopify's truthiness, a blank string is true.

outputEscape

outputEscape can be used to automatically escape output strings. It can be one of "escape", "json", or (val: unknown) => string, defaults to undefined.

  • For untrusted output variables, set outputEscape: "escape" makes them be HTML escaped by default. You'll need raw filter for direct output.
  • "json" is useful when you're using LiquidJS to create valid JSON files.
  • It can even be a function that allows you to control what variables are output throughout LiquidJS. Please note the input can be any type other than string, e.g. a filter may return a non-string value.

Date

timezoneOffset is used to specify a different timezone to output dates, your local timezone will be used if not specified. For example, set timezoneOffset: 0 to output all dates in UTC/GMT 00:00.

preserveTimezones is a boolean that affects only literal timestamps. When set to true, all literal timestamps will remain the same when output. This is a parser option, so Date objects passed to LiquidJS as data will not be affected. Note that preserveTimezones has a higher priority than timezoneOffset.

dateFormat is used to specify a default format to output dates. %A, %B %-e, %Y at %-l:%M %P %z will be used if not specified. For example, set dateFormat: %Y-%m-%dT%H:%M:%S:%LZ to output all dates in JavaScript Date.toJson() format.

Trimming

greedy, trimOutputLeft, trimOutputRight, trimTagLeft, trimTagRight options are used to eliminate extra newlines and indents in templates around Liquid Constructs. See Whitespace Control for details.

Delimiter

outputDelimiterLeft, outputDelimiterRight, tagDelimiterLeft, tagDelimiterRight are used to customize the delimiters for LiquidJS Tags and Filters. For example with outputDelimiterLeft: <%=, outputDelimiterRight: %> we are able to avoid conflicts with other languages:

<%= username | append: ", welcome to LiquidJS!" %>

Strict

strictFilters is used to assert filter existence. If set to false, undefined filters will be skipped. Otherwise, undefined filters will cause a parse exception. Defaults to false.

strictVariables is used to assert variable existence. If set to false, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause a render exception. Defaults to false.

lenientIf modifies the behavior of strictVariables to allow handling optional variables. If set to true, an undefined variable will not cause an exception in the following two situations: a) it is the condition to an if, elsif, or unless tag; b) it occurs right before a default filter. Irrelevant if strictVariables is not set. Defaults to false.

ownPropertyOnly limits template property reads on plain scope objects to own properties. Defaults to true. See Security Model.

{% note info Nonexistent Tags %} Nonexistent tags always throw errors during parsing and this behavior cannot be customized. {% endnote %}

Parameter Order

Parameter orders are ignored by default, for example {% for i in (1..8) reversed limit:3 %} will always perform limit before reversed, even if reversed occurs before limit. To make parameter order respected, set orderedFilterParameters to true. Its default value is false.