mikel-cli 0.25.0

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.
Files changed (3) hide show
  1. package/README.md +130 -0
  2. package/index.js +248 -0
  3. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # mikel-cli
2
+
3
+ ![npm version](https://badgen.net/npm/v/mikel-cli?labelColor=1d2734&color=21bf81)
4
+ ![license](https://badgen.net/github/license/jmjuanes/mikel?labelColor=1d2734&color=21bf81)
5
+
6
+ A command-line interface for the [Mikel](https://github.com/jmjuanes/mikel) templating engine. This CLI tool allows you to render Mikel templates from the command line with support for data files, partials, helpers, and functions.
7
+
8
+ ## Installation
9
+
10
+ Install the tool using **npm** or **yarn**:
11
+
12
+ ```bash
13
+ # Using npm
14
+ $ npm install mikel-cli mikel
15
+
16
+ # Using yarn
17
+ $ yarn add mikel-cli mikel
18
+ ```
19
+
20
+ **Note:** You need to install both `mikel-cli` and `mikel` packages. The `mikel` package is a peer dependency that provides the core templating functionality.
21
+
22
+ ## Usage
23
+
24
+ ### Basic Usage
25
+
26
+ ```bash
27
+ $ mikel <template> [options]
28
+ ```
29
+
30
+ ### Options
31
+
32
+ | Option | Short | Description |
33
+ |--------|-------|-------------|
34
+ | `--help` | `-h` | Display help information |
35
+ | `--data <file>` | `-D` | Path to JSON data file |
36
+ | `--output <file>` | `-o` | Output file path |
37
+ | `--partial <file>` | `-P` | Register a partial template (supports glob patterns, can be used multiple times) |
38
+ | `--helper <file>` | `-H` | Register helper functions from a JavaScript module (supports glob patterns, can be used multiple times) |
39
+ | `--function <file>` | `-F` | Register functions from a JavaScript module (supports glob patterns, can be used multiple times) |
40
+
41
+ ### Examples
42
+
43
+ #### Simple Template Rendering
44
+
45
+ Render a template and output to console:
46
+
47
+ ```bash
48
+ mikel template.html
49
+ ```
50
+
51
+ #### Using Data File
52
+
53
+ Render a template with data from a JSON file:
54
+
55
+ ```bash
56
+ mikel template.html --data data.json
57
+ ```
58
+
59
+ #### Output to File
60
+
61
+ Render template and save to an output file:
62
+
63
+ ```bash
64
+ mikel template.html --data data.json --output dist/index.html
65
+ ```
66
+
67
+ #### Using Partials
68
+
69
+ Register partial templates for reusable components:
70
+
71
+ ```bash
72
+ mikel template.html --data data.json --partial header.html --partial footer.html --output dist/index.html
73
+ ```
74
+
75
+ #### Using Helpers and Functions
76
+
77
+ Register custom helpers and functions from JavaScript modules:
78
+
79
+ ```bash
80
+ mikel template.html --data data.json --helper helpers.js --function utils.js --output dist/index.html
81
+ ```
82
+
83
+ #### Using Glob Patterns
84
+
85
+ Load multiple files using glob patterns:
86
+
87
+ ```bash
88
+ # Load all HTML partials from a directory
89
+ mikel template.html --partial 'partials/*.html' --output dist/index.html
90
+
91
+ # Load all JavaScript helpers from a directory
92
+ mikel template.html --helper 'helpers/*.js' --output dist/index.html
93
+
94
+ # Load partials from subdirectories (recursive)
95
+ mikel template.html --partial 'components/**/*.html' --output dist/index.html
96
+
97
+ # Mix exact files and glob patterns
98
+ mikel template.html --partial header.html --partial 'components/*.html' --output dist/index.html
99
+ ```
100
+
101
+ #### Complex Example
102
+
103
+ A complete example combining all features:
104
+
105
+ ```bash
106
+ mikel src/template.html \
107
+ --data src/data.json \
108
+ --partial 'src/partials/*.html' \
109
+ --partial 'src/components/**/*.html' \
110
+ --helper 'src/helpers/*.js' \
111
+ --function 'src/utils/*.js' \
112
+ --output dist/index.html
113
+ ```
114
+
115
+ ### Glob Pattern Support
116
+
117
+ The `--partial`, `--helper`, and `--function` options support glob patterns for loading multiple files at once:
118
+
119
+ | Pattern | Description | Example |
120
+ |---------|-------------|---------|
121
+ | `*.ext` | All files with extension in current directory | `*.html`, `*.js` |
122
+ | `dir/*.ext` | All files with extension in specific directory | `partials/*.html` |
123
+ | `dir/**/*.ext` | All files with extension in directory and subdirectories | `components/**/*.html` |
124
+ | `?` | Single character wildcard | `file?.html` |
125
+
126
+ **Note:** Glob patterns should be quoted to prevent shell expansion.
127
+
128
+ ## License
129
+
130
+ Licensed under the [MIT License](../../LICENSE).
package/index.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import mikel from "mikel";
8
+
9
+ // @description get the files that matches the provided patterns
10
+ // this is a utility function to expand glob patterns to actual file paths.
11
+ // it uses Node.js 24+ built-in fs.glob to handle glob patterns.
12
+ const expandGlobPatterns = async (patterns = []) => {
13
+ const files = [];
14
+ for (let i = 0; i < patterns.length; i++) {
15
+ const pattern = patterns[i];
16
+ if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
17
+ try {
18
+ // use Node.js 24+ built-in fs.glob
19
+ // https://nodejs.org/api/fs.html#fspromisesglobpattern-options
20
+ for await (const file of fs.glob(pattern, { cwd: process.cwd() })) {
21
+ files.push(file);
22
+ }
23
+ } catch (error) {
24
+ files.push(pattern);
25
+ }
26
+ } else {
27
+ files.push(pattern);
28
+ }
29
+ }
30
+ // remove duplicates and resolve to absolute paths
31
+ return Array.from(new Set(files)).map(file => {
32
+ return path.resolve(process.cwd(), file);
33
+ });
34
+ };
35
+
36
+ // @description load JSON data from the provided path
37
+ const loadData = async (file = null) => {
38
+ if (!file) {
39
+ return {};
40
+ }
41
+ // build the full data file path and check if exists
42
+ const dataPath = path.resolve(process.cwd(), file);
43
+ if (!existsSync(dataPath)) {
44
+ throw new Error(`Data file '${dataPath}' was not found.`);
45
+ }
46
+ // read the file and parse it as JSON
47
+ try {
48
+ const content = await fs.readFile(dataPath, "utf8");
49
+ return JSON.parse(content);
50
+ } catch (error) {
51
+ if (error instanceof SyntaxError) {
52
+ throw new Error(`Invalid JSON in data file '${dataPath}': ${error.message}`);
53
+ }
54
+ throw new Error(`Failed to read data file '${dataPath}': ${error.message}`);
55
+ }
56
+ };
57
+
58
+ // @description load partials
59
+ const loadPartials = async (patterns = []) => {
60
+ const partials = {}; // output partials object
61
+ const files = await expandGlobPatterns(patterns);
62
+
63
+ // load all files
64
+ for (let i = 0; i < files.length; i++) {
65
+ const file = files[i]; // path.resolve(process.cwd(), uniqueFiles[i]);
66
+ if (!existsSync(file)) {
67
+ throw new Error(`Partial file '${file}' was not found.`);
68
+ }
69
+
70
+ // check if it's a file (not a directory)
71
+ const stats = await fs.stat(file);
72
+ if (!stats.isFile()) {
73
+ continue; // skip directories
74
+ }
75
+
76
+ // load the file and save it as a partial
77
+ try {
78
+ partials[path.basename(file)] = await fs.readFile(file, "utf8");
79
+ } catch (error) {
80
+ throw new Error(`Failed to read partial file '${file}': ${error.message}`);
81
+ }
82
+ }
83
+
84
+ return partials;
85
+ };
86
+
87
+ // @description load javascript modules
88
+ const loadModules = async (patterns = []) => {
89
+ const result = {};
90
+ const files = await expandGlobPatterns(patterns);
91
+
92
+ for (let i = 0; i < files.length; i++) {
93
+ const file = files[i]; // path.resolve(process.cwd(), uniqueFiles[i]);
94
+ if (!existsSync(file)) {
95
+ throw new Error(`Module '${file}' was not found.`);
96
+ }
97
+
98
+ // Check if it's a file (not a directory)
99
+ const stats = await fs.stat(file);
100
+ if (!stats.isFile()) {
101
+ continue; // Skip directories
102
+ }
103
+
104
+ const extension = path.extname(file);
105
+ if (extension !== ".js" && extension !== ".mjs") {
106
+ throw new Error(`Module '${file}' is not supported. Only ESM JavaScript (.js or .mjs) files are supported.`);
107
+ }
108
+ // import the module
109
+ try {
110
+ const content = (await import(file)) || {};
111
+ if (typeof content === "object" && !!content) {
112
+ Object.assign(result, content);
113
+ }
114
+ } catch (error) {
115
+ throw new Error(`Failed to import module '${file}': ${error.message}`);
116
+ }
117
+ }
118
+ return result;
119
+ };
120
+
121
+ // print the help of the tool
122
+ const printHelp = () => {
123
+ console.log("Usage: ");
124
+ console.log(" mikel --help");
125
+ console.log(" mikel <template> [...options]");
126
+ console.log("");
127
+ console.log("Options:");
128
+ console.log(" -h, --help Prints the usage information");
129
+ console.log(" -P, --partial <file> Register a partial (supports glob patterns, can be used multiple times)");
130
+ console.log(" -H, --helper <file> Register a helper (supports glob patterns, can be used multiple times)");
131
+ console.log(" -F, --function <file> Register a function (supports glob patterns, can be used multiple times)");
132
+ console.log(" -D, --data <file> Path to the data file to use (JSON)");
133
+ console.log(" -o, --output <file> Output file");
134
+ console.log("");
135
+ console.log("Examples:");
136
+ console.log(" mikel template.html --data data.json --output www/index.html");
137
+ console.log(" mikel template.html --data data.json --partial header.html --partial footer.html --output www/index.html");
138
+ console.log(" mikel template.html --helper helpers.js --function utils.js --output dist/index.html");
139
+ console.log(" mikel template.html --partial 'partials/*.html' --helper 'helpers/*.js' --output dist/index.html");
140
+ console.log(" mikel template.html --partial 'components/**/*.html' --output dist/index.html");
141
+ console.log("");
142
+ process.exit(0);
143
+ };
144
+
145
+ // @description main function
146
+ const main = async (input = "", options = {}) => {
147
+ // check to print help
148
+ if (options.help) {
149
+ return printHelp();
150
+ }
151
+
152
+ // make sure that input file exists
153
+ if (!input) {
154
+ throw new Error(`No input template file provided.`);
155
+ }
156
+ const inputPath = path.resolve(process.cwd(), input);
157
+ if (!existsSync(inputPath)) {
158
+ throw new Error(`Template file '${inputPath}' was not found.`);
159
+ }
160
+
161
+ let template;
162
+ try {
163
+ template = await fs.readFile(inputPath, "utf8");
164
+ } catch (error) {
165
+ throw new Error(`Failed to read template file '${inputPath}': ${error.message}`);
166
+ }
167
+
168
+ // load additional data
169
+ const data = await loadData(options.data);
170
+ const partials = await loadPartials(options.partial);
171
+ const helpers = await loadModules(options.helper);
172
+ const functions = await loadModules(options.function);
173
+
174
+ // compile the template
175
+ let result;
176
+ try {
177
+ result = mikel(template, data || {}, { helpers, functions, partials });
178
+ } catch (error) {
179
+ throw new Error(`Template compilation failed: ${error.message}`);
180
+ }
181
+
182
+ // check if output argument has been provided to write the result to a file
183
+ // this will also create any intermediary directory that does not exist
184
+ if (options.output) {
185
+ const outputPath = path.resolve(process.cwd(), options.output);
186
+ const outputDirectory = path.dirname(outputPath);
187
+ // make sure that any directory containing the output file exists
188
+ if (!existsSync(outputDirectory)) {
189
+ try {
190
+ await fs.mkdir(outputDirectory, { recursive: true });
191
+ } catch (error) {
192
+ throw new Error(`Failed to create output directory '${outputDirectory}': ${error.message}`);
193
+ }
194
+ }
195
+ try {
196
+ await fs.writeFile(outputPath, result, "utf8");
197
+ console.error(`✓ Template rendered successfully to '${outputPath}'`);
198
+ } catch (error) {
199
+ throw new Error(`Failed to write output file '${outputPath}': ${error.message}`);
200
+ }
201
+ process.exit(0);
202
+ }
203
+ // if no output file has been provided, print the result to console
204
+ else {
205
+ process.stdout.write(result);
206
+ process.exit(0);
207
+ }
208
+ };
209
+
210
+ // process arguments
211
+ const { positionals, values } = parseArgs({
212
+ options: {
213
+ data: {
214
+ type: "string",
215
+ short: "D",
216
+ },
217
+ output: {
218
+ type: "string",
219
+ short: "o",
220
+ },
221
+ helper: {
222
+ type: "string",
223
+ short: "H",
224
+ multiple: true,
225
+ },
226
+ function: {
227
+ type: "string",
228
+ short: "F",
229
+ multiple: true,
230
+ },
231
+ partial: {
232
+ type: "string",
233
+ short: "P",
234
+ multiple: true,
235
+ },
236
+ help: {
237
+ type: "boolean",
238
+ short: "h",
239
+ },
240
+ },
241
+ allowPositionals: true,
242
+ });
243
+
244
+ // run main script
245
+ main(positionals[0], values).catch(error => {
246
+ console.error(`\n❌ Error: ${error.message}\n`);
247
+ process.exit(1);
248
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "mikel-cli",
3
+ "description": "The cli tool for mikel templating.",
4
+ "version": "0.25.0",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Josemi Juanes",
9
+ "email": "hello@josemi.xyz"
10
+ },
11
+ "repository": "https://github.com/jmjuanes/mikel",
12
+ "bugs": "https://github.com/jmjuanes/mikel/issues",
13
+ "engines": {
14
+ "node": ">=24.0.0"
15
+ },
16
+ "exports": {
17
+ ".": "./index.js",
18
+ "./index.js": "./index.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "bin": {
22
+ "mikel": "index.js"
23
+ },
24
+ "peerDependencies": {
25
+ "mikel": "^0.25.0"
26
+ },
27
+ "files": [
28
+ "README.md",
29
+ "index.js"
30
+ ],
31
+ "keywords": [
32
+ "mikel",
33
+ "cli",
34
+ "templating"
35
+ ]
36
+ }