mikel-cli 0.33.1 → 0.34.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.
- package/README.md +117 -15
- package/cli.js +306 -0
- package/index.js +11 -233
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
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
|
|
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, functions, and plugins.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -31,6 +31,7 @@ $ mikel <template> [options]
|
|
|
31
31
|
|
|
32
32
|
| Option | Short | Description |
|
|
33
33
|
|--------|-------|-------------|
|
|
34
|
+
| `--config <file>` | `-c` | Path to configuration file |
|
|
34
35
|
| `--help` | `-h` | Display help information |
|
|
35
36
|
| `--data <file>` | `-D` | Path to JSON data file |
|
|
36
37
|
| `--output <file>` | `-o` | Output file path |
|
|
@@ -99,20 +100,6 @@ mikel template.html --partial 'components/**/*.html' --output dist/index.html
|
|
|
99
100
|
mikel template.html --partial header.html --partial 'components/*.html' --output dist/index.html
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
#### Complex Example
|
|
103
|
-
|
|
104
|
-
A complete example combining all features:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
mikel src/template.html \
|
|
108
|
-
--data src/data.json \
|
|
109
|
-
--partial 'src/partials/*.html' \
|
|
110
|
-
--partial 'src/components/**/*.html' \
|
|
111
|
-
--helper 'src/helpers/*.js' \
|
|
112
|
-
--function 'src/utils/*.js' \
|
|
113
|
-
--output dist/index.html
|
|
114
|
-
```
|
|
115
|
-
|
|
116
103
|
### Glob Pattern Support
|
|
117
104
|
|
|
118
105
|
The `--partial`, `--helper`, and `--function` options support glob patterns for loading multiple files at once:
|
|
@@ -126,6 +113,121 @@ The `--partial`, `--helper`, and `--function` options support glob patterns for
|
|
|
126
113
|
|
|
127
114
|
**Note:** Glob patterns should be quoted to prevent shell expansion.
|
|
128
115
|
|
|
116
|
+
## Configuration File
|
|
117
|
+
|
|
118
|
+
For more complex use cases — multiple input files, output renaming, or plugins — you can use a configuration file instead of CLI arguments. Create a `mikel.config.js` file in your project root:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
export default {
|
|
122
|
+
input: "src/**/*.mustache",
|
|
123
|
+
output: {
|
|
124
|
+
dir: "dist/",
|
|
125
|
+
rename: {
|
|
126
|
+
"^src/(.+)\\.mustache$": "$1.html",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
data: "./data/site.json",
|
|
130
|
+
plugins: [],
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Then run mikel pointing to the config file:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
mikel --config mikel.config.js
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
CLI arguments take precedence over the configuration file when both are provided. However, when using a configuration file, `input` can be a glob or an array of globs — something not possible via CLI arguments alone.
|
|
141
|
+
|
|
142
|
+
### Configuration Fields
|
|
143
|
+
|
|
144
|
+
#### `input`
|
|
145
|
+
|
|
146
|
+
The template file(s) to process. Accepts a string (file path or glob) or an array of strings:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// single file
|
|
150
|
+
input: "src/index.mustache"
|
|
151
|
+
|
|
152
|
+
// array of files and globs
|
|
153
|
+
input: ["src/index.mustache", "src/pages/**/*.mustache"]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `output`
|
|
157
|
+
|
|
158
|
+
Where to write the rendered files. Accepts a string (directory path) or an object:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
// simple directory
|
|
162
|
+
output: "dist/"
|
|
163
|
+
|
|
164
|
+
// with rename rules
|
|
165
|
+
output: {
|
|
166
|
+
dir: "dist/",
|
|
167
|
+
rename: {
|
|
168
|
+
"^src/(.+)\\.mustache$": "$1.html",
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `rename` field works like Jest's `moduleNameMapper` — keys are regular expressions and values are replacement strings. The first matching pattern wins. If no pattern matches, the basename of the input file is used as the output filename.
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
// src/docs/guide/index.mustache → dist/docs/guide/index.html
|
|
177
|
+
rename: {
|
|
178
|
+
"^src/(.+)\\.mustache$": "$1.html",
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `data`
|
|
183
|
+
|
|
184
|
+
Data to pass to the templates. Accepts a path to a JSON file or a plain object:
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
// path to JSON file
|
|
188
|
+
data: "./data/site.json"
|
|
189
|
+
|
|
190
|
+
// inline object
|
|
191
|
+
data: {
|
|
192
|
+
site: {
|
|
193
|
+
title: "My Site",
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `plugins`
|
|
199
|
+
|
|
200
|
+
An array of Mikel plugins to load. See the [Plugins](#plugins) section for details.
|
|
201
|
+
|
|
202
|
+
## Plugins
|
|
203
|
+
|
|
204
|
+
Plugins extend Mikel's functionality by registering additional helpers, functions, or partials. They can be loaded both via the `--plugin` CLI flag and the `plugins` configuration field.
|
|
205
|
+
|
|
206
|
+
### Loading Plugins via CLI
|
|
207
|
+
|
|
208
|
+
Use the `--plugin` flag to load a plugin from a JavaScript module:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
mikel template.html --plugin mikel-markdown --output dist/index.html
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Loading Plugins via Configuration
|
|
215
|
+
|
|
216
|
+
Use the `plugins` field in your configuration file. Each entry can be a module name (string) or a tuple of `[moduleName, options]` when the plugin requires configuration:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
export default {
|
|
220
|
+
plugins: [
|
|
221
|
+
// plugin without options
|
|
222
|
+
"mikel-frontmatter",
|
|
223
|
+
// plugin with options
|
|
224
|
+
["mikel-markdown", {
|
|
225
|
+
classNames: { ... },
|
|
226
|
+
}],
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
129
231
|
## License
|
|
130
232
|
|
|
131
233
|
Licensed under the [MIT License](../../LICENSE).
|
package/cli.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
import { expandGlobPatterns, applyRename } from "./index.js";
|
|
9
|
+
|
|
10
|
+
// load configuration file
|
|
11
|
+
const loadConfiguration = async (configurationFile) => {
|
|
12
|
+
if (!configurationFile) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
const configurationPath = path.resolve(process.cwd(), configurationFile);
|
|
16
|
+
if (!existsSync(configurationPath)) {
|
|
17
|
+
throw new Error(`Configuration file '${configurationPath}' was not found.`);
|
|
18
|
+
}
|
|
19
|
+
// check the extension of the file
|
|
20
|
+
const configurationExtension = path.extname(configurationFile);
|
|
21
|
+
if (configurationExtension === ".js") {
|
|
22
|
+
return await import(configurationPath);
|
|
23
|
+
}
|
|
24
|
+
else if (configurationExtension === ".json") {
|
|
25
|
+
const content = await fs.readFile(configurationPath, "utf8");
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw new Error(`Unknown extension for configuration file '${configurationFile}'`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// @description loading input files
|
|
34
|
+
const loadInput = async (inputFiles) => {
|
|
35
|
+
if (!inputFiles || inputFiles?.length === 0) {
|
|
36
|
+
throw new Error(`No input templates provided.`);
|
|
37
|
+
}
|
|
38
|
+
// expand glob patterns
|
|
39
|
+
return expandGlobPatterns([inputFiles].flat());
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// @description resolve output
|
|
43
|
+
const resolveOutput = (inputFile, outputFile, outputConfig) => {
|
|
44
|
+
// 1. output file is provided via cli arguments
|
|
45
|
+
if (outputFile) {
|
|
46
|
+
return path.resolve(process.cwd(), outputFile);
|
|
47
|
+
}
|
|
48
|
+
// 2. output configuration is provided and it is a string
|
|
49
|
+
else if (outputConfig && typeof outputConfig === "string") {
|
|
50
|
+
return path.resolve(process.cwd(), path.join(outputConfig, inputFile));
|
|
51
|
+
}
|
|
52
|
+
// 3. output configuration is provided and it is an object
|
|
53
|
+
else if (outputConfig && typeof outputConfig === "object") {
|
|
54
|
+
const renamedOutputFile = applyRename(inputFile, outputConfig?.rename || {});
|
|
55
|
+
return path.resolve(process.cwd(), path.join(outputConfig?.dir || ".", renamedOutputFile));
|
|
56
|
+
}
|
|
57
|
+
// 4. other case???
|
|
58
|
+
throw new Error(`Unknown error resolving output for template '${inputFile}'`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// @description load JSON data from the provided path
|
|
62
|
+
const loadData = async (fileOrObject = null) => {
|
|
63
|
+
if (!fileOrObject) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
// 1. check for object containing data (from config.data)
|
|
67
|
+
if (typeof fileOrObject === "object") {
|
|
68
|
+
return fileOrObject;
|
|
69
|
+
}
|
|
70
|
+
// 2. build the full data file path and check if exists
|
|
71
|
+
const dataPath = path.resolve(process.cwd(), fileOrObject);
|
|
72
|
+
if (!existsSync(dataPath)) {
|
|
73
|
+
throw new Error(`Data file '${dataPath}' was not found.`);
|
|
74
|
+
}
|
|
75
|
+
// 3. read the file and parse it as JSON
|
|
76
|
+
try {
|
|
77
|
+
const content = await fs.readFile(dataPath, "utf8");
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof SyntaxError) {
|
|
81
|
+
throw new Error(`Invalid JSON in data file '${dataPath}': ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Failed to read data file '${dataPath}': ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// @description load javascript modules
|
|
88
|
+
const loadModules = async (patterns = [], callback) => {
|
|
89
|
+
const files = await expandGlobPatterns(patterns);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < files.length; i++) {
|
|
92
|
+
const file = files[i]; // path.resolve(process.cwd(), uniqueFiles[i]);
|
|
93
|
+
if (!existsSync(file)) {
|
|
94
|
+
throw new Error(`File '${file}' was not found.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if it's a file (not a directory)
|
|
98
|
+
const stats = await fs.stat(file);
|
|
99
|
+
if (!stats.isFile()) {
|
|
100
|
+
continue; // Skip directories
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// import the module
|
|
104
|
+
try {
|
|
105
|
+
await callback(file);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`Failed to load '${file}': ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// print the help of the tool
|
|
113
|
+
const printHelp = () => {
|
|
114
|
+
console.log("Usage: ");
|
|
115
|
+
console.log(" mikel --help");
|
|
116
|
+
console.log(" mikel <template> [...options]");
|
|
117
|
+
console.log("");
|
|
118
|
+
console.log("Options:");
|
|
119
|
+
console.log(" -h, --help Prints the usage information");
|
|
120
|
+
console.log(" -P, --partial <file> Register a partial (supports glob patterns, can be used multiple times)");
|
|
121
|
+
console.log(" -H, --helper <file> Register a helper (supports glob patterns, can be used multiple times)");
|
|
122
|
+
console.log(" -F, --function <file> Register a function (supports glob patterns, can be used multiple times)");
|
|
123
|
+
console.log(" -L, --plugin <file> Load a plugin from node_modules (can be used multiple times)");
|
|
124
|
+
console.log(" -D, --data <file> Path to the data file to use (JSON)");
|
|
125
|
+
console.log(" -o, --output <file> Output file");
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log("Examples:");
|
|
128
|
+
console.log(" mikel template.html --data data.json --output www/index.html");
|
|
129
|
+
console.log(" mikel template.html --data data.json --partial header.html --partial footer.html --output www/index.html");
|
|
130
|
+
console.log(" mikel template.html --helper helpers.js --function utils.js --output dist/index.html");
|
|
131
|
+
console.log(" mikel template.html --partial 'partials/*.html' --helper 'helpers/*.js' --output dist/index.html");
|
|
132
|
+
console.log(" mikel template.html --partial 'components/**/*.html' --output dist/index.html");
|
|
133
|
+
console.log("");
|
|
134
|
+
process.exit(0);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// @description main function
|
|
138
|
+
const main = async (inputOption = "", options = {}) => {
|
|
139
|
+
// check to print help
|
|
140
|
+
if (options.help) {
|
|
141
|
+
return printHelp();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// load configuration file and inputs
|
|
145
|
+
const config = await loadConfiguration(options.config);
|
|
146
|
+
const inputs = await loadInput(inputOption || config.input || null);
|
|
147
|
+
const data = await loadData(options.data || config.data || null);
|
|
148
|
+
const mikelInstance = mikel.create({});
|
|
149
|
+
|
|
150
|
+
// if no input files were provided, throw an error and stop processing
|
|
151
|
+
if (!inputs || inputs?.length === 0) {
|
|
152
|
+
throw new Error(`No input templates found`);
|
|
153
|
+
}
|
|
154
|
+
// load plugins
|
|
155
|
+
const plugins = options.plugin || config.plugins || [];
|
|
156
|
+
for (let i = 0; i < plugins.length; i++) {
|
|
157
|
+
const pluginName = Array.isArray(plugins[i]) ? plugins[i][0] : plugins[i];
|
|
158
|
+
const pluginOptions = Array.isArray(plugins[i]) && plugins[i].length === 2 ? plugins[i][1] : null;
|
|
159
|
+
let pluginModule;
|
|
160
|
+
try {
|
|
161
|
+
// try to import the plugin from node_modules
|
|
162
|
+
pluginModule = (await import(pluginName))?.default;
|
|
163
|
+
if (typeof pluginModule !== "function") {
|
|
164
|
+
throw new Error(`Plugin '${pluginName}' does not export a valid plugin function.`);
|
|
165
|
+
}
|
|
166
|
+
mikelInstance.use(pluginModule(pluginOptions));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new Error(`Failed to load plugin '${pluginName}': ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// load additional partials, helpers, and functions
|
|
173
|
+
await loadModules(options.partial, async (file) => {
|
|
174
|
+
try {
|
|
175
|
+
mikelInstance.addPartial(path.basename(file), await fs.readFile(file, "utf8"));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Failed to read partial file '${file}': ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
await loadModules(options.helper, async (file) => {
|
|
181
|
+
const extension = path.extname(file);
|
|
182
|
+
if (extension !== ".js" && extension !== ".mjs") {
|
|
183
|
+
throw new Error(`Module '${file}' is not supported. Only ESM JavaScript (.js or .mjs) files are supported.`);
|
|
184
|
+
}
|
|
185
|
+
// import the module
|
|
186
|
+
const content = (await import(file)) || {};
|
|
187
|
+
if (typeof content === "object" && !!content) {
|
|
188
|
+
Object.keys(content).forEach(helperName => {
|
|
189
|
+
mikelInstance.addHelper(helperName, content[helperName]);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
await loadModules(options.function, async (file) => {
|
|
194
|
+
const extension = path.extname(file);
|
|
195
|
+
if (extension !== ".js" && extension !== ".mjs") {
|
|
196
|
+
throw new Error(`Module '${file}' is not supported. Only ESM JavaScript (.js or .mjs) files are supported.`);
|
|
197
|
+
}
|
|
198
|
+
// import the module
|
|
199
|
+
const content = (await import(file)) || {};
|
|
200
|
+
if (typeof content === "object" && !!content) {
|
|
201
|
+
Object.keys(content).forEach(functionName => {
|
|
202
|
+
mikelInstance.addFunction(functionName, content[functionName]);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// process input files
|
|
207
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
208
|
+
// const inputPath = path.resolve(process.cwd(), inputs[i]);
|
|
209
|
+
const inputPath = inputs[i]; // inputs contains absolute paths already
|
|
210
|
+
const relativeInputPath = path.relative(process.cwd(), inputPath);
|
|
211
|
+
if (!existsSync(inputPath)) {
|
|
212
|
+
throw new Error(`Template file '${inputPath}' was not found.`);
|
|
213
|
+
}
|
|
214
|
+
let template;
|
|
215
|
+
try {
|
|
216
|
+
template = await fs.readFile(inputPath, "utf8");
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Failed to read template file '${inputPath}': ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
// compile the template
|
|
221
|
+
let result;
|
|
222
|
+
try {
|
|
223
|
+
result = mikelInstance(template, data);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new Error(`Template compilation failed: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
// check if output argument has been provided to write the result to a file
|
|
228
|
+
// this will also create any intermediary directory that does not exist
|
|
229
|
+
if (options.output || config.output) {
|
|
230
|
+
const outputPath = resolveOutput(relativeInputPath, options.output, config.output);
|
|
231
|
+
const outputDirectory = path.dirname(outputPath);
|
|
232
|
+
// make sure that any directory containing the output file exists
|
|
233
|
+
if (!existsSync(outputDirectory)) {
|
|
234
|
+
try {
|
|
235
|
+
await fs.mkdir(outputDirectory, { recursive: true });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new Error(`Failed to create output directory '${outputDirectory}': ${error.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
await fs.writeFile(outputPath, result, "utf8");
|
|
242
|
+
console.error(`✓ Saving '${inputPath}' -> '${outputPath}'`);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new Error(`Failed to write output file '${outputPath}': ${error.message}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// if no output file has been provided, print the result to console
|
|
248
|
+
else if (inputs.length === 1) {
|
|
249
|
+
process.stdout.write(result);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
throw new Error(`Unconsistent usage of input and output arguments.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// exit
|
|
256
|
+
process.exit(0);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// process arguments
|
|
260
|
+
const { positionals, values } = parseArgs({
|
|
261
|
+
options: {
|
|
262
|
+
config: {
|
|
263
|
+
type: "string",
|
|
264
|
+
short: "c",
|
|
265
|
+
},
|
|
266
|
+
data: {
|
|
267
|
+
type: "string",
|
|
268
|
+
short: "D",
|
|
269
|
+
},
|
|
270
|
+
output: {
|
|
271
|
+
type: "string",
|
|
272
|
+
short: "o",
|
|
273
|
+
},
|
|
274
|
+
helper: {
|
|
275
|
+
type: "string",
|
|
276
|
+
short: "H",
|
|
277
|
+
multiple: true,
|
|
278
|
+
},
|
|
279
|
+
function: {
|
|
280
|
+
type: "string",
|
|
281
|
+
short: "F",
|
|
282
|
+
multiple: true,
|
|
283
|
+
},
|
|
284
|
+
partial: {
|
|
285
|
+
type: "string",
|
|
286
|
+
short: "P",
|
|
287
|
+
multiple: true,
|
|
288
|
+
},
|
|
289
|
+
plugin: {
|
|
290
|
+
type: "string",
|
|
291
|
+
short: "L",
|
|
292
|
+
multiple: true,
|
|
293
|
+
},
|
|
294
|
+
help: {
|
|
295
|
+
type: "boolean",
|
|
296
|
+
short: "h",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
allowPositionals: true,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// run main script
|
|
303
|
+
main(positionals[0], values).catch(error => {
|
|
304
|
+
console.error(`\n❌ Error: ${error.message}\n`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
});
|
package/index.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import fs from "node:fs/promises";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
2
|
import path from "node:path";
|
|
6
|
-
import { parseArgs } from "node:util";
|
|
7
|
-
import mikel from "mikel";
|
|
8
3
|
|
|
9
4
|
// @description get the files that matches the provided patterns
|
|
10
5
|
// this is a utility function to expand glob patterns to actual file paths.
|
|
11
6
|
// it uses Node.js 24+ built-in fs.glob to handle glob patterns.
|
|
12
|
-
const expandGlobPatterns = async (patterns = []) => {
|
|
7
|
+
export const expandGlobPatterns = async (patterns = []) => {
|
|
13
8
|
const files = [];
|
|
14
9
|
for (let i = 0; i < patterns.length; i++) {
|
|
15
10
|
const pattern = patterns[i];
|
|
@@ -33,233 +28,16 @@ const expandGlobPatterns = async (patterns = []) => {
|
|
|
33
28
|
});
|
|
34
29
|
};
|
|
35
30
|
|
|
36
|
-
// @description
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 javascript modules
|
|
59
|
-
const loadModules = async (patterns = [], callback) => {
|
|
60
|
-
const files = await expandGlobPatterns(patterns);
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < files.length; i++) {
|
|
63
|
-
const file = files[i]; // path.resolve(process.cwd(), uniqueFiles[i]);
|
|
64
|
-
if (!existsSync(file)) {
|
|
65
|
-
throw new Error(`File '${file}' was not found.`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check if it's a file (not a directory)
|
|
69
|
-
const stats = await fs.stat(file);
|
|
70
|
-
if (!stats.isFile()) {
|
|
71
|
-
continue; // Skip directories
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// import the module
|
|
75
|
-
try {
|
|
76
|
-
await callback(file);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
throw new Error(`Failed to load '${file}': ${error.message}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// print the help of the tool
|
|
84
|
-
const printHelp = () => {
|
|
85
|
-
console.log("Usage: ");
|
|
86
|
-
console.log(" mikel --help");
|
|
87
|
-
console.log(" mikel <template> [...options]");
|
|
88
|
-
console.log("");
|
|
89
|
-
console.log("Options:");
|
|
90
|
-
console.log(" -h, --help Prints the usage information");
|
|
91
|
-
console.log(" -P, --partial <file> Register a partial (supports glob patterns, can be used multiple times)");
|
|
92
|
-
console.log(" -H, --helper <file> Register a helper (supports glob patterns, can be used multiple times)");
|
|
93
|
-
console.log(" -F, --function <file> Register a function (supports glob patterns, can be used multiple times)");
|
|
94
|
-
console.log(" -L, --plugin <file> Load a plugin from node_modules (can be used multiple times)");
|
|
95
|
-
console.log(" -D, --data <file> Path to the data file to use (JSON)");
|
|
96
|
-
console.log(" -o, --output <file> Output file");
|
|
97
|
-
console.log("");
|
|
98
|
-
console.log("Examples:");
|
|
99
|
-
console.log(" mikel template.html --data data.json --output www/index.html");
|
|
100
|
-
console.log(" mikel template.html --data data.json --partial header.html --partial footer.html --output www/index.html");
|
|
101
|
-
console.log(" mikel template.html --helper helpers.js --function utils.js --output dist/index.html");
|
|
102
|
-
console.log(" mikel template.html --partial 'partials/*.html' --helper 'helpers/*.js' --output dist/index.html");
|
|
103
|
-
console.log(" mikel template.html --partial 'components/**/*.html' --output dist/index.html");
|
|
104
|
-
console.log("");
|
|
105
|
-
process.exit(0);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// @description main function
|
|
109
|
-
const main = async (input = "", options = {}) => {
|
|
110
|
-
// check to print help
|
|
111
|
-
if (options.help) {
|
|
112
|
-
return printHelp();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// make sure that input file exists
|
|
116
|
-
if (!input) {
|
|
117
|
-
throw new Error(`No input template file provided.`);
|
|
118
|
-
}
|
|
119
|
-
const inputPath = path.resolve(process.cwd(), input);
|
|
120
|
-
if (!existsSync(inputPath)) {
|
|
121
|
-
throw new Error(`Template file '${inputPath}' was not found.`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let template;
|
|
125
|
-
try {
|
|
126
|
-
template = await fs.readFile(inputPath, "utf8");
|
|
127
|
-
} catch (error) {
|
|
128
|
-
throw new Error(`Failed to read template file '${inputPath}': ${error.message}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// initialize the template engine
|
|
132
|
-
const mikelInstance = mikel.create({});
|
|
133
|
-
const data = await loadData(options.data);
|
|
134
|
-
|
|
135
|
-
// load plugins
|
|
136
|
-
for (let i = 0; i < (options.plugin || []).length; i++) {
|
|
137
|
-
const pluginName = options.plugin[i];
|
|
138
|
-
let pluginModule;
|
|
139
|
-
try {
|
|
140
|
-
// try to import the plugin from node_modules
|
|
141
|
-
pluginModule = (await import(pluginName))?.default;
|
|
142
|
-
if (typeof pluginModule !== "function") {
|
|
143
|
-
throw new Error(`Plugin '${pluginName}' does not export a valid plugin function.`);
|
|
144
|
-
}
|
|
145
|
-
mikelInstance.use(pluginModule());
|
|
146
|
-
} catch (error) {
|
|
147
|
-
throw new Error(`Failed to load plugin '${pluginName}': ${error.message}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// load additional partials, helpers, and functions
|
|
152
|
-
await loadModules(options.partial, async (file) => {
|
|
153
|
-
try {
|
|
154
|
-
mikelInstance.addPartial(path.basename(file), await fs.readFile(file, "utf8"));
|
|
155
|
-
} catch (error) {
|
|
156
|
-
throw new Error(`Failed to read partial file '${file}': ${error.message}`);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
await loadModules(options.helper, async (file) => {
|
|
160
|
-
const extension = path.extname(file);
|
|
161
|
-
if (extension !== ".js" && extension !== ".mjs") {
|
|
162
|
-
throw new Error(`Module '${file}' is not supported. Only ESM JavaScript (.js or .mjs) files are supported.`);
|
|
163
|
-
}
|
|
164
|
-
// import the module
|
|
165
|
-
const content = (await import(file)) || {};
|
|
166
|
-
if (typeof content === "object" && !!content) {
|
|
167
|
-
Object.keys(content).forEach(helperName => {
|
|
168
|
-
mikelInstance.addHelper(helperName, content[helperName]);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
await loadModules(options.function, async (file) => {
|
|
173
|
-
const extension = path.extname(file);
|
|
174
|
-
if (extension !== ".js" && extension !== ".mjs") {
|
|
175
|
-
throw new Error(`Module '${file}' is not supported. Only ESM JavaScript (.js or .mjs) files are supported.`);
|
|
176
|
-
}
|
|
177
|
-
// import the module
|
|
178
|
-
const content = (await import(file)) || {};
|
|
179
|
-
if (typeof content === "object" && !!content) {
|
|
180
|
-
Object.keys(content).forEach(functionName => {
|
|
181
|
-
mikelInstance.addFunction(functionName, content[functionName]);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// compile the template
|
|
187
|
-
let result;
|
|
188
|
-
try {
|
|
189
|
-
result = mikelInstance(template, data || {});
|
|
190
|
-
} catch (error) {
|
|
191
|
-
throw new Error(`Template compilation failed: ${error.message}`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// check if output argument has been provided to write the result to a file
|
|
195
|
-
// this will also create any intermediary directory that does not exist
|
|
196
|
-
if (options.output) {
|
|
197
|
-
const outputPath = path.resolve(process.cwd(), options.output);
|
|
198
|
-
const outputDirectory = path.dirname(outputPath);
|
|
199
|
-
// make sure that any directory containing the output file exists
|
|
200
|
-
if (!existsSync(outputDirectory)) {
|
|
201
|
-
try {
|
|
202
|
-
await fs.mkdir(outputDirectory, { recursive: true });
|
|
203
|
-
} catch (error) {
|
|
204
|
-
throw new Error(`Failed to create output directory '${outputDirectory}': ${error.message}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
await fs.writeFile(outputPath, result, "utf8");
|
|
209
|
-
console.error(`✓ Template rendered successfully to '${outputPath}'`);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
throw new Error(`Failed to write output file '${outputPath}': ${error.message}`);
|
|
31
|
+
// @description apply a rename to the provided file path based on a rename configuration
|
|
32
|
+
// object
|
|
33
|
+
export const applyRename = (filePath, rename = {}) => {
|
|
34
|
+
const patterns = Object.keys(rename);
|
|
35
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
36
|
+
const regex = new RegExp(patterns[i]);
|
|
37
|
+
if (regex.test(filePath)) {
|
|
38
|
+
return filePath.replace(regex, rename[patterns[i]]);
|
|
212
39
|
}
|
|
213
|
-
process.exit(0);
|
|
214
|
-
}
|
|
215
|
-
// if no output file has been provided, print the result to console
|
|
216
|
-
else {
|
|
217
|
-
process.stdout.write(result);
|
|
218
|
-
process.exit(0);
|
|
219
40
|
}
|
|
41
|
+
// fallback: only returns the basename of the file
|
|
42
|
+
return path.basename(filePath);
|
|
220
43
|
};
|
|
221
|
-
|
|
222
|
-
// process arguments
|
|
223
|
-
const { positionals, values } = parseArgs({
|
|
224
|
-
options: {
|
|
225
|
-
data: {
|
|
226
|
-
type: "string",
|
|
227
|
-
short: "D",
|
|
228
|
-
},
|
|
229
|
-
output: {
|
|
230
|
-
type: "string",
|
|
231
|
-
short: "o",
|
|
232
|
-
},
|
|
233
|
-
helper: {
|
|
234
|
-
type: "string",
|
|
235
|
-
short: "H",
|
|
236
|
-
multiple: true,
|
|
237
|
-
},
|
|
238
|
-
function: {
|
|
239
|
-
type: "string",
|
|
240
|
-
short: "F",
|
|
241
|
-
multiple: true,
|
|
242
|
-
},
|
|
243
|
-
partial: {
|
|
244
|
-
type: "string",
|
|
245
|
-
short: "P",
|
|
246
|
-
multiple: true,
|
|
247
|
-
},
|
|
248
|
-
plugin: {
|
|
249
|
-
type: "string",
|
|
250
|
-
short: "L",
|
|
251
|
-
multiple: true,
|
|
252
|
-
},
|
|
253
|
-
help: {
|
|
254
|
-
type: "boolean",
|
|
255
|
-
short: "h",
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
allowPositionals: true,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// run main script
|
|
262
|
-
main(positionals[0], values).catch(error => {
|
|
263
|
-
console.error(`\n❌ Error: ${error.message}\n`);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
});
|
package/package.json
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikel-cli",
|
|
3
3
|
"description": "The cli tool for mikel templating.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.34.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Josemi Juanes",
|
|
9
9
|
"email": "hello@josemi.xyz"
|
|
10
10
|
},
|
|
11
|
-
"repository":
|
|
11
|
+
"repository": {
|
|
12
|
+
"url": "https://github.com/jmjuanes/mikel",
|
|
13
|
+
"directory": "packages/mikel-cli"
|
|
14
|
+
},
|
|
12
15
|
"bugs": "https://github.com/jmjuanes/mikel/issues",
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=24.0.0"
|
|
15
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test.js"
|
|
21
|
+
},
|
|
16
22
|
"exports": {
|
|
17
23
|
".": "./index.js",
|
|
18
24
|
"./index.js": "./index.js",
|
|
25
|
+
"./cli.js": "./cli.js",
|
|
19
26
|
"./package.json": "./package.json"
|
|
20
27
|
},
|
|
21
28
|
"bin": {
|
|
22
|
-
"mikel": "
|
|
29
|
+
"mikel": "cli.js"
|
|
23
30
|
},
|
|
24
31
|
"peerDependencies": {
|
|
25
|
-
"mikel": "^0.
|
|
32
|
+
"mikel": "^0.34.0"
|
|
26
33
|
},
|
|
27
34
|
"files": [
|
|
28
35
|
"README.md",
|
|
36
|
+
"cli.js",
|
|
29
37
|
"index.js"
|
|
30
38
|
],
|
|
31
39
|
"keywords": [
|