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.
Files changed (5) hide show
  1. package/README.md +172 -17
  2. package/cli.js +95 -0
  3. package/index.d.ts +19 -0
  4. package/index.js +189 -205
  5. package/package.json +22 -7
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ![npm version](https://badgen.net/npm/v/mikel-cli?labelColor=1d2734&color=21bf81)
4
4
  ![license](https://badgen.net/github/license/jmjuanes/mikel?labelColor=1d2734&color=21bf81)
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 functions.
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
- | `--data <file>` | `-D` | Path to JSON data file |
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`, `*.js` |
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 (let i = 0; i < patterns.length; i++) {
15
- const pattern = patterns[i];
11
+ for (const pattern of patterns) {
16
12
  if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
17
- try {
18
- // use Node.js 24+ built-in fs.glob
19
- // https://nodejs.org/api/fs.html#fspromisesglobpattern-options
20
- for await (const file of fs.glob(pattern, { cwd: process.cwd() })) {
21
- files.push(file);
22
- }
23
- } catch (error) {
24
- files.push(pattern);
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 and resolve to absolute paths
31
- return Array.from(new Set(files)).map(file => {
32
- return path.resolve(process.cwd(), file);
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 (file = null) => {
38
- if (!file) {
85
+ export const loadData = async (root, fileOrObject = null) => {
86
+ if (!fileOrObject) {
39
87
  return {};
40
88
  }
41
- // build the full data file path and check if exists
42
- const dataPath = path.resolve(process.cwd(), file);
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
- const content = await fs.readFile(dataPath, "utf8");
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 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
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 callback(file);
115
+ partials[path.basename(file)] = await fs.readFile(path.resolve(root, file), "utf8");
77
116
  } catch (error) {
78
- throw new Error(`Failed to load '${file}': ${error.message}`);
117
+ throw new Error(`Failed to read partial file '${file}': ${error.message}`);
79
118
  }
80
119
  }
120
+ return partials;
81
121
  };
82
122
 
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);
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 main function
109
- const main = async (input = "", options = {}) => {
110
- // check to print help
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
- // 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
- }
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
- // initialize the template engine
132
- const mikelInstance = mikel.create({});
133
- const data = await loadData(options.data);
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 (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.`);
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
- // load additional partials, helpers, and functions
152
- await loadModules(options.partial, async (file) => {
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
- mikelInstance.addPartial(path.basename(file), await fs.readFile(file, "utf8"));
207
+ template = await fs.readFile(inputPath, "utf8");
155
208
  } catch (error) {
156
- throw new Error(`Failed to read partial file '${file}': ${error.message}`);
209
+ throw new Error(`Failed to read template file '${inputPath}': ${error.message}`);
157
210
  }
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
- });
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
- // 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)) {
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.mkdir(outputDirectory, { recursive: true });
232
+ await fs.writeFile(outputPath, result, "utf8");
233
+ console.error(`✓ Saving '${inputPath}' -> '${outputPath}'`);
203
234
  } catch (error) {
204
- throw new Error(`Failed to create output directory '${outputDirectory}': ${error.message}`);
235
+ throw new Error(`Failed to write output file '${outputPath}': ${error.message}`);
205
236
  }
206
237
  }
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}`);
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
- // 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
- });
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.33.1",
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": "https://github.com/jmjuanes/mikel",
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
- ".": "./index.js",
18
- "./index.js": "./index.js",
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": "index.js"
35
+ "mikel": "cli.js"
23
36
  },
24
37
  "peerDependencies": {
25
- "mikel": "^0.33.1"
38
+ "mikel": "^0.35.0"
26
39
  },
27
40
  "files": [
28
41
  "README.md",
29
- "index.js"
42
+ "cli.js",
43
+ "index.js",
44
+ "index.d.ts"
30
45
  ],
31
46
  "keywords": [
32
47
  "mikel",