mikel-cli 0.34.0 → 0.35.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/README.md +80 -27
- package/cli.js +59 -270
- package/index.d.ts +19 -0
- package/index.js +228 -22
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -31,10 +31,10 @@ $ mikel <template> [options]
|
|
|
31
31
|
|
|
32
32
|
| Option | Short | Description |
|
|
33
33
|
|--------|-------|-------------|
|
|
34
|
-
| `--config <file>` | `-c` | Path to configuration file |
|
|
35
34
|
| `--help` | `-h` | Display help information |
|
|
36
|
-
| `--
|
|
35
|
+
| `--config <file>` | `-c` | Path to configuration file |
|
|
37
36
|
| `--output <file>` | `-o` | Output file path |
|
|
37
|
+
| `--data <file>` | `-D` | Path to JSON data file |
|
|
38
38
|
| `--plugin <module>` | `-L` | Load a Mikel plugin from a JavaScript module (can be used multiple times) |
|
|
39
39
|
| `--partial <file>` | `-P` | Register a partial template (supports glob patterns, can be used multiple times) |
|
|
40
40
|
| `--helper <file>` | `-H` | Register helper functions from a JavaScript module (supports glob patterns, can be used multiple times) |
|
|
@@ -106,7 +106,7 @@ The `--partial`, `--helper`, and `--function` options support glob patterns for
|
|
|
106
106
|
|
|
107
107
|
| Pattern | Description | Example |
|
|
108
108
|
|---------|-------------|---------|
|
|
109
|
-
| `*.ext` | All files with extension in current directory | `*.html
|
|
109
|
+
| `*.ext` | All files with extension in current directory | `*.html` |
|
|
110
110
|
| `dir/*.ext` | All files with extension in specific directory | `partials/*.html` |
|
|
111
111
|
| `dir/**/*.ext` | All files with extension in directory and subdirectories | `components/**/*.html` |
|
|
112
112
|
| `?` | Single character wildcard | `file?.html` |
|
|
@@ -122,7 +122,7 @@ export default {
|
|
|
122
122
|
input: "src/**/*.mustache",
|
|
123
123
|
output: {
|
|
124
124
|
dir: "dist/",
|
|
125
|
-
|
|
125
|
+
nameMapper: {
|
|
126
126
|
"^src/(.+)\\.mustache$": "$1.html",
|
|
127
127
|
},
|
|
128
128
|
},
|
|
@@ -155,44 +155,97 @@ input: ["src/index.mustache", "src/pages/**/*.mustache"]
|
|
|
155
155
|
|
|
156
156
|
#### `output`
|
|
157
157
|
|
|
158
|
-
|
|
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.
|
|
159
163
|
|
|
160
164
|
```js
|
|
161
|
-
|
|
162
|
-
output:
|
|
163
|
-
|
|
164
|
-
// with rename rules
|
|
165
|
-
output: {
|
|
166
|
-
dir: "dist/",
|
|
167
|
-
rename: {
|
|
168
|
-
"^src/(.+)\\.mustache$": "$1.html",
|
|
165
|
+
export default {
|
|
166
|
+
output: {
|
|
167
|
+
dir: "dist/",
|
|
169
168
|
},
|
|
170
|
-
|
|
169
|
+
// ...
|
|
170
|
+
};
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
|
|
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.
|
|
174
176
|
|
|
175
177
|
```js
|
|
176
|
-
// src/docs/guide/index.mustache
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
};
|
|
180
187
|
```
|
|
181
188
|
|
|
182
189
|
#### `data`
|
|
183
190
|
|
|
184
|
-
Data to pass to the templates. Accepts a path to a JSON file
|
|
191
|
+
Data to pass to the templates. Accepts a path to a JSON file:
|
|
185
192
|
|
|
186
193
|
```js
|
|
187
|
-
|
|
188
|
-
data: "./data/site.json"
|
|
194
|
+
export default {
|
|
195
|
+
data: "./data/site.json",
|
|
196
|
+
// ...
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or a plain object:
|
|
189
201
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
```js
|
|
203
|
+
export default {
|
|
204
|
+
data: {
|
|
205
|
+
site: {
|
|
206
|
+
title: "My Site",
|
|
207
|
+
},
|
|
194
208
|
},
|
|
195
|
-
|
|
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
|
+
};
|
|
196
249
|
```
|
|
197
250
|
|
|
198
251
|
#### `plugins`
|
package/cli.js
CHANGED
|
@@ -1,306 +1,95 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
3
|
import { parseArgs } from "node:util";
|
|
7
|
-
import
|
|
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
|
-
};
|
|
4
|
+
import { build, resolveConfigurationFromArgs } from "./index.js";
|
|
111
5
|
|
|
112
6
|
// print the help of the tool
|
|
113
7
|
const printHelp = () => {
|
|
114
8
|
console.log("Usage: ");
|
|
115
9
|
console.log(" mikel --help");
|
|
116
10
|
console.log(" mikel <template> [...options]");
|
|
11
|
+
console.log(" mikel --config <configurationFile> [...options]");
|
|
117
12
|
console.log("");
|
|
118
13
|
console.log("Options:");
|
|
119
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");
|
|
120
17
|
console.log(" -P, --partial <file> Register a partial (supports glob patterns, can be used multiple times)");
|
|
121
18
|
console.log(" -H, --helper <file> Register a helper (supports glob patterns, can be used multiple times)");
|
|
122
19
|
console.log(" -F, --function <file> Register a function (supports glob patterns, can be used multiple times)");
|
|
123
20
|
console.log(" -L, --plugin <file> Load a plugin from node_modules (can be used multiple times)");
|
|
124
21
|
console.log(" -D, --data <file> Path to the data file to use (JSON)");
|
|
125
|
-
console.log(" -o, --output <file> Output file");
|
|
126
22
|
console.log("");
|
|
127
23
|
console.log("Examples:");
|
|
128
24
|
console.log(" mikel template.html --data data.json --output www/index.html");
|
|
129
25
|
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
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");
|
|
133
29
|
console.log("");
|
|
134
30
|
process.exit(0);
|
|
135
31
|
};
|
|
136
32
|
|
|
137
33
|
// @description main function
|
|
138
|
-
const main = async (
|
|
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
|
+
|
|
139
78
|
// check to print help
|
|
140
|
-
if (
|
|
79
|
+
if (values.help) {
|
|
141
80
|
return printHelp();
|
|
142
81
|
}
|
|
143
82
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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`);
|
|
83
|
+
// build with the provided configuration
|
|
84
|
+
try {
|
|
85
|
+
const config = await resolveConfigurationFromArgs(process.cwd(), { positionals, values });
|
|
86
|
+
await build(config);
|
|
153
87
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error(`\n❌ ${error.message}\n`);
|
|
90
|
+
process.exit(1);
|
|
170
91
|
}
|
|
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
92
|
process.exit(0);
|
|
257
93
|
};
|
|
258
94
|
|
|
259
|
-
|
|
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
|
-
});
|
|
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,43 +1,249 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import mikel from "mikel";
|
|
3
5
|
|
|
4
6
|
// @description get the files that matches the provided patterns
|
|
5
7
|
// this is a utility function to expand glob patterns to actual file paths.
|
|
6
8
|
// it uses Node.js 24+ built-in fs.glob to handle glob patterns.
|
|
7
|
-
export const expandGlobPatterns = async (patterns = []) => {
|
|
9
|
+
export const expandGlobPatterns = async (root, patterns = []) => {
|
|
8
10
|
const files = [];
|
|
9
|
-
for (
|
|
10
|
-
const pattern = patterns[i];
|
|
11
|
+
for (const pattern of patterns) {
|
|
11
12
|
if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
files.push(file);
|
|
17
|
-
}
|
|
18
|
-
} catch (error) {
|
|
19
|
-
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);
|
|
20
17
|
}
|
|
21
18
|
} else {
|
|
22
19
|
files.push(pattern);
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
|
-
// remove duplicates
|
|
26
|
-
return Array.from(new Set(files))
|
|
27
|
-
return path.resolve(process.cwd(), file);
|
|
28
|
-
});
|
|
22
|
+
// remove duplicates
|
|
23
|
+
return Array.from(new Set(files));
|
|
29
24
|
};
|
|
30
25
|
|
|
31
26
|
// @description apply a rename to the provided file path based on a rename configuration
|
|
32
27
|
// object
|
|
33
|
-
export const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return filePath.replace(regex, rename[patterns[i]]);
|
|
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]);
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
35
|
// fallback: only returns the basename of the file
|
|
42
|
-
return path.basename(
|
|
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)).default;
|
|
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}'`);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// @description load JSON data from the provided path
|
|
85
|
+
export const loadData = async (root, fileOrObject = null) => {
|
|
86
|
+
if (!fileOrObject) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
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);
|
|
95
|
+
if (!existsSync(dataPath)) {
|
|
96
|
+
throw new Error(`Data file '${dataPath}' was not found.`);
|
|
97
|
+
}
|
|
98
|
+
// 3. read the file and parse it as JSON
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(await fs.readFile(dataPath, "utf8"));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof SyntaxError) {
|
|
103
|
+
throw new Error(`Invalid JSON in data file '${dataPath}': ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Failed to read data file '${dataPath}': ${error.message}`);
|
|
106
|
+
}
|
|
43
107
|
};
|
|
108
|
+
|
|
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) {
|
|
114
|
+
try {
|
|
115
|
+
partials[path.basename(file)] = await fs.readFile(path.resolve(root, file), "utf8");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new Error(`Failed to read partial file '${file}': ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return partials;
|
|
121
|
+
};
|
|
122
|
+
|
|
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;
|
|
143
|
+
};
|
|
144
|
+
|
|
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);
|
|
148
|
+
|
|
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 || []);
|
|
153
|
+
|
|
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
|
+
});
|
|
176
|
+
|
|
177
|
+
// load plugins
|
|
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}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
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;
|
|
206
|
+
try {
|
|
207
|
+
template = await fs.readFile(inputPath, "utf8");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw new Error(`Failed to read template file '${inputPath}': ${error.message}`);
|
|
210
|
+
}
|
|
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}`);
|
|
217
|
+
}
|
|
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
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await fs.writeFile(outputPath, result, "utf8");
|
|
233
|
+
console.error(`✓ Saving '${inputPath}' -> '${outputPath}'`);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`Failed to write output file '${outputPath}': ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
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.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// @description utility method to provide a typed configuration
|
|
249
|
+
export const defineConfig = (config = {}) => config;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikel-cli",
|
|
3
3
|
"description": "The cli tool for mikel templating.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.35.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": {
|
|
@@ -16,25 +16,32 @@
|
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=24.0.0"
|
|
18
18
|
},
|
|
19
|
+
"types": "index.d.ts",
|
|
19
20
|
"scripts": {
|
|
20
|
-
"test": "node test.js"
|
|
21
|
+
"test": "node --import=./loader.js test.js"
|
|
21
22
|
},
|
|
22
23
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./index.js",
|
|
26
|
+
"types": "./index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./index.js": {
|
|
29
|
+
"import": "./index.js",
|
|
30
|
+
"types": "./index.d.ts"
|
|
31
|
+
},
|
|
26
32
|
"./package.json": "./package.json"
|
|
27
33
|
},
|
|
28
34
|
"bin": {
|
|
29
35
|
"mikel": "cli.js"
|
|
30
36
|
},
|
|
31
37
|
"peerDependencies": {
|
|
32
|
-
"mikel": "^0.
|
|
38
|
+
"mikel": "^0.35.1"
|
|
33
39
|
},
|
|
34
40
|
"files": [
|
|
35
41
|
"README.md",
|
|
36
42
|
"cli.js",
|
|
37
|
-
"index.js"
|
|
43
|
+
"index.js",
|
|
44
|
+
"index.d.ts"
|
|
38
45
|
],
|
|
39
46
|
"keywords": [
|
|
40
47
|
"mikel",
|