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.
Files changed (5) hide show
  1. package/README.md +80 -27
  2. package/cli.js +59 -270
  3. package/index.d.ts +19 -0
  4. package/index.js +228 -22
  5. 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
- | `--data <file>` | `-D` | Path to JSON data file |
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`, `*.js` |
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
- rename: {
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
- Where to write the rendered files. Accepts a string (directory path) or an object:
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
- // simple directory
162
- output: "dist/"
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
- The `rename` field works like Jest's `moduleNameMapper` — keys are regular expressions and values are replacement strings. The first matching pattern wins. If no pattern matches, the basename of the input file is used as the output filename.
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 dist/docs/guide/index.html
177
- rename: {
178
- "^src/(.+)\\.mustache$": "$1.html",
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 or a plain object:
191
+ Data to pass to the templates. Accepts a path to a JSON file:
185
192
 
186
193
  ```js
187
- // path to JSON file
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
- // inline object
191
- data: {
192
- site: {
193
- title: "My Site",
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 mikel from "mikel";
8
- import { expandGlobPatterns, applyRename } from "./index.js";
9
-
10
- // load configuration file
11
- const loadConfiguration = async (configurationFile) => {
12
- if (!configurationFile) {
13
- return {};
14
- }
15
- const configurationPath = path.resolve(process.cwd(), configurationFile);
16
- if (!existsSync(configurationPath)) {
17
- throw new Error(`Configuration file '${configurationPath}' was not found.`);
18
- }
19
- // check the extension of the file
20
- const configurationExtension = path.extname(configurationFile);
21
- if (configurationExtension === ".js") {
22
- return await import(configurationPath);
23
- }
24
- else if (configurationExtension === ".json") {
25
- const content = await fs.readFile(configurationPath, "utf8");
26
- return JSON.parse(content);
27
- }
28
- else {
29
- throw new Error(`Unknown extension for configuration file '${configurationFile}'`);
30
- }
31
- };
32
-
33
- // @description loading input files
34
- const loadInput = async (inputFiles) => {
35
- if (!inputFiles || inputFiles?.length === 0) {
36
- throw new Error(`No input templates provided.`);
37
- }
38
- // expand glob patterns
39
- return expandGlobPatterns([inputFiles].flat());
40
- };
41
-
42
- // @description resolve output
43
- const resolveOutput = (inputFile, outputFile, outputConfig) => {
44
- // 1. output file is provided via cli arguments
45
- if (outputFile) {
46
- return path.resolve(process.cwd(), outputFile);
47
- }
48
- // 2. output configuration is provided and it is a string
49
- else if (outputConfig && typeof outputConfig === "string") {
50
- return path.resolve(process.cwd(), path.join(outputConfig, inputFile));
51
- }
52
- // 3. output configuration is provided and it is an object
53
- else if (outputConfig && typeof outputConfig === "object") {
54
- const renamedOutputFile = applyRename(inputFile, outputConfig?.rename || {});
55
- return path.resolve(process.cwd(), path.join(outputConfig?.dir || ".", renamedOutputFile));
56
- }
57
- // 4. other case???
58
- throw new Error(`Unknown error resolving output for template '${inputFile}'`);
59
- };
60
-
61
- // @description load JSON data from the provided path
62
- const loadData = async (fileOrObject = null) => {
63
- if (!fileOrObject) {
64
- return {};
65
- }
66
- // 1. check for object containing data (from config.data)
67
- if (typeof fileOrObject === "object") {
68
- return fileOrObject;
69
- }
70
- // 2. build the full data file path and check if exists
71
- const dataPath = path.resolve(process.cwd(), fileOrObject);
72
- if (!existsSync(dataPath)) {
73
- throw new Error(`Data file '${dataPath}' was not found.`);
74
- }
75
- // 3. read the file and parse it as JSON
76
- try {
77
- const content = await fs.readFile(dataPath, "utf8");
78
- return JSON.parse(content);
79
- } catch (error) {
80
- if (error instanceof SyntaxError) {
81
- throw new Error(`Invalid JSON in data file '${dataPath}': ${error.message}`);
82
- }
83
- throw new Error(`Failed to read data file '${dataPath}': ${error.message}`);
84
- }
85
- };
86
-
87
- // @description load javascript modules
88
- const loadModules = async (patterns = [], callback) => {
89
- const files = await expandGlobPatterns(patterns);
90
-
91
- for (let i = 0; i < files.length; i++) {
92
- const file = files[i]; // path.resolve(process.cwd(), uniqueFiles[i]);
93
- if (!existsSync(file)) {
94
- throw new Error(`File '${file}' was not found.`);
95
- }
96
-
97
- // Check if it's a file (not a directory)
98
- const stats = await fs.stat(file);
99
- if (!stats.isFile()) {
100
- continue; // Skip directories
101
- }
102
-
103
- // import the module
104
- try {
105
- await callback(file);
106
- } catch (error) {
107
- throw new Error(`Failed to load '${file}': ${error.message}`);
108
- }
109
- }
110
- };
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 (inputOption = "", options = {}) => {
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 (options.help) {
79
+ if (values.help) {
141
80
  return printHelp();
142
81
  }
143
82
 
144
- // load configuration file and inputs
145
- const config = await loadConfiguration(options.config);
146
- const inputs = await loadInput(inputOption || config.input || null);
147
- const data = await loadData(options.data || config.data || null);
148
- const mikelInstance = mikel.create({});
149
-
150
- // if no input files were provided, throw an error and stop processing
151
- if (!inputs || inputs?.length === 0) {
152
- throw new Error(`No input templates found`);
83
+ // build with the provided configuration
84
+ try {
85
+ const config = await resolveConfigurationFromArgs(process.cwd(), { positionals, values });
86
+ await build(config);
153
87
  }
154
- // load plugins
155
- const plugins = options.plugin || config.plugins || [];
156
- for (let i = 0; i < plugins.length; i++) {
157
- const pluginName = Array.isArray(plugins[i]) ? plugins[i][0] : plugins[i];
158
- const pluginOptions = Array.isArray(plugins[i]) && plugins[i].length === 2 ? plugins[i][1] : null;
159
- let pluginModule;
160
- try {
161
- // try to import the plugin from node_modules
162
- pluginModule = (await import(pluginName))?.default;
163
- if (typeof pluginModule !== "function") {
164
- throw new Error(`Plugin '${pluginName}' does not export a valid plugin function.`);
165
- }
166
- mikelInstance.use(pluginModule(pluginOptions));
167
- } catch (error) {
168
- throw new Error(`Failed to load plugin '${pluginName}': ${error.message}`);
169
- }
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
- // process arguments
260
- const { positionals, values } = parseArgs({
261
- options: {
262
- config: {
263
- type: "string",
264
- short: "c",
265
- },
266
- data: {
267
- type: "string",
268
- short: "D",
269
- },
270
- output: {
271
- type: "string",
272
- short: "o",
273
- },
274
- helper: {
275
- type: "string",
276
- short: "H",
277
- multiple: true,
278
- },
279
- function: {
280
- type: "string",
281
- short: "F",
282
- multiple: true,
283
- },
284
- partial: {
285
- type: "string",
286
- short: "P",
287
- multiple: true,
288
- },
289
- plugin: {
290
- type: "string",
291
- short: "L",
292
- multiple: true,
293
- },
294
- help: {
295
- type: "boolean",
296
- short: "h",
297
- },
298
- },
299
- allowPositionals: true,
300
- });
301
-
302
- // run main script
303
- main(positionals[0], values).catch(error => {
304
- console.error(`\n❌ Error: ${error.message}\n`);
305
- process.exit(1);
306
- });
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 (let i = 0; i < patterns.length; i++) {
10
- const pattern = patterns[i];
11
+ for (const pattern of patterns) {
11
12
  if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
12
- try {
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: process.cwd() })) {
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 and resolve to absolute paths
26
- return Array.from(new Set(files)).map(file => {
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 applyRename = (filePath, rename = {}) => {
34
- const patterns = Object.keys(rename);
35
- for (let i = 0; i < patterns.length; i++) {
36
- const regex = new RegExp(patterns[i]);
37
- if (regex.test(filePath)) {
38
- return filePath.replace(regex, rename[patterns[i]]);
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(filePath);
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.34.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
- ".": "./index.js",
24
- "./index.js": "./index.js",
25
- "./cli.js": "./cli.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
+ },
26
32
  "./package.json": "./package.json"
27
33
  },
28
34
  "bin": {
29
35
  "mikel": "cli.js"
30
36
  },
31
37
  "peerDependencies": {
32
- "mikel": "^0.34.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",