Skip to content

Commit

Permalink
feat: Adds support for options to CLI and improves usability (#586)
Browse files Browse the repository at this point in the history
* Improved UX of CLI

* Updated docs and setup tutorial

* Corrected version number in TODO comments

* Downgraded lock file version

* Restored previous lock file

* Removed --keep-output-type option

* Switch to using async IO to avoid problems stdin in some scenarios

https://stackoverflow.com/questions/40362369/stdin-read-fails-on-some-input

https://stackoverflow.com/questions/40362369/stdin-read-fails-on-some-input

* Addressed feedback from code review

* Changed to curl-like systax for specifing inputs as literals, paths or stdin

---------

Co-authored-by: Daniel Rosenberg <[email protected]>
  • Loading branch information
DaRosenberg and Daniel Rosenberg committed Feb 14, 2023
1 parent a661853 commit 24c8a1e
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 24 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ Or use the UMD bundle from jsDelivr:
<script src="https://cdn.jsdelivr.net/npm/liquidjs/dist/liquid.browser.min.js"></script>
```

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

Expand Down
141 changes: 128 additions & 13 deletions bin/liquid.js
Original file line number Diff line number Diff line change
@@ -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 | @path>', 'liquid template to render (@- to read from stdin)') // TODO: Change to argument in 11.0
.option('-c, --context <json | @path>', 'input context in JSON format (@- to read from stdin)')
.option('-o, --output <path>', 'write rendered output to file (omit to write to stdout)')
.option('--cache [size]', 'cache previously parsed template structures (default cache size: 1024)')
.option('--extname <string>', '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 <path...>', '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 <string>', 'left delimiter to use for liquid outputs')
.option('--output-delimiter-right <string>', 'right delimiter to use for liquid outputs')
.option('--partials <path...>', 'directories from where to resolve partials (defaults to --root)')
.option('--preserve-timezones', 'preserve input timezone in date filter')
.option('--root <path...>', '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 <value>', '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 resolveContext (contextOption) {
let contextJson = '{}'
if (contextOption) {
contextJson = await resolveInputOption(contextOption)
}
const context = JSON.parse(contextJson)
return context
}

async function render (tpl) {
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 html = await liquid.parseAndRender(tpl, context)
process.stdout.write(html)
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
}
39 changes: 35 additions & 4 deletions docs/source/tutorials/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
"typedoc-plugin-markdown": "^2.2.17",
"typescript": "^4.5.3"
},
"dependencies": {
"commander": "^10.0.0"
},
"release": {
"branch": "master",
"plugins": [
Expand Down Expand Up @@ -160,6 +163,5 @@
"pre-commit": "npm run check",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"dependencies": {}
}
}

0 comments on commit 24c8a1e

Please sign in to comment.