mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 20:30:39 -07:00
feat: Adds support for options to CLI and improves usability (#586)
* 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]>
This commit is contained in:
co-authored by
Daniel Rosenberg
parent
a6618538d1
commit
24c8a1e372
@@ -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>
|
<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
|
## Related Projects
|
||||||
|
|
||||||
|
|||||||
+130
-15
@@ -1,24 +1,139 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs/promises')
|
||||||
const Liquid = require('..').Liquid
|
const Liquid = require('..').Liquid
|
||||||
const contextArg = process.argv.slice(2)[0]
|
|
||||||
let context = {}
|
|
||||||
|
|
||||||
if (contextArg) {
|
// Preserve compatibility by falling back to legacy CLI behavior if:
|
||||||
if (contextArg.endsWith('.json')) {
|
// - stdin is redirected (i.e. not connected to a terminal) AND
|
||||||
const fs = require('fs')
|
// - there are either no arguments, or only a single argument which does not start with a dash
|
||||||
context = JSON.parse(fs.readFileSync(contextArg, 'utf8'))
|
// 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 {
|
} else {
|
||||||
context = JSON.parse(contextArg)
|
process.stdout.write(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tpl = ''
|
async function resolveContext (contextOption) {
|
||||||
process.stdin.on('data', chunk => (tpl += chunk))
|
let contextJson = '{}'
|
||||||
process.stdin.on('end', () => render(tpl))
|
if (contextOption) {
|
||||||
|
contextJson = await resolveInputOption(contextOption)
|
||||||
async function render (tpl) {
|
}
|
||||||
const liquid = new Liquid()
|
const context = JSON.parse(contextJson)
|
||||||
const html = await liquid.parseAndRender(tpl, context)
|
return context
|
||||||
process.stdout.write(html)
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,20 +53,51 @@ Pre-built UMD bundles are also available:
|
|||||||
|
|
||||||
## LiquidJS in CLI
|
## LiquidJS in CLI
|
||||||
|
|
||||||
LiquidJS is also available from CLI:
|
LiquidJS can also be used to render a template directly from CLI using `npx`:
|
||||||
|
|
||||||
```bash
|
```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
|
```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
|
## 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/).
|
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
|
[intro]: ./intro-to-liquid.html
|
||||||
|
[options]: ./options.md
|
||||||
|
|||||||
Generated
+3
-4
@@ -2802,10 +2802,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.20.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz",
|
||||||
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
|
"integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"commondir": {
|
"commondir": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|||||||
+4
-2
@@ -111,6 +111,9 @@
|
|||||||
"typedoc-plugin-markdown": "^2.2.17",
|
"typedoc-plugin-markdown": "^2.2.17",
|
||||||
"typescript": "^4.5.3"
|
"typescript": "^4.5.3"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^10.0.0"
|
||||||
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -160,6 +163,5 @@
|
|||||||
"pre-commit": "npm run check",
|
"pre-commit": "npm run check",
|
||||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"dependencies": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user