mikel-cli 0.33.1 → 0.35.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 +172 -17
- package/cli.js +95 -0
- package/index.d.ts +19 -0
- package/index.js +189 -205
- package/package.json +22 -7
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
|
|
|
@@ -32,8 +32,9 @@ $ mikel <template> [options]
|
|
|
32
32
|
| Option | Short | Description |
|
|
33
33
|
|--------|-------|-------------|
|
|
34
34
|
| `--help` | `-h` | Display help information |
|
|
35
|
-
| `--
|
|
35
|
+
| `--config <file>` | `-c` | Path to configuration file |
|
|
36
36
|
| `--output <file>` | `-o` | Output file path |
|
|
37
|
+
| `--data <file>` | `-D` | Path to JSON data file |
|
|
37
38
|
| `--plugin <module>` | `-L` | Load a Mikel plugin from a JavaScript module (can be used multiple times) |
|
|
38
39
|
| `--partial <file>` | `-P` | Register a partial template (supports glob patterns, can be used multiple times) |
|
|
39
40
|
| `--helper <file>` | `-H` | Register helper functions from a JavaScript module (supports glob patterns, can be used multiple times) |
|
|
@@ -99,33 +100,187 @@ 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:
|
|
119
106
|
|
|
120
107
|
| Pattern | Description | Example |
|
|
121
108
|
|---------|-------------|---------|
|
|
122
|
-
| `*.ext` | All files with extension in current directory | `*.html
|
|
109
|
+
| `*.ext` | All files with extension in current directory | `*.html` |
|
|
123
110
|
| `dir/*.ext` | All files with extension in specific directory | `partials/*.html` |
|
|
124
111
|
| `dir/**/*.ext` | All files with extension in directory and subdirectories | `components/**/*.html` |
|
|
125
112
|
| `?` | Single character wildcard | `file?.html` |
|
|
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
|
+
nameMapper: {
|
|
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
|
+
An object containing the options to instruct mikel where and how to save rendered templates.
|
|
159
|
+
|
|
160
|
+
##### `output.dir`
|
|
161
|
+
|
|
162
|
+
Output directory to save the rendered templates. If not provided, rendered templates will be saved in the current working directory.
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
export default {
|
|
166
|
+
output: {
|
|
167
|
+
dir: "dist/",
|
|
168
|
+
},
|
|
169
|
+
// ...
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
##### `output.nameMapper`
|
|
174
|
+
|
|
175
|
+
An object to map the input file names to the output file names. It 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.
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
// src/docs/guide/index.mustache -> dist/docs/guide/index.html
|
|
179
|
+
export default {
|
|
180
|
+
output: {
|
|
181
|
+
nameMapper: {
|
|
182
|
+
"^src/(.+)\\.mustache$": "$1.html",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
// ...
|
|
186
|
+
};
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `data`
|
|
190
|
+
|
|
191
|
+
Data to pass to the templates. Accepts a path to a JSON file:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
export default {
|
|
195
|
+
data: "./data/site.json",
|
|
196
|
+
// ...
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or a plain object:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
export default {
|
|
204
|
+
data: {
|
|
205
|
+
site: {
|
|
206
|
+
title: "My Site",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
// ...
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### `helpers`
|
|
214
|
+
|
|
215
|
+
An object containing helpers that will be registered in the mikel engine:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
export default {
|
|
219
|
+
helpers: {
|
|
220
|
+
uppercase: ({ fn, data }) => {
|
|
221
|
+
return fn(data).toUpperCase();
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### `functions`
|
|
228
|
+
|
|
229
|
+
An object containing functions that will be registered in the mikel engine:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
export default {
|
|
233
|
+
functions: {
|
|
234
|
+
sayHello: () => "Hello!",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `partials`
|
|
240
|
+
|
|
241
|
+
An object containing partials that will be registered in the mikel engine:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
export default {
|
|
245
|
+
partials: {
|
|
246
|
+
"foo": "Hello {{this.bar}}",
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### `plugins`
|
|
252
|
+
|
|
253
|
+
An array of Mikel plugins to load. See the [Plugins](#plugins) section for details.
|
|
254
|
+
|
|
255
|
+
## Plugins
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
259
|
+
### Loading Plugins via CLI
|
|
260
|
+
|
|
261
|
+
Use the `--plugin` flag to load a plugin from a JavaScript module:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
mikel template.html --plugin mikel-markdown --output dist/index.html
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Loading Plugins via Configuration
|
|
268
|
+
|
|
269
|
+
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:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
export default {
|
|
273
|
+
plugins: [
|
|
274
|
+
// plugin without options
|
|
275
|
+
"mikel-frontmatter",
|
|
276
|
+
// plugin with options
|
|
277
|
+
["mikel-markdown", {
|
|
278
|
+
classNames: { ... },
|
|
279
|
+
}],
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
129
284
|
## License
|
|
130
285
|
|
|
131
286
|
Licensed under the [MIT License](../../LICENSE).
|
package/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { build, resolveConfigurationFromArgs } from "./index.js";
|
|
5
|
+
|
|
6
|
+
// print the help of the tool
|
|
7
|
+
const printHelp = () => {
|
|
8
|
+
console.log("Usage: ");
|
|
9
|
+
console.log(" mikel --help");
|
|
10
|
+
console.log(" mikel <template> [...options]");
|
|
11
|
+
console.log(" mikel --config <configurationFile> [...options]");
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log("Options:");
|
|
14
|
+
console.log(" -h, --help Prints the usage information");
|
|
15
|
+
console.log(" -c, --config <file> Configuration file to use");
|
|
16
|
+
console.log(" -o, --output <path> Output file or directory to save compiled templates");
|
|
17
|
+
console.log(" -P, --partial <file> Register a partial (supports glob patterns, can be used multiple times)");
|
|
18
|
+
console.log(" -H, --helper <file> Register a helper (supports glob patterns, can be used multiple times)");
|
|
19
|
+
console.log(" -F, --function <file> Register a function (supports glob patterns, can be used multiple times)");
|
|
20
|
+
console.log(" -L, --plugin <file> Load a plugin from node_modules (can be used multiple times)");
|
|
21
|
+
console.log(" -D, --data <file> Path to the data file to use (JSON)");
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log("Examples:");
|
|
24
|
+
console.log(" mikel template.html --data data.json --output www/index.html");
|
|
25
|
+
console.log(" mikel template.html --data data.json --partial header.html --partial footer.html --output www/index.html");
|
|
26
|
+
console.log(" mikel template.html --partial 'components/**/*.html' --output dist/index.html");
|
|
27
|
+
console.log(" mikel template.html --helper helpers.js --function utils.js --output dist/index.html");
|
|
28
|
+
console.log(" mikel template.html --plugin mikel-markdown --output dist/index.html");
|
|
29
|
+
console.log("");
|
|
30
|
+
process.exit(0);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// @description main function
|
|
34
|
+
const main = async () => {
|
|
35
|
+
// process arguments
|
|
36
|
+
const { positionals, values } = parseArgs({
|
|
37
|
+
options: {
|
|
38
|
+
config: {
|
|
39
|
+
type: "string",
|
|
40
|
+
short: "c",
|
|
41
|
+
},
|
|
42
|
+
data: {
|
|
43
|
+
type: "string",
|
|
44
|
+
short: "D",
|
|
45
|
+
},
|
|
46
|
+
output: {
|
|
47
|
+
type: "string",
|
|
48
|
+
short: "o",
|
|
49
|
+
},
|
|
50
|
+
partial: {
|
|
51
|
+
type: "string",
|
|
52
|
+
short: "P",
|
|
53
|
+
multiple: true,
|
|
54
|
+
},
|
|
55
|
+
plugin: {
|
|
56
|
+
type: "string",
|
|
57
|
+
short: "L",
|
|
58
|
+
multiple: true,
|
|
59
|
+
},
|
|
60
|
+
helper: {
|
|
61
|
+
type: "string",
|
|
62
|
+
short: "H",
|
|
63
|
+
multiple: true,
|
|
64
|
+
},
|
|
65
|
+
function: {
|
|
66
|
+
type: "string",
|
|
67
|
+
short: "F",
|
|
68
|
+
multiple: true,
|
|
69
|
+
},
|
|
70
|
+
help: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
short: "h",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
allowPositionals: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// check to print help
|
|
79
|
+
if (values.help) {
|
|
80
|
+
return printHelp();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// build with the provided configuration
|
|
84
|
+
try {
|
|
85
|
+
const config = await resolveConfigurationFromArgs(process.cwd(), { positionals, values });
|
|
86
|
+
await build(config);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error(`\n❌ ${error.message}\n`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
main()
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MikelHelper, MikelFunction, MikelPartial, MikelPlugin } from "mikel";
|
|
2
|
+
|
|
3
|
+
export type MikelCliPlugin = string | [string, ...any] | MikelPlugin;
|
|
4
|
+
|
|
5
|
+
export type MikelCliConfig = {
|
|
6
|
+
context?: string;
|
|
7
|
+
input?: string | string[];
|
|
8
|
+
output?: string | {
|
|
9
|
+
dir?: string;
|
|
10
|
+
nameMapper?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
partials?: Record<string, string | MikelPartial>;
|
|
13
|
+
helpers?: Record<string, MikelHelper>;
|
|
14
|
+
functions?: Record<string, MikelFunction>;
|
|
15
|
+
plugins?: MikelCliPlugin[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export declare const defineConfig: (config: MikelCliConfig) => MikelCliConfig;
|
|
19
|
+
export declare const build: (config: MikelCliConfig) => Promise<void>;
|
package/index.js
CHANGED
|
@@ -1,52 +1,103 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import fs from "node:fs/promises";
|
|
4
2
|
import { existsSync } from "node:fs";
|
|
5
3
|
import path from "node:path";
|
|
6
|
-
import { parseArgs } from "node:util";
|
|
7
4
|
import mikel from "mikel";
|
|
8
5
|
|
|
9
6
|
// @description get the files that matches the provided patterns
|
|
10
7
|
// this is a utility function to expand glob patterns to actual file paths.
|
|
11
8
|
// it uses Node.js 24+ built-in fs.glob to handle glob patterns.
|
|
12
|
-
const expandGlobPatterns = async (patterns = []) => {
|
|
9
|
+
export const expandGlobPatterns = async (root, patterns = []) => {
|
|
13
10
|
const files = [];
|
|
14
|
-
for (
|
|
15
|
-
const pattern = patterns[i];
|
|
11
|
+
for (const pattern of patterns) {
|
|
16
12
|
if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
files.push(file);
|
|
22
|
-
}
|
|
23
|
-
} catch (error) {
|
|
24
|
-
files.push(pattern);
|
|
13
|
+
// use Node.js 24+ built-in fs.glob
|
|
14
|
+
// https://nodejs.org/api/fs.html#fspromisesglobpattern-options
|
|
15
|
+
for await (const file of fs.glob(pattern, { cwd: root })) {
|
|
16
|
+
files.push(file);
|
|
25
17
|
}
|
|
26
18
|
} else {
|
|
27
19
|
files.push(pattern);
|
|
28
20
|
}
|
|
29
21
|
}
|
|
30
|
-
// remove duplicates
|
|
31
|
-
return Array.from(new Set(files))
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
// remove duplicates
|
|
23
|
+
return Array.from(new Set(files));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// @description apply a rename to the provided file path based on a rename configuration
|
|
27
|
+
// object
|
|
28
|
+
export const applyNameMapping = (file, mapping = {}) => {
|
|
29
|
+
for (const pattern of Object.keys(mapping)) {
|
|
30
|
+
const regex = new RegExp(pattern);
|
|
31
|
+
if (regex.test(file)) {
|
|
32
|
+
return file.replace(regex, mapping[pattern]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// fallback: only returns the basename of the file
|
|
36
|
+
return path.basename(file);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// @description load configuration file from the provided path
|
|
40
|
+
export const loadConfiguration = async (configurationFile) => {
|
|
41
|
+
if (!configurationFile) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
const configurationPath = path.resolve(process.cwd(), configurationFile);
|
|
45
|
+
if (!existsSync(configurationPath)) {
|
|
46
|
+
throw new Error(`Configuration file '${configurationPath}' was not found.`);
|
|
47
|
+
}
|
|
48
|
+
// check the extension of the file
|
|
49
|
+
const configurationExtension = path.extname(configurationFile);
|
|
50
|
+
if (configurationExtension === ".js" || configurationExtension === ".mjs") {
|
|
51
|
+
return await import(configurationPath);
|
|
52
|
+
}
|
|
53
|
+
else if (configurationExtension === ".json") {
|
|
54
|
+
return JSON.parse(await fs.readFile(configurationPath, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
// invalid configuration extension
|
|
57
|
+
throw new Error(`Unknown extension for configuration file '${configurationFile}'`);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// @description get the files that matches the provided input files patterns
|
|
61
|
+
export const loadInputFiles = async (root, inputFiles) => {
|
|
62
|
+
if (!inputFiles || inputFiles?.length === 0) {
|
|
63
|
+
throw new Error(`No input templates provided.`);
|
|
64
|
+
}
|
|
65
|
+
return expandGlobPatterns(root, Array.isArray(inputFiles) ? inputFiles : [inputFiles]);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// @description resolve output
|
|
69
|
+
export const resolveOutput = (root, file, output) => {
|
|
70
|
+
// 1. the provided output is an string
|
|
71
|
+
if (!!output && typeof output === "string") {
|
|
72
|
+
// directory if ends with /, otherwise treat as output file
|
|
73
|
+
return path.resolve(root, output.endsWith("/") ? path.join(output, file) : output);
|
|
74
|
+
}
|
|
75
|
+
// 2. output configuration is provided as an object
|
|
76
|
+
else if (!!output && typeof output === "object") {
|
|
77
|
+
const renamedOutputFile = applyNameMapping(file, output?.nameMapping || {});
|
|
78
|
+
return path.resolve(root, path.join(output?.dir || ".", renamedOutputFile));
|
|
79
|
+
}
|
|
80
|
+
// 3. other case???
|
|
81
|
+
throw new Error(`Unknown error resolving output for template '${file}'`);
|
|
34
82
|
};
|
|
35
83
|
|
|
36
84
|
// @description load JSON data from the provided path
|
|
37
|
-
const loadData = async (
|
|
38
|
-
if (!
|
|
85
|
+
export const loadData = async (root, fileOrObject = null) => {
|
|
86
|
+
if (!fileOrObject) {
|
|
39
87
|
return {};
|
|
40
88
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
89
|
+
// 1. check for object containing data (from config.data)
|
|
90
|
+
if (typeof fileOrObject === "object") {
|
|
91
|
+
return fileOrObject;
|
|
92
|
+
}
|
|
93
|
+
// 2. build the full data file path and check if exists
|
|
94
|
+
const dataPath = path.resolve(root, fileOrObject);
|
|
43
95
|
if (!existsSync(dataPath)) {
|
|
44
96
|
throw new Error(`Data file '${dataPath}' was not found.`);
|
|
45
97
|
}
|
|
46
|
-
// read the file and parse it as JSON
|
|
98
|
+
// 3. read the file and parse it as JSON
|
|
47
99
|
try {
|
|
48
|
-
|
|
49
|
-
return JSON.parse(content);
|
|
100
|
+
return JSON.parse(await fs.readFile(dataPath, "utf8"));
|
|
50
101
|
} catch (error) {
|
|
51
102
|
if (error instanceof SyntaxError) {
|
|
52
103
|
throw new Error(`Invalid JSON in data file '${dataPath}': ${error.message}`);
|
|
@@ -55,211 +106,144 @@ const loadData = async (file = null) => {
|
|
|
55
106
|
}
|
|
56
107
|
};
|
|
57
108
|
|
|
58
|
-
// @description load
|
|
59
|
-
const
|
|
60
|
-
const files = await expandGlobPatterns(patterns);
|
|
61
|
-
|
|
62
|
-
for (
|
|
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
|
|
109
|
+
// @description load partials
|
|
110
|
+
const loadPartials = async (root, patterns = []) => {
|
|
111
|
+
const files = await expandGlobPatterns(root, [patterns].flat());
|
|
112
|
+
const partials = {};
|
|
113
|
+
for (const file of files) {
|
|
75
114
|
try {
|
|
76
|
-
await
|
|
115
|
+
partials[path.basename(file)] = await fs.readFile(path.resolve(root, file), "utf8");
|
|
77
116
|
} catch (error) {
|
|
78
|
-
throw new Error(`Failed to
|
|
117
|
+
throw new Error(`Failed to read partial file '${file}': ${error.message}`);
|
|
79
118
|
}
|
|
80
119
|
}
|
|
120
|
+
return partials;
|
|
81
121
|
};
|
|
82
122
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(" mikel template.html --partial 'components/**/*.html' --output dist/index.html");
|
|
104
|
-
console.log("");
|
|
105
|
-
process.exit(0);
|
|
123
|
+
// @description load javascript modules from the specified patterns
|
|
124
|
+
const loadModules = async (root, patterns = []) => {
|
|
125
|
+
const files = await expandGlobPatterns(root, [patterns].flat());
|
|
126
|
+
const loadedModules = {};
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const filePath = path.resolve(root, file);
|
|
129
|
+
//only javascript modules are supported, so we have to check the extension of the file
|
|
130
|
+
const extension = path.extname(filePath);
|
|
131
|
+
if (extension !== ".js" && extension !== ".mjs") {
|
|
132
|
+
throw new Error(`Module '${filePath}' is not supported. Only ESM JavaScript (.js or .mjs) files are supported.`);
|
|
133
|
+
}
|
|
134
|
+
// import the module and call the register method
|
|
135
|
+
const module = await import(filePath);
|
|
136
|
+
for (const [name, fn] of Object.entries(module)) {
|
|
137
|
+
if (typeof fn === "function") {
|
|
138
|
+
loadedModules[name] = fn;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return loadedModules;
|
|
106
143
|
};
|
|
107
144
|
|
|
108
|
-
// @description
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (options.help) {
|
|
112
|
-
return printHelp();
|
|
113
|
-
}
|
|
145
|
+
// @description build a configuration object from CLI arguments
|
|
146
|
+
export const resolveConfigurationFromArgs = async (root = process.cwd(), args = {}) => {
|
|
147
|
+
const config = await loadConfiguration(args?.values?.config);
|
|
114
148
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
}
|
|
149
|
+
// resolve partials, helpers and functions from cli arguments
|
|
150
|
+
const partials = await loadPartials(root, args?.values?.partial || []);
|
|
151
|
+
const helpers = await loadModules(root, args?.values?.helper || []);
|
|
152
|
+
const functions = await loadModules(root, args?.values?.function || []);
|
|
130
153
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
// return parsed configuration object
|
|
155
|
+
return {
|
|
156
|
+
context: config.context || root,
|
|
157
|
+
input: (!!args?.positionals && Array.isArray(args?.positionals) && args.positionals.length > 0) ? args.positionals : config.input,
|
|
158
|
+
output: args?.values?.output || config.output,
|
|
159
|
+
data: args?.values?.data || config.data,
|
|
160
|
+
partials: Object.assign(config.partials || {}, partials),
|
|
161
|
+
helpers: Object.assign(config.helpers || {}, helpers),
|
|
162
|
+
functions: Object.assign(config.functions || {}, functions),
|
|
163
|
+
plugins: args?.values?.plugin || config.plugins || [],
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// @description main build script
|
|
168
|
+
export const build = async (config = {}) => {
|
|
169
|
+
const inputFiles = await loadInputFiles(config.context, config.input);
|
|
170
|
+
const data = await loadData(config.context, config.data);
|
|
171
|
+
const mikelInstance = mikel.create({
|
|
172
|
+
helpers: config.helpers,
|
|
173
|
+
functions: config.functions,
|
|
174
|
+
partials: config.partials,
|
|
175
|
+
});
|
|
134
176
|
|
|
135
177
|
// load plugins
|
|
136
|
-
for (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
for (const plugin of config.plugins) {
|
|
179
|
+
// check if the provided plugin is a function or an object
|
|
180
|
+
if (typeof plugin === "function" || (typeof plugin === "object" && !Array.isArray(plugin) && !!plugin)) {
|
|
181
|
+
mikelInstance.use(plugin);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
|
|
185
|
+
const pluginOptions = Array.isArray(plugin) && plugin.length > 1 ? plugin.slice(1) : [];
|
|
186
|
+
try {
|
|
187
|
+
// try to import the plugin from node_modules
|
|
188
|
+
const pluginModule = (await import(pluginName))?.default;
|
|
189
|
+
if (typeof pluginModule !== "function") {
|
|
190
|
+
throw new Error(`Plugin '${pluginName}' does not export a valid plugin function.`);
|
|
191
|
+
}
|
|
192
|
+
mikelInstance.use(pluginModule(...pluginOptions));
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new Error(`Failed to load plugin '${pluginName}': ${error.message}`);
|
|
144
195
|
}
|
|
145
|
-
mikelInstance.use(pluginModule());
|
|
146
|
-
} catch (error) {
|
|
147
|
-
throw new Error(`Failed to load plugin '${pluginName}': ${error.message}`);
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
198
|
|
|
151
|
-
//
|
|
152
|
-
|
|
199
|
+
// process input files
|
|
200
|
+
for (const inputFile of inputFiles) {
|
|
201
|
+
const inputPath = path.resolve(config.context, inputFile);
|
|
202
|
+
if (!existsSync(inputPath)) {
|
|
203
|
+
throw new Error(`Template file '${inputPath}' was not found.`);
|
|
204
|
+
}
|
|
205
|
+
let template;
|
|
153
206
|
try {
|
|
154
|
-
|
|
207
|
+
template = await fs.readFile(inputPath, "utf8");
|
|
155
208
|
} catch (error) {
|
|
156
|
-
throw new Error(`Failed to read
|
|
209
|
+
throw new Error(`Failed to read template file '${inputPath}': ${error.message}`);
|
|
157
210
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
});
|
|
211
|
+
// compile the template
|
|
212
|
+
let result;
|
|
213
|
+
try {
|
|
214
|
+
result = mikelInstance(template, data);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(`Template compilation error: ${error.message}`);
|
|
183
217
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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)) {
|
|
218
|
+
// check if output argument has been provided to write the result to a file
|
|
219
|
+
// this will also create any intermediary directory that does not exist
|
|
220
|
+
if (config.output) {
|
|
221
|
+
const outputPath = resolveOutput(config.context, inputFile, config.output);
|
|
222
|
+
const outputDirectory = path.dirname(outputPath);
|
|
223
|
+
// make sure that any directory containing the output file exists
|
|
224
|
+
if (!existsSync(outputDirectory)) {
|
|
225
|
+
try {
|
|
226
|
+
await fs.mkdir(outputDirectory, { recursive: true });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new Error(`Failed to create output directory '${outputDirectory}': ${error.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
201
231
|
try {
|
|
202
|
-
await fs.
|
|
232
|
+
await fs.writeFile(outputPath, result, "utf8");
|
|
233
|
+
console.error(`✓ Saving '${inputPath}' -> '${outputPath}'`);
|
|
203
234
|
} catch (error) {
|
|
204
|
-
throw new Error(`Failed to
|
|
235
|
+
throw new Error(`Failed to write output file '${outputPath}': ${error.message}`);
|
|
205
236
|
}
|
|
206
237
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
238
|
+
// if no output file has been provided, print the result to console
|
|
239
|
+
else if (inputFiles.length === 1) {
|
|
240
|
+
process.stdout.write(result);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
throw new Error(`Unconsistent usage of input and output arguments.`);
|
|
212
244
|
}
|
|
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
245
|
}
|
|
220
246
|
};
|
|
221
247
|
|
|
222
|
-
//
|
|
223
|
-
const
|
|
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
|
-
});
|
|
248
|
+
// @description utility method to provide a typed configuration
|
|
249
|
+
export const defineConfig = (config = {}) => config;
|
package/package.json
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikel-cli",
|
|
3
3
|
"description": "The cli tool for mikel templating.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.35.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
|
+
"types": "index.d.ts",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --import=./loader.js test.js"
|
|
22
|
+
},
|
|
16
23
|
"exports": {
|
|
17
|
-
".":
|
|
18
|
-
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./index.js",
|
|
26
|
+
"types": "./index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./index.js": {
|
|
29
|
+
"import": "./index.js",
|
|
30
|
+
"types": "./index.d.ts"
|
|
31
|
+
},
|
|
19
32
|
"./package.json": "./package.json"
|
|
20
33
|
},
|
|
21
34
|
"bin": {
|
|
22
|
-
"mikel": "
|
|
35
|
+
"mikel": "cli.js"
|
|
23
36
|
},
|
|
24
37
|
"peerDependencies": {
|
|
25
|
-
"mikel": "^0.
|
|
38
|
+
"mikel": "^0.35.0"
|
|
26
39
|
},
|
|
27
40
|
"files": [
|
|
28
41
|
"README.md",
|
|
29
|
-
"
|
|
42
|
+
"cli.js",
|
|
43
|
+
"index.js",
|
|
44
|
+
"index.d.ts"
|
|
30
45
|
],
|
|
31
46
|
"keywords": [
|
|
32
47
|
"mikel",
|