diff --git a/README.md b/README.md
index a0898b966..3ea202508 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,13 @@ Or use the UMD bundle from jsDelivr:
```
-More details, refer to [The Setup Guide][setup].
+Or render directly from CLI using npx:
+
+```bash
+npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
+```
+
+For more details, refer to the [Setup Guide][setup].
## Related Projects
diff --git a/bin/liquid.js b/bin/liquid.js
index a331b4fd6..9d54b865d 100755
--- a/bin/liquid.js
+++ b/bin/liquid.js
@@ -1,24 +1,139 @@
#!/usr/bin/env node
+const fs = require('fs/promises')
const Liquid = require('..').Liquid
-const contextArg = process.argv.slice(2)[0]
-let context = {}
-if (contextArg) {
- if (contextArg.endsWith('.json')) {
- const fs = require('fs')
- context = JSON.parse(fs.readFileSync(contextArg, 'utf8'))
+// Preserve compatibility by falling back to legacy CLI behavior if:
+// - stdin is redirected (i.e. not connected to a terminal) AND
+// - there are either no arguments, or only a single argument which does not start with a dash
+// TODO: Remove this fallback for 11.0
+
+let renderPromise = null
+if (!process.stdin.isTTY && (process.argv.length === 2 || (process.argv.length === 3 && !process.argv[2].startsWith('-')))) {
+ renderPromise = renderLegacy()
+} else {
+ renderPromise = render()
+}
+
+renderPromise.catch(err => {
+ process.stderr.write(`${err.message}\n`)
+ process.exitCode = 1
+})
+
+async function render () {
+ const { program } = require('commander')
+
+ program
+ .name('liquidjs')
+ .description('Render a Liquid template')
+ .requiredOption('-t, --template ', 'liquid template to render (@- to read from stdin)') // TODO: Change to argument in 11.0
+ .option('-c, --context ', 'input context in JSON format (@- to read from stdin)')
+ .option('-o, --output ', 'write rendered output to file (omit to write to stdout)')
+ .option('--cache [size]', 'cache previously parsed template structures (default cache size: 1024)')
+ .option('--extname ', 'use a default filename extension when resolving partials and layouts')
+ .option('--jekyll-include', 'use jekyll-style include (pass parameters to include variable of current scope)')
+ .option('--js-truthy', 'use JavaScript-style truthiness')
+ .option('--layouts ', 'directories from where to resolve layouts (defaults to --root)')
+ .option('--lenient-if', 'do not throw on undefined variables in conditional expressions (when using --strict-variables)')
+ .option('--no-dynamic-partials', 'always treat file paths for partials and layouts as a literal value')
+ .option('--no-greedy', 'disable greedy matching for --trim* options')
+ .option('--no-relative-reference', 'require absolute file paths for partials and layouts')
+ .option('--ordered-filter-parameters', 'respect parameter order when using filters')
+ .option('--output-delimiter-left ', 'left delimiter to use for liquid outputs')
+ .option('--output-delimiter-right ', 'right delimiter to use for liquid outputs')
+ .option('--partials ', 'directories from where to resolve partials (defaults to --root)')
+ .option('--preserve-timezones', 'preserve input timezone in date filter')
+ .option('--root ', 'directories from where to resolve partials and layouts (defaults to ".")')
+ .option('--strict-filters', 'throw on undefined filters instead of skipping them')
+ .option('--strict-variables', 'throw on undefined variables instead of rendering them as empty string')
+ .option('--tag-delimiter-left', 'left delimiter to use for liquid tags')
+ .option('--tag-delimiter-right', 'right delimiter to use for liquid tags')
+ .option('--timezone-offset ', 'JavaScript timezone name or timezoneOffset value to use in date filter (defaults to local timezone)')
+ .option('--trim-output-left', 'trim whitespace from left of liquid outputs')
+ .option('--trim-output-right', 'trim whitespace from right of liquid outputs')
+ .option('--trim-tag-left', 'trim whitespace from left of liquid tags')
+ .option('--trim-tag-right', 'trim whitespace from right of liquid tags')
+ .showHelpAfterError('Use -h or --help for additional information.')
+ .parse()
+
+ const options = program.opts()
+
+ if (Object.values(options).filter((value) => value === '@-').length > 1) {
+ throw new Error(`The stdin input specifier '@-' must only be used once.`)
+ }
+
+ const template = await resolveInputOption(options.template)
+ const context = await resolveContext(options.context)
+ const liquid = new Liquid(options)
+ const output = liquid.parseAndRenderSync(template, context)
+ if (options.output) {
+ await fs.writeFile(options.output, output)
} else {
- context = JSON.parse(contextArg)
+ process.stdout.write(output)
}
}
-let tpl = ''
-process.stdin.on('data', chunk => (tpl += chunk))
-process.stdin.on('end', () => render(tpl))
-
-async function render (tpl) {
- const liquid = new Liquid()
- const html = await liquid.parseAndRender(tpl, context)
- process.stdout.write(html)
+async function resolveContext (contextOption) {
+ let contextJson = '{}'
+ if (contextOption) {
+ contextJson = await resolveInputOption(contextOption)
+ }
+ const context = JSON.parse(contextJson)
+ return context
+}
+
+async function resolveInputOption (option) {
+ let content = null
+ if (option) {
+ if (option === '@-') {
+ content = await readStream(process.stdin)
+ } else if (option.startsWith('@')) {
+ const filePath = option.slice(1)
+ const stat = await fs.stat(filePath, { throwIfNoEntry: false })
+ if (!stat || !stat.isFile) {
+ throw new Error(`'${filePath}' does not exist or is not a file`)
+ }
+ content = await fs.readFile(filePath, 'utf8')
+ } else {
+ content = option
+ }
+ }
+ return content
+}
+
+async function readStream (stream) {
+ const chunks = []
+ for await (const chunk of stream) {
+ chunks.push(chunk)
+ }
+ return Buffer.concat(chunks).toString('utf8')
+}
+
+// TODO: Remove for 11.0
+async function renderLegacy () {
+ process.stderr.write('Reading template from stdin. This mode will be removed in next major version, use --template option instead.\n')
+ const contextArg = process.argv.slice(2)[0]
+ let context = {}
+ if (contextArg) {
+ const contextJson = await resolveInputOptionLegacy(contextArg)
+ context = JSON.parse(contextJson)
+ }
+ const template = await readStream(process.stdin)
+ const liquid = new Liquid()
+ const output = liquid.parseAndRenderSync(template, context)
+ process.stdout.write(output)
+}
+
+// TODO: Remove for 11.0
+async function resolveInputOptionLegacy (option) {
+ let content = null
+ if (option) {
+ const stat = await fs.stat(option).catch(e => null)
+ if (stat && stat.isFile) {
+ content = await fs.readFile(option, 'utf8')
+ } else {
+ content = option
+ }
+ }
+ return content
}
diff --git a/docs/source/tutorials/setup.md b/docs/source/tutorials/setup.md
index 4568d1a94..8969628a2 100644
--- a/docs/source/tutorials/setup.md
+++ b/docs/source/tutorials/setup.md
@@ -53,20 +53,51 @@ Pre-built UMD bundles are also available:
## LiquidJS in CLI
-LiquidJS is also available from CLI:
+LiquidJS can also be used to render a template directly from CLI using `npx`:
```bash
-echo '{{"hello" | capitalize}}' | npx liquidjs
+npx liquidjs --template '{{"hello" | capitalize}}'
```
-If you pass a path to a JSON file or a JSON string as the first argument, it will be used as the context for your template.
+You can either pass the template inline (as shown above) or you can read it from a file by using the `@` character followed by a path, like so:
```bash
-echo 'Hello, {{ name }}.' | npx liquidjs '{"name": "Snake"}'
+npx liquidjs --template @./some-template.liquid
```
+You can also use the `@-` syntax to read the template from `stdin`:
+
+```bash
+echo '{{"hello" | capitalize}}' | npx liquidjs --template @-
+```
+
+A context can be passed in the same ways (i.e. inline, from a path or piped through `stdin`). The following three are equivalent:
+
+```bash
+npx liquidjs --template 'Hello, {{ name }}!' --context '{"name": "Snake"}'
+npx liquidjs --template 'Hello, {{ name }}!' --context @./some-context.json
+echo '{"name": "Snake"}' | npx liquidjs --template 'Hello, {{ name }}!' --context @-
+```
+
+Note that you can only use the `stdin` specifier `@-` for a single argument. If you try to use it for both `--template` and `--context` you will get an error.
+
+The rendered output is written to `stdout` by default, but you can also specify an output file (if the file exists, it will be overwritten):
+
+```bash
+npx liquidjs --template '{{"hello" | capitalize}}' --output ./hello.txt
+```
+
+You can also pass a number of options to customize template rendering behavior. For example, the `--js-truthy` option can be used to enable JavaScript truthiness:
+
+```bash
+npx liquidjs --template @./some-template.liquid --js-truthy
+```
+
+Most of the [options available through the JavaScript API][options] are also available from the CLI. For help on available options, use `npx liquidjs --help`.
+
## Miscellaneous
A ReactJS demo is also added by [@stevenanthonyrevo](https://github.com/stevenanthonyrevo), see [liquidjs/demo/reactjs/](https://github.com/harttle/liquidjs/blob/master/demo/reactjs/).
[intro]: ./intro-to-liquid.html
+[options]: ./options.md
diff --git a/package-lock.json b/package-lock.json
index ea6f9a808..e5f7f4706 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2802,10 +2802,9 @@
}
},
"commander": {
- "version": "2.20.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
- "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
- "dev": true
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz",
+ "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA=="
},
"commondir": {
"version": "1.0.1",
diff --git a/package.json b/package.json
index e3dcf051b..4f7848e19 100644
--- a/package.json
+++ b/package.json
@@ -111,6 +111,9 @@
"typedoc-plugin-markdown": "^2.2.17",
"typescript": "^4.5.3"
},
+ "dependencies": {
+ "commander": "^10.0.0"
+ },
"release": {
"branch": "master",
"plugins": [
@@ -160,6 +163,5 @@
"pre-commit": "npm run check",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
- },
- "dependencies": {}
+ }
}