@rse/nunjucks-cli 1.6.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,131 @@
1
+
2
+ ChangeLog
3
+ =========
4
+
5
+ 2.0.0 (2025-12-21)
6
+ ------------------
7
+
8
+ - REFACTORING: switched to ESLint 9
9
+ - REFACTORING: switched to TypeScript
10
+ - REFACTORING: converted to STX and added packaging
11
+ - MAINTENANCE: upgraded dependencies
12
+
13
+ 1.6.0 (2025-06-02)
14
+ ------------------
15
+
16
+ - REFACTORING: upgraded to ESM land
17
+ - MAINTENANCE: upgraded dependencies
18
+ - MAINTENANCE: bumped year in all copyright messages
19
+
20
+ 1.5.2 (2024-03-11)
21
+ ------------------
22
+
23
+ - MAINTENANCE: updated copyright messages
24
+ - MAINTENANCE: upgraded dependencies
25
+ - MAINTENANCE: updated Dockerfile
26
+
27
+ 1.5.1 (2023-12-25)
28
+ ------------------
29
+
30
+ - MAINTENANCE: upgraded dependencies
31
+
32
+ 1.5.0 (2023-10-02)
33
+ ------------------
34
+
35
+ - FEATURE: support that option -d can occur multiple times (mixes structures deeply)
36
+ - MAINTENANCE: made ESLint happy with newer language features
37
+ - MAINTENANCE: upgraded dependencies
38
+ - MAINTENANCE: added Docker scripts
39
+ - MAINTENANCE: fixed stdver
40
+
41
+ 1.4.5 (2023-08-28)
42
+ ------------------
43
+
44
+ - BUGFIX: fixed once again the repo URL
45
+
46
+ 1.4.4 (2023-08-28)
47
+ ------------------
48
+
49
+ - BUGFIX: fixed repo URL
50
+ - FEATURE: added manpage
51
+
52
+ 1.4.3 (2023-08-27)
53
+ ------------------
54
+
55
+ - MAINTENANCE: linked manpage
56
+ - MAINTENANCE: added stdver
57
+ - MAINTENANCE: improved badges
58
+
59
+ 1.4.2 (2023-08-26)
60
+ ------------------
61
+
62
+ - FEATURE: added Unix manual page
63
+
64
+ 1.4.1 (2023-08-21)
65
+ ------------------
66
+
67
+ - REFACTORING: switched option parser from yargs to commander in order to support -Dx=y and not just -D x=y (with space)
68
+
69
+ 1.4.0 (2023-08-20)
70
+ ------------------
71
+
72
+ - FEATURE: added CLI option -C
73
+ - MAINTENANCE: upgraded dependencies
74
+
75
+ 1.3.1 (2023-08-18)
76
+ ------------------
77
+
78
+ - FEATURE: support local paths, too
79
+
80
+ 1.3.0 (2023-08-18)
81
+ ------------------
82
+
83
+ - FEATURE: added better installation and usage hints
84
+ - REFACTORING: removed addons as they are now in their own package
85
+
86
+ 1.2.1 (2023-08-17)
87
+ ------------------
88
+
89
+ - FEATURE: added uuid global function
90
+ - FEATURE: provided sprintf global function
91
+ - FEATURE: added sprintf filter
92
+ - FEATURE: added pad filter
93
+
94
+ 1.2.0 (2023-08-15)
95
+ ------------------
96
+
97
+ - FEATURE: added jsonpath extension
98
+ - REFACTORING: made jsYAML usage more modern and added support for jsonpath extension
99
+
100
+ 1.1.1 (2023-08-15)
101
+ ------------------
102
+
103
+ - MAINTENANCE: used copyright symbol
104
+
105
+ 1.1.0 (2023-08-15)
106
+ ------------------
107
+
108
+ - FEATURE: allowed NPM modules to be used as extensions
109
+
110
+ 1.0.1 (2023-08-15)
111
+ ------------------
112
+
113
+ - BUGFIX: remembered extensions
114
+
115
+ 1.0.0 (2023-08-15)
116
+ ------------------
117
+
118
+ - FEATURE: added hint to npx usage
119
+
120
+ 0.9.6 (2023-08-15)
121
+ ------------------
122
+
123
+ - MAINTENANCE: final cleanup
124
+ - MAINTENANCE: fixed npm publishing
125
+ - MAINTENANCE: switched to private namespace
126
+
127
+ 0.9.5 (2023-08-15)
128
+ ------------------
129
+
130
+ (first version)
131
+
package/Dockerfile CHANGED
@@ -5,12 +5,12 @@
5
5
  # build arguments (early)
6
6
  ARG IMAGE_PREFIX=docker.io/engelschall/
7
7
  ARG IMAGE_NAME=nunjucks-cli
8
- ARG IMAGE_VERSION=1.5.2
9
- ARG IMAGE_RELEASE=20240311
8
+ ARG IMAGE_VERSION=2.0.0
9
+ ARG IMAGE_RELEASE=20251221
10
10
  ARG IMAGE_ALIAS=latest
11
11
 
12
12
  # derive image from a certain base image
13
- FROM node:24-alpine3.21
13
+ FROM node:24-alpine3.23
14
14
 
15
15
  # add additional build tools
16
16
  RUN apk update && \
package/eslint.mjs CHANGED
@@ -9,10 +9,12 @@ import pluginStd from "neostandard"
9
9
  import pluginN from "eslint-plugin-n"
10
10
  import pluginImport from "eslint-plugin-import"
11
11
  import pluginPromise from "eslint-plugin-promise"
12
+ import pluginTS from "typescript-eslint"
12
13
  import globals from "globals"
13
14
 
14
15
  export default [
15
16
  pluginJs.configs.recommended,
17
+ ...pluginTS.configs.recommended,
16
18
  ...pluginStd({
17
19
  ignores: pluginStd.resolveIgnoresFromGitignore()
18
20
  }),
@@ -22,7 +24,7 @@ export default [
22
24
  "import": pluginImport,
23
25
  "promise": pluginPromise
24
26
  },
25
- files: [ "**/*.js" ],
27
+ files: [ "**/*.js", "**/*.ts" ],
26
28
  ignores: [ "dst/" ],
27
29
  languageOptions: {
28
30
  ecmaVersion: 2022,
@@ -58,7 +60,10 @@ export default [
58
60
  "@stylistic/array-bracket-spacing": "off",
59
61
  "@stylistic/lines-between-class-members": "off",
60
62
  "@stylistic/multiline-ternary": "off",
61
- "@stylistic/quote-props": "off"
63
+ "@stylistic/quote-props": "off",
64
+
65
+ "@typescript-eslint/no-explicit-any": "off",
66
+ "@typescript-eslint/no-unused-vars": [ "error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" } ]
62
67
  }
63
68
  }
64
69
  ]
package/nunjucks.js CHANGED
@@ -4,176 +4,184 @@
4
4
  ** Copyright (c) 2019-2025 Dr. Ralf S. Engelschall <http://engelschall.com>
5
5
  ** Licensed under MIT <http://spdx.org/licenses/MIT.html>
6
6
  */
7
-
8
- /* internal requirements */
9
- import fs from "node:fs"
10
- import path from "node:path"
11
-
7
+ /* built-in requirements */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { createRequire } from "node:module";
12
11
  /* external requirements */
13
- import { Command } from "commander"
14
- import chalk from "chalk"
15
- import jsYAML from "js-yaml"
16
- import nunjucks from "nunjucks"
17
- import deepmerge from "deepmerge"
18
-
12
+ import { Command } from "commander";
13
+ import chalk from "chalk";
14
+ import jsYAML from "js-yaml";
15
+ import nunjucks from "nunjucks";
16
+ import deepmerge from "deepmerge";
19
17
  /* load my own information */
20
- const my = JSON.parse(await fs.promises.readFile(new URL("./package.json", import.meta.url)))
21
-
18
+ const my = JSON.parse(await fs.promises.readFile(new URL("./package.json", import.meta.url), "utf-8"));
22
19
  /* parse command-line arguments */
23
- const program = new Command()
20
+ const program = new Command();
21
+ const reduceArray = (v, l) => l.concat([v]);
24
22
  program.name("nunjucks")
25
23
  .description("Nunjucks Template Rendering Command-Line Interface")
26
24
  .showHelpAfterError("hint: use option --help for usage information")
27
25
  .option("-h, --help", "show usage help", false)
28
26
  .option("-V, --version", "show program version information", false)
29
27
  .option("-c, --config <config-file>", "load Nunjucks configuration YAML file", "")
30
- .option("-C, --option <key>=<value>", "set Nunjucks configuration option", (v, l) => l.concat([ v ]), [])
31
- .option("-d, --defines <context-file>", "load context definition YAML file", (v, l) => l.concat([ v ]), [])
32
- .option("-D, --define <key>=<value>", "set context definition key/value", (v, l) => l.concat([ v ]), [])
33
- .option("-e, --extension <module-name>", "load Nunjucks JavaScript extension module", (v, l) => l.concat([ v ]), [])
28
+ .option("-C, --option <key>=<value>", "set Nunjucks configuration option", reduceArray, [])
29
+ .option("-d, --defines <context-file>", "load context definition YAML file", reduceArray, [])
30
+ .option("-D, --define <key>=<value>", "set context definition key/value", reduceArray, [])
31
+ .option("-e, --extension <module-name>", "load Nunjucks JavaScript extension module", reduceArray, [])
34
32
  .option("-o, --output <output-file>", "save output file", "-")
35
- .argument("[<input-file>]", "input file")
36
- program.parse(process.argv)
37
- const argv = { ...program.opts(), _: program.args }
38
-
33
+ .argument("[<input-file>]", "input file");
34
+ program.parse(process.argv);
35
+ const argv = {
36
+ ...program.opts(),
37
+ _: program.args
38
+ };
39
39
  /* handle special help request */
40
40
  if (argv.help) {
41
- console.log(program.helpInformation())
42
- console.log("Example:\n $ echo \"Hello, {{ who }}!\" | nunjucks -Dwho=World -\n")
43
- process.exit(0)
41
+ console.log(program.helpInformation());
42
+ console.log("Example:\n $ echo \"Hello, {{ who }}!\" | nunjucks -Dwho=World -\n");
43
+ process.exit(0);
44
44
  }
45
-
46
45
  /* handle special version request */
47
46
  if (argv.version) {
48
- console.log(`${my.name} ${my.version} (Node.js ${process.versions.node}, Nunjucks: ${my.dependencies.nunjucks})`)
49
- console.log(`${my.description}`)
50
- console.log(`Copyright (c) 2019-2025 ${my.author.name} <${my.author.url}>`)
51
- console.log(`Licensed under ${my.license} <http://spdx.org/licenses/${my.license}.html>`)
52
- process.exit(0)
47
+ console.log(`${my.name} ${my.version} (Node.js ${process.versions.node}, Nunjucks: ${my.dependencies.nunjucks})`);
48
+ console.log(`${my.description}`);
49
+ console.log(`Copyright (c) 2019-2025 ${my.author.name} <${my.author.url}>`);
50
+ console.log(`Licensed under ${my.license} <http://spdx.org/licenses/${my.license}.html>`);
51
+ process.exit(0);
53
52
  }
54
-
55
53
  /* read input file */
56
- let input = ""
54
+ let input = "";
57
55
  if (argv._.length > 1) {
58
- console.error(chalk.red("nunjucks: ERROR: invalid number of arguments (zero or one input file expected)"))
59
- process.exit(1)
56
+ console.error(chalk.red("nunjucks: ERROR: invalid number of arguments (zero or one input file expected)"));
57
+ process.exit(1);
60
58
  }
61
- let inputFile = argv._[0] ?? "-"
59
+ let inputFile = argv._[0] ?? "-";
62
60
  if (inputFile === "-") {
63
- inputFile = "<stdin>"
64
- process.stdin.setEncoding("utf-8")
65
- const BUFSIZE = 256
66
- const buf = Buffer.alloc(BUFSIZE)
61
+ inputFile = "<stdin>";
62
+ process.stdin.setEncoding("utf-8");
63
+ const BUFSIZE = 256;
64
+ const buf = Buffer.alloc(BUFSIZE);
67
65
  while (true) {
68
- let bytesRead = 0
66
+ let bytesRead = 0;
69
67
  try {
70
- bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE)
68
+ bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE, null);
71
69
  }
72
70
  catch (ex) {
73
- if (ex.code === "EAGAIN") continue
74
- else if (ex.code === "EOF") break
75
- else throw ex
71
+ if (ex.code === "EAGAIN")
72
+ continue;
73
+ else if (ex.code === "EOF")
74
+ break;
75
+ else
76
+ throw ex;
76
77
  }
77
78
  if (bytesRead === 0)
78
- break
79
- input += buf.toString("utf8", 0, bytesRead)
79
+ break;
80
+ input += buf.toString("utf8", 0, bytesRead);
80
81
  }
81
82
  }
82
83
  else {
83
84
  if (!fs.existsSync(inputFile)) {
84
- console.error(chalk.red(`nunjucks: ERROR: failed to find input file: ${inputFile}`))
85
- process.exit(1)
85
+ console.error(chalk.red(`nunjucks: ERROR: failed to find input file: ${inputFile}`));
86
+ process.exit(1);
86
87
  }
87
- input = fs.readFileSync(inputFile, { encoding: "utf8" })
88
+ input = fs.readFileSync(inputFile, { encoding: "utf8" });
88
89
  }
89
-
90
90
  /* provide context variables for template */
91
- let context = {}
91
+ let context = {};
92
92
  for (const define of argv.defines) {
93
93
  try {
94
- context = deepmerge(context, jsYAML.load(fs.readFileSync(define, { encoding: "utf8" })))
94
+ context = deepmerge(context, jsYAML.load(fs.readFileSync(define, { encoding: "utf8" })));
95
95
  }
96
96
  catch (ex) {
97
- console.error(chalk.red(`nunjucks: ERROR: failed to load context YAML file: ${ex.toString()}`))
98
- process.exit(1)
97
+ console.error(chalk.red(`nunjucks: ERROR: failed to load context YAML file: ${ex.toString()}`));
98
+ process.exit(1);
99
99
  }
100
100
  }
101
-
102
101
  /* expose environment variables to template */
103
- context.env = process.env
104
-
102
+ context.env = process.env;
105
103
  /* add context defines */
106
104
  argv.define.forEach((define) => {
107
- let [ , key, val ] = define.match(/^([^=]+)(?:=(.*))?$/)
105
+ const match = define.match(/^([^=]+)(?:=(.*))?$/);
106
+ if (!match)
107
+ return;
108
+ let [, key, val] = match;
109
+ if (!key)
110
+ return;
108
111
  if (val === undefined)
109
- val = true
110
- context[key] = val
111
- })
112
-
112
+ val = "true";
113
+ context[key] = val;
114
+ });
113
115
  /* determine Nunjucks options */
114
- let options = {}
116
+ let options = {};
115
117
  if (argv.config) {
116
118
  try {
117
- options = jsYAML.load(fs.readFileSync(argv.config, { encoding: "utf8" }))
119
+ options = jsYAML.load(fs.readFileSync(argv.config, { encoding: "utf8" }));
118
120
  }
119
121
  catch (ex) {
120
- console.error(chalk.red(`nunjucks: ERROR: failed to load options YAML file: ${ex.toString()}`))
121
- process.exit(1)
122
+ console.error(chalk.red(`nunjucks: ERROR: failed to load options YAML file: ${ex.toString()}`));
123
+ process.exit(1);
122
124
  }
123
125
  }
124
126
  if (argv.option.length > 0)
125
- options = Object.assign(options, argv.option)
126
- options = Object.assign({}, {
127
- autoescape: false,
127
+ options = Object.assign(options, argv.option);
128
+ options = {
129
+ autoescape: false,
128
130
  throwOnUndefined: false,
129
- trimBlocks: true,
130
- lstripBlocks: true,
131
- watch: false,
132
- noCache: true
133
- }, options)
134
-
131
+ trimBlocks: true,
132
+ lstripBlocks: true,
133
+ watch: false,
134
+ noCache: true,
135
+ ...options
136
+ };
135
137
  /* configure environment */
136
- const env = nunjucks.configure(inputFile, options)
137
-
138
+ const env = nunjucks.configure(inputFile, options);
138
139
  /* load external extension files */
139
140
  for (const extension of argv.extension) {
140
- let modpath = path.resolve(extension)
141
+ let modpath = path.resolve(extension);
141
142
  if (!fs.existsSync(modpath)) {
142
143
  try {
143
- modpath = require.resolve(extension)
144
+ const require = createRequire(import.meta.url);
145
+ modpath = require.resolve(extension);
144
146
  }
145
- catch (ex) {
146
- modpath = null
147
+ catch (_ex) {
148
+ modpath = null;
147
149
  }
148
150
  }
149
151
  if (modpath === null) {
150
- console.error(chalk.red(`nunjucks: ERROR: failed to find extension module: ${extension}`))
151
- process.exit(1)
152
+ console.error(chalk.red(`nunjucks: ERROR: failed to find extension module: ${extension}`));
153
+ process.exit(1);
154
+ }
155
+ /* dynamically import the module */
156
+ let mod;
157
+ try {
158
+ mod = await import(modpath);
159
+ /* handle both default and named exports */
160
+ mod = mod.default ?? mod;
161
+ }
162
+ catch (ex) {
163
+ console.error(chalk.red(`nunjucks: ERROR: failed to load extension module: ${ex.toString()}`));
164
+ process.exit(1);
152
165
  }
153
- const mod = require(modpath)
154
166
  if (!(mod !== null && typeof mod === "function")) {
155
- console.error(chalk.red(`nunjucks: ERROR: failed to call extension file: ${modpath}`))
156
- process.exit(1)
167
+ console.error(chalk.red(`nunjucks: ERROR: failed to call extension file: ${modpath}`));
168
+ process.exit(1);
157
169
  }
158
- mod(env)
170
+ mod(env);
159
171
  }
160
-
161
172
  /* render Nunjucks template */
162
- let output
173
+ let output;
163
174
  try {
164
- output = env.renderString(input, context)
175
+ output = env.renderString(input, context);
165
176
  }
166
177
  catch (ex) {
167
- console.error(chalk.red(`nunjucks: ERROR: failed to render template: ${ex.toString()}`))
168
- process.exit(1)
178
+ console.error(chalk.red(`nunjucks: ERROR: failed to render template: ${ex.toString()}`));
179
+ process.exit(1);
169
180
  }
170
-
171
181
  /* write output */
172
182
  if (argv.output === "-")
173
- process.stdout.write(output)
183
+ process.stdout.write(output);
174
184
  else
175
- fs.writeFileSync(argv.output, output, { encoding: "utf8" })
176
-
185
+ fs.writeFileSync(argv.output, output, { encoding: "utf8" });
177
186
  /* die gracefully */
178
- process.exit(0)
179
-
187
+ process.exit(0);
package/nunjucks.ts ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ ** nunjucks -- Nunjucks Template Rendering Command-Line Interface
4
+ ** Copyright (c) 2019-2025 Dr. Ralf S. Engelschall <http://engelschall.com>
5
+ ** Licensed under MIT <http://spdx.org/licenses/MIT.html>
6
+ */
7
+
8
+ /* built-in requirements */
9
+ import fs from "node:fs"
10
+ import path from "node:path"
11
+ import { createRequire } from "node:module"
12
+
13
+ /* external requirements */
14
+ import { Command } from "commander"
15
+ import chalk from "chalk"
16
+ import jsYAML from "js-yaml"
17
+ import nunjucks from "nunjucks"
18
+ import deepmerge from "deepmerge"
19
+
20
+ /* type definitions */
21
+ type PackageInfo = {
22
+ name: string
23
+ version: string
24
+ description: string
25
+ author: { name: string; email: string; url: string }
26
+ license: string
27
+ dependencies: { nunjucks: string }
28
+ }
29
+ type ContextType = Record<string, any>
30
+ type OptionsType = {
31
+ autoescape?: boolean
32
+ throwOnUndefined?: boolean
33
+ trimBlocks?: boolean
34
+ lstripBlocks?: boolean
35
+ watch?: boolean
36
+ noCache?: boolean
37
+ }
38
+ type CLIOptions = {
39
+ help: boolean
40
+ version: boolean
41
+ config: string
42
+ option: string[]
43
+ defines: string[]
44
+ define: string[]
45
+ extension: string[]
46
+ output: string
47
+ _: string[]
48
+ }
49
+
50
+ /* load my own information */
51
+ const my: PackageInfo = JSON.parse(await fs.promises.readFile(new URL("./package.json", import.meta.url), "utf-8"))
52
+
53
+ /* parse command-line arguments */
54
+ const program = new Command()
55
+ const reduceArray = (v: string, l: string[]) => l.concat([ v ])
56
+ program.name("nunjucks")
57
+ .description("Nunjucks Template Rendering Command-Line Interface")
58
+ .showHelpAfterError("hint: use option --help for usage information")
59
+ .option("-h, --help", "show usage help", false)
60
+ .option("-V, --version", "show program version information", false)
61
+ .option("-c, --config <config-file>", "load Nunjucks configuration YAML file", "")
62
+ .option("-C, --option <key>=<value>", "set Nunjucks configuration option", reduceArray, [])
63
+ .option("-d, --defines <context-file>", "load context definition YAML file", reduceArray, [])
64
+ .option("-D, --define <key>=<value>", "set context definition key/value", reduceArray, [])
65
+ .option("-e, --extension <module-name>", "load Nunjucks JavaScript extension module", reduceArray, [])
66
+ .option("-o, --output <output-file>", "save output file", "-")
67
+ .argument("[<input-file>]", "input file")
68
+ program.parse(process.argv)
69
+ const argv: CLIOptions = {
70
+ ...program.opts(),
71
+ _: program.args
72
+ } as CLIOptions
73
+
74
+ /* handle special help request */
75
+ if (argv.help) {
76
+ console.log(program.helpInformation())
77
+ console.log("Example:\n $ echo \"Hello, {{ who }}!\" | nunjucks -Dwho=World -\n")
78
+ process.exit(0)
79
+ }
80
+
81
+ /* handle special version request */
82
+ if (argv.version) {
83
+ console.log(`${my.name} ${my.version} (Node.js ${process.versions.node}, Nunjucks: ${my.dependencies.nunjucks})`)
84
+ console.log(`${my.description}`)
85
+ console.log(`Copyright (c) 2019-2025 ${my.author.name} <${my.author.url}>`)
86
+ console.log(`Licensed under ${my.license} <http://spdx.org/licenses/${my.license}.html>`)
87
+ process.exit(0)
88
+ }
89
+
90
+ /* read input file */
91
+ let input = ""
92
+ if (argv._.length > 1) {
93
+ console.error(chalk.red("nunjucks: ERROR: invalid number of arguments (zero or one input file expected)"))
94
+ process.exit(1)
95
+ }
96
+ let inputFile: string = argv._[0] ?? "-"
97
+ if (inputFile === "-") {
98
+ inputFile = "<stdin>"
99
+ process.stdin.setEncoding("utf-8")
100
+ const BUFSIZE = 256
101
+ const buf = Buffer.alloc(BUFSIZE)
102
+ while (true) {
103
+ let bytesRead = 0
104
+ try {
105
+ bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE, null)
106
+ }
107
+ catch (ex: any) {
108
+ if (ex.code === "EAGAIN") continue
109
+ else if (ex.code === "EOF") break
110
+ else throw ex
111
+ }
112
+ if (bytesRead === 0)
113
+ break
114
+ input += buf.toString("utf8", 0, bytesRead)
115
+ }
116
+ }
117
+ else {
118
+ if (!fs.existsSync(inputFile)) {
119
+ console.error(chalk.red(`nunjucks: ERROR: failed to find input file: ${inputFile}`))
120
+ process.exit(1)
121
+ }
122
+ input = fs.readFileSync(inputFile, { encoding: "utf8" })
123
+ }
124
+
125
+ /* provide context variables for template */
126
+ let context: ContextType = {}
127
+ for (const define of argv.defines) {
128
+ try {
129
+ context = deepmerge(context, jsYAML.load(fs.readFileSync(define, { encoding: "utf8" })) as ContextType)
130
+ }
131
+ catch (ex: any) {
132
+ console.error(chalk.red(`nunjucks: ERROR: failed to load context YAML file: ${ex.toString()}`))
133
+ process.exit(1)
134
+ }
135
+ }
136
+
137
+ /* expose environment variables to template */
138
+ context.env = process.env
139
+
140
+ /* add context defines */
141
+ argv.define.forEach((define: string) => {
142
+ const match = define.match(/^([^=]+)(?:=(.*))?$/)
143
+ if (!match)
144
+ return
145
+ let [ , key, val ]: (string | undefined)[] = match
146
+ if (!key)
147
+ return
148
+ if (val === undefined)
149
+ val = "true"
150
+ context[key] = val
151
+ })
152
+
153
+ /* determine Nunjucks options */
154
+ let options: OptionsType = {}
155
+ if (argv.config) {
156
+ try {
157
+ options = jsYAML.load(fs.readFileSync(argv.config, { encoding: "utf8" })) as OptionsType
158
+ }
159
+ catch (ex: any) {
160
+ console.error(chalk.red(`nunjucks: ERROR: failed to load options YAML file: ${ex.toString()}`))
161
+ process.exit(1)
162
+ }
163
+ }
164
+ if (argv.option.length > 0)
165
+ options = Object.assign(options, argv.option)
166
+ options = {
167
+ autoescape: false,
168
+ throwOnUndefined: false,
169
+ trimBlocks: true,
170
+ lstripBlocks: true,
171
+ watch: false,
172
+ noCache: true,
173
+ ...options
174
+ }
175
+
176
+ /* configure environment */
177
+ const env = nunjucks.configure(inputFile, options)
178
+
179
+ /* load external extension files */
180
+ for (const extension of argv.extension) {
181
+ let modpath: string | null = path.resolve(extension)
182
+ if (!fs.existsSync(modpath)) {
183
+ try {
184
+ const require = createRequire(import.meta.url)
185
+ modpath = require.resolve(extension)
186
+ }
187
+ catch (_ex) {
188
+ modpath = null
189
+ }
190
+ }
191
+ if (modpath === null) {
192
+ console.error(chalk.red(`nunjucks: ERROR: failed to find extension module: ${extension}`))
193
+ process.exit(1)
194
+ }
195
+
196
+ /* dynamically import the module */
197
+ let mod: any
198
+ try {
199
+ mod = await import(modpath)
200
+
201
+ /* handle both default and named exports */
202
+ mod = mod.default ?? mod
203
+ }
204
+ catch (ex: any) {
205
+ console.error(chalk.red(`nunjucks: ERROR: failed to load extension module: ${ex.toString()}`))
206
+ process.exit(1)
207
+ }
208
+ if (!(mod !== null && typeof mod === "function")) {
209
+ console.error(chalk.red(`nunjucks: ERROR: failed to call extension file: ${modpath}`))
210
+ process.exit(1)
211
+ }
212
+ mod(env)
213
+ }
214
+
215
+ /* render Nunjucks template */
216
+ let output: string
217
+ try {
218
+ output = env.renderString(input, context)
219
+ }
220
+ catch (ex: any) {
221
+ console.error(chalk.red(`nunjucks: ERROR: failed to render template: ${ex.toString()}`))
222
+ process.exit(1)
223
+ }
224
+
225
+ /* write output */
226
+ if (argv.output === "-")
227
+ process.stdout.write(output)
228
+ else
229
+ fs.writeFileSync(argv.output, output, { encoding: "utf8" })
230
+
231
+ /* die gracefully */
232
+ process.exit(0)
233
+
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@rse/nunjucks-cli",
3
3
  "publishConfig": { "access": "public" },
4
- "version": "1.6.0",
5
- "stdver": "1.6.0-GA",
4
+ "version": "2.0.1",
5
+ "stdver": "2.0.0-GA",
6
6
  "description": "Nunjucks Template Rendering Command-Line Interface",
7
7
  "author": {
8
8
  "name": "Dr. Ralf S. Engelschall",
@@ -22,28 +22,39 @@
22
22
  },
23
23
  "man": "nunjucks.1",
24
24
  "type": "module",
25
+ "engines": {
26
+ "node": ">=22.18.0"
27
+ },
25
28
  "dependencies": {
26
29
  "nunjucks": "3.2.4",
27
- "chalk": "5.4.1",
28
- "commander": "14.0.0",
29
- "js-yaml": "4.1.0",
30
+ "chalk": "5.6.2",
31
+ "commander": "14.0.2",
32
+ "js-yaml": "4.1.1",
30
33
  "deepmerge": "4.3.1"
31
34
  },
32
35
  "devDependencies": {
33
- "eslint": "9.28.0",
34
- "@eslint/js": "9.28.0",
35
- "neostandard": "0.12.1",
36
+ "eslint": "9.39.2",
37
+ "@eslint/js": "9.39.2",
38
+ "neostandard": "0.12.2",
36
39
  "eslint-plugin-promise": "7.2.1",
37
- "eslint-plugin-import": "2.31.0",
38
- "eslint-plugin-n": "17.19.0",
39
- "globals": "16.2.0",
40
+ "eslint-plugin-import": "2.32.0",
41
+ "eslint-plugin-n": "17.23.1",
42
+ "globals": "16.5.0",
40
43
  "remark-cli": "12.0.1",
41
44
  "remark": "15.0.1",
42
- "remark-man": "9.0.0"
45
+ "remark-man": "9.0.0",
46
+ "typescript": "5.9.3",
47
+ "typescript-eslint": "8.50.0",
48
+ "@rse/stx": "1.1.3",
49
+ "shx": "0.4.0",
50
+ "@yao-pkg/pkg": "6.11.0",
51
+
52
+ "@types/node": "25.0.3",
53
+ "@types/js-yaml": "4.0.9",
54
+ "@types/nunjucks": "3.2.6"
43
55
  },
44
56
  "scripts": {
45
- "lint": "eslint --config eslint.mjs nunjucks.js",
46
- "man": "remark --quiet --use remark-man --output nunjucks.1 nunjucks.md",
47
- "test": "echo 'Hello, {{who}}!' | node nunjucks.js -D who=world -"
57
+ "start": "stx -v4 -c stx.conf",
58
+ "test": "npm start test"
48
59
  }
49
60
  }
package/stx.conf ADDED
@@ -0,0 +1,95 @@
1
+ ##
2
+ ## nunjucks -- Nunjucks Template Rendering Command-Line Interface
3
+ ## Copyright (c) 2019-2025 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ## Licensed under MIT <https://spdx.org/licenses/MIT>
5
+ ##
6
+
7
+ # static code analysis
8
+ lint
9
+ eslint --config eslint.mjs nunjucks.ts && \
10
+ tsc --noEmit --project tsconfig.json
11
+
12
+ # build JavaScript from TypeScript
13
+ build
14
+ tsc --project tsconfig.json
15
+
16
+ # build manual page
17
+ man
18
+ remark --quiet --use remark-man --output nunjucks.1 nunjucks.md
19
+
20
+ # execute simple smole test
21
+ test : build
22
+ echo 'Hello, {{who}}!' | node nunjucks.js -D who=world -
23
+
24
+ # build all-in-one packages
25
+ build:pkg [hostname=en4.*]
26
+
27
+ # package distribution archives
28
+ package : build build:pkg [hostname=en4.*]
29
+ VERSION=`sed -n '/"version":/ s/.*: *"\(.*\)".*/\1/p' package.json` && \
30
+ targets="node24-linux-x64,node24-linux-arm64" && \
31
+ targets="$targets,node24-win-x64,node24-win-arm64" && \
32
+ targets="$targets,node24-macos-x64,node24-macos-arm64" && \
33
+ sed -e 's;@rse/nunjucks-cli;nunjucks;' <package.json >nunjucks.json && \
34
+ pkg --sea --public -c nunjucks.json -t "$targets" nunjucks.js && \
35
+ rm -f nunjucks.json && \
36
+ shx mv nunjucks-linux-x64 nunjucks-lnx-x64 && \
37
+ shx mv nunjucks-linux-arm64 nunjucks-lnx-a64 && \
38
+ shx mv nunjucks-win-x64.exe nunjucks-win-x64.exe && \
39
+ shx mv nunjucks-win-arm64.exe nunjucks-win-a64.exe && \
40
+ shx mv nunjucks-macos-x64 nunjucks-mac-x64 && \
41
+ shx mv nunjucks-macos-arm64 nunjucks-mac-a64
42
+ mkdir -p nunjucks-$VERSION/ && \
43
+ mkdir -p nunjucks-$VERSION-win-x64/ && \
44
+ mkdir -p nunjucks-$VERSION-win-a64/ && \
45
+ mkdir -p nunjucks-$VERSION-mac-x64/ && \
46
+ mkdir -p nunjucks-$VERSION-mac-a64/ && \
47
+ mkdir -p nunjucks-$VERSION-lnx-x64/ && \
48
+ mkdir -p nunjucks-$VERSION-lnx-a64/ && \
49
+ cp -p nunjucks.js nunjucks-$VERSION/nunjucks.js && \
50
+ cp -p nunjucks-win-x64.exe nunjucks-$VERSION-win-x64/nunjucks.exe && \
51
+ cp -p nunjucks-win-a64.exe nunjucks-$VERSION-win-a64/nunjucks.exe && \
52
+ cp -p nunjucks-mac-x64 nunjucks-$VERSION-mac-x64/nunjucks && \
53
+ cp -p nunjucks-mac-a64 nunjucks-$VERSION-mac-a64/nunjucks && \
54
+ cp -p nunjucks-lnx-x64 nunjucks-$VERSION-lnx-x64/nunjucks && \
55
+ cp -p nunjucks-lnx-a64 nunjucks-$VERSION-lnx-a64/nunjucks && \
56
+ cp -p nunjucks.1 nunjucks-$VERSION/nunjucks.man && \
57
+ cp -p nunjucks.1 nunjucks-$VERSION-win-x64/nunjucks.man && \
58
+ cp -p nunjucks.1 nunjucks-$VERSION-win-a64/nunjucks.man && \
59
+ cp -p nunjucks.1 nunjucks-$VERSION-mac-x64/nunjucks.man && \
60
+ cp -p nunjucks.1 nunjucks-$VERSION-mac-a64/nunjucks.man && \
61
+ cp -p nunjucks.1 nunjucks-$VERSION-lnx-x64/nunjucks.man && \
62
+ cp -p nunjucks.1 nunjucks-$VERSION-lnx-a64/nunjucks.man && \
63
+ zip -9 -r nunjucks-$VERSION.zip nunjucks-$VERSION/ && \
64
+ zip -9 -r nunjucks-$VERSION-win-x64.zip nunjucks-$VERSION-win-x64/ && \
65
+ zip -9 -r nunjucks-$VERSION-win-a64.zip nunjucks-$VERSION-win-a64/ && \
66
+ zip -9 -r nunjucks-$VERSION-mac-x64.zip nunjucks-$VERSION-mac-x64/ && \
67
+ zip -9 -r nunjucks-$VERSION-mac-a64.zip nunjucks-$VERSION-mac-a64/ && \
68
+ zip -9 -r nunjucks-$VERSION-lnx-x64.zip nunjucks-$VERSION-lnx-x64/ && \
69
+ zip -9 -r nunjucks-$VERSION-lnx-a64.zip nunjucks-$VERSION-lnx-a64/
70
+
71
+ # publish distribution archives
72
+ publish : package [hostname=en4.*]
73
+ VERSION=`sed -n '/"version":/ s/.*: *"\(.*\)".*/\1/p' package.json` && \
74
+ V=`echo "$VERSION" | sed -e 's;\.;\\.;g'` && \
75
+ sed -n -e "/^${V} /, /^[0-9]\\./ { /^${V} /p; /^[0-9]\\./!p; }" <CHANGELOG.md >.notes.md && \
76
+ gh release create --title "Nunjucks $VERSION" --notes-file .notes.md --verify-tag $VERSION \
77
+ nunjucks-$VERSION.zip \
78
+ nunjucks-$VERSION-win-x64.zip \
79
+ nunjucks-$VERSION-win-a64.zip \
80
+ nunjucks-$VERSION-mac-x64.zip \
81
+ nunjucks-$VERSION-mac-a64.zip \
82
+ nunjucks-$VERSION-lnx-x64.zip \
83
+ nunjucks-$VERSION-lnx-a64.zip && \
84
+ rm -f .notes.md
85
+
86
+ # remove all generated artifacts
87
+ clean
88
+ shx rm -f nunjucks.js && \
89
+ shx rm -rf nunjucks-*.zip
90
+
91
+ # remove all generated artifacts
92
+ distclean: clean
93
+ shx rm -f package-lock.json && \
94
+ shx rm -rf node_modules
95
+
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "node20",
5
+ "lib": [ "ES2024" ],
6
+ "moduleResolution": "node16",
7
+ "resolveJsonModule": true,
8
+ "declaration": false,
9
+ "allowSyntheticDefaultImports": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "outDir": ".",
15
+ "types": [ "node" ]
16
+ },
17
+ "include": [
18
+ "nunjucks.ts"
19
+ ],
20
+ "exclude": [
21
+ "node_modules"
22
+ ]
23
+ }