@schalkneethling/miyagi-core 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,52 @@ _miyagi_ is a component development tool for JavaScript templating engines.
22
22
 
23
23
  ## Requirements
24
24
 
25
- - NodeJS `20.11.0` or higher
25
+ - NodeJS `24` or higher
26
+
27
+ ## Quick start
28
+
29
+ Twig example:
30
+
31
+ ```bash
32
+ pnpm add -D @schalkneethling/miyagi-core twing
33
+ ```
34
+
35
+ Create `.miyagi.mjs`:
36
+
37
+ ```js
38
+ import { createSynchronousEnvironment, createSynchronousFilesystemLoader } from "twing";
39
+ import fs from "node:fs";
40
+
41
+ const twing = createSynchronousEnvironment(createSynchronousFilesystemLoader(fs));
42
+
43
+ export default {
44
+ components: {
45
+ folder: "src/components",
46
+ },
47
+ docs: {
48
+ folder: "docs",
49
+ },
50
+ engine: {
51
+ async render({ name, context, cb }) {
52
+ try {
53
+ return cb(null, await twing.render(name, context));
54
+ } catch (err) {
55
+ return cb(err.toString());
56
+ }
57
+ },
58
+ },
59
+ };
60
+ ```
61
+
62
+ Then start miyagi:
63
+
64
+ ```bash
65
+ pnpm exec miyagi start
66
+ ```
67
+
68
+ Open `http://localhost:5000`.
69
+
70
+ If you use a different template engine, swap the `engine.render` implementation for that engine.
26
71
 
27
72
  ## Demos
28
73
 
@@ -38,6 +83,8 @@ _miyagi_ is a component development tool for JavaScript templating engines.
38
83
 
39
84
  [https://docs.miyagi.dev](https://docs.miyagi.dev)
40
85
 
86
+ CLI quickstart: [https://docs.miyagi.dev/cli-commands/starting-miyagi/](https://docs.miyagi.dev/cli-commands/starting-miyagi/)
87
+
41
88
  ## Sponsor
42
89
 
43
90
  <a href="https://factorial.io"><img src="https://logo.factorial.io/color.png" width="40" height="56" alt="Factorial"></a>
package/bin/miyagi.js CHANGED
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import "../index.js";
2
+ import runCli from "../index.js";
3
+
4
+ const result = await runCli();
5
+
6
+ if (result?.shouldExit) {
7
+ process.exit(result.code ?? 0);
8
+ }
9
+
10
+ process.exitCode = result?.code ?? 0;
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import Miyagi from "./lib/index.js";
1
+ import { runCli } from "./lib/cli/run.js";
2
2
 
3
- export default await Miyagi();
3
+ export default runCli;
@@ -2,18 +2,24 @@ import generateComponent from "../generator/component.js";
2
2
  import log from "../logger.js";
3
3
  import appConfig from "../default-config.js";
4
4
  import { t } from "../i18n/index.js";
5
+ import { EXIT_CODES } from "../errors.js";
5
6
 
6
7
  /**
7
8
  * @param {object} cliParams
8
- * @returns {Promise<void>}
9
+ * @returns {Promise<object>}
9
10
  */
10
11
  export default async function createComponentViaCli(cliParams) {
11
- const commands = cliParams._.slice(1);
12
+ const commands = [cliParams.component].filter(Boolean);
12
13
 
13
14
  if (commands.length === 0) {
14
- log("error", t("generator.noComponentNameDefined"));
15
-
16
- return;
15
+ const message = t("generator.noComponentNameDefined");
16
+ log("error", message);
17
+ return {
18
+ success: false,
19
+ code: EXIT_CODES.CLI_USAGE_ERROR,
20
+ shouldExit: true,
21
+ message,
22
+ };
17
23
  }
18
24
 
19
25
  const [component] = commands;
@@ -25,8 +31,20 @@ export default async function createComponentViaCli(cliParams) {
25
31
  try {
26
32
  const result = await generateComponent({ component, fileTypes });
27
33
  log("success", result);
34
+ return {
35
+ success: true,
36
+ code: EXIT_CODES.SUCCESS,
37
+ shouldExit: true,
38
+ message: result,
39
+ };
28
40
  } catch (message) {
29
41
  log("error", message);
42
+ return {
43
+ success: false,
44
+ code: EXIT_CODES.GENERAL_ERROR,
45
+ shouldExit: true,
46
+ message: String(message),
47
+ };
30
48
  }
31
49
  }
32
50
 
@@ -0,0 +1,153 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import log from "../logger.js";
4
+ import { EXIT_CODES } from "../errors.js";
5
+ import pkgJson from "../../package.json" with { type: "json" };
6
+
7
+ const CONFIG_FILES = [".miyagi.js", ".miyagi.mjs"];
8
+
9
+ /**
10
+ * @returns {Promise<object>}
11
+ */
12
+ export default async function doctor() {
13
+ let success = true;
14
+
15
+ if (checkNodeVersion()) {
16
+ log("success", `Node.js ${process.versions.node} satisfies ${pkgJson.engines.node}.`);
17
+ } else {
18
+ success = false;
19
+ log(
20
+ "error",
21
+ `Node.js ${process.versions.node} does not satisfy ${pkgJson.engines.node}.`,
22
+ );
23
+ }
24
+
25
+ const configResult = await loadUserConfig();
26
+
27
+ if (!configResult.userFileName) {
28
+ success = false;
29
+ log(
30
+ "error",
31
+ "No miyagi config found. Create .miyagi.js or .miyagi.mjs in your project root.",
32
+ );
33
+ return {
34
+ success,
35
+ code: EXIT_CODES.CONFIG_ERROR,
36
+ shouldExit: true,
37
+ };
38
+ }
39
+
40
+ if (configResult.error) {
41
+ success = false;
42
+ log(
43
+ "error",
44
+ `Could not parse ${configResult.userFileName}. Check its syntax and exports.`,
45
+ );
46
+ log("error", configResult.error.message, configResult.error);
47
+ return {
48
+ success,
49
+ code: EXIT_CODES.CONFIG_ERROR,
50
+ shouldExit: true,
51
+ };
52
+ }
53
+
54
+ log("success", `Loaded config from ${configResult.userFileName}.`);
55
+
56
+ const config = configResult.config || {};
57
+
58
+ if (typeof config.engine?.render === "function") {
59
+ log("success", "engine.render is defined.");
60
+ } else {
61
+ success = false;
62
+ log("error", "engine.render is missing.");
63
+ }
64
+
65
+ if (config.components?.folder || config.docs?.folder) {
66
+ log("success", "At least one source folder is configured.");
67
+ } else {
68
+ success = false;
69
+ log(
70
+ "error",
71
+ "Set at least one of components.folder or docs.folder in your miyagi config.",
72
+ );
73
+ }
74
+
75
+ if (config.components?.folder) {
76
+ if (await pathExists(config.components.folder)) {
77
+ log("success", `components.folder exists: ${config.components.folder}`);
78
+ } else {
79
+ success = false;
80
+ log("error", `components.folder does not exist: ${config.components.folder}`);
81
+ }
82
+ }
83
+
84
+ if (config.docs?.folder) {
85
+ if (await pathExists(config.docs.folder)) {
86
+ log("success", `docs.folder exists: ${config.docs.folder}`);
87
+ } else {
88
+ success = false;
89
+ log("error", `docs.folder does not exist: ${config.docs.folder}`);
90
+ }
91
+ }
92
+
93
+ log(success ? "success" : "error", success ? "Doctor checks passed." : "Doctor found issues.");
94
+
95
+ return {
96
+ success,
97
+ code: success ? EXIT_CODES.SUCCESS : EXIT_CODES.CONFIG_ERROR,
98
+ shouldExit: true,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * @returns {boolean}
104
+ */
105
+ function checkNodeVersion() {
106
+ const minimumMajor = Number(pkgJson.engines.node.match(/\d+/)?.[0] || 0);
107
+ const currentMajor = Number(process.versions.node.split(".")[0]);
108
+ return currentMajor >= minimumMajor;
109
+ }
110
+
111
+ /**
112
+ * @param {string} relativePath
113
+ * @returns {Promise<boolean>}
114
+ */
115
+ async function pathExists(relativePath) {
116
+ try {
117
+ await stat(path.resolve(process.cwd(), relativePath));
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @returns {Promise<object>}
126
+ */
127
+ async function loadUserConfig() {
128
+ for (const fileName of CONFIG_FILES) {
129
+ const fullPath = path.resolve(process.cwd(), fileName);
130
+
131
+ if (!(await pathExists(fileName))) {
132
+ continue;
133
+ }
134
+
135
+ try {
136
+ const module = await import(`${fullPath}?time=${Date.now()}`);
137
+ return {
138
+ userFileName: fileName,
139
+ config: module.default || {},
140
+ };
141
+ } catch (error) {
142
+ return {
143
+ userFileName: fileName,
144
+ error: /** @type {Error} */ (error),
145
+ };
146
+ }
147
+ }
148
+
149
+ return {
150
+ userFileName: null,
151
+ config: null,
152
+ };
153
+ }
@@ -4,6 +4,7 @@ import { readFile, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import yaml from "js-yaml";
6
6
  import log from "../logger.js";
7
+ import { EXIT_CODES } from "../errors.js";
7
8
  import { loadAssetsConfig } from "../drupal/load-assets-config.js";
8
9
  import {
9
10
  parseLibrariesYaml,
@@ -18,27 +19,47 @@ import {
18
19
 
19
20
  /**
20
21
  * @param {object} args - CLI arguments from yargs
22
+ * @returns {Promise<object>}
21
23
  */
22
24
  export default async function drupalAssets(args) {
23
25
  let config;
24
26
  try {
25
27
  config = await loadAssetsConfig(args);
26
28
  } catch (err) {
27
- log("error", /** @type {Error} */ (err).message);
28
- process.exit(1);
29
+ const message = /** @type {Error} */ (err).message;
30
+ log("error", message);
31
+ return {
32
+ success: false,
33
+ code: EXIT_CODES.CONFIG_ERROR,
34
+ shouldExit: true,
35
+ message,
36
+ };
29
37
  }
30
38
 
31
39
  if (!config.libraries) {
32
- log("error", "No libraries file specified. Use --libraries or configure it in .miyagi-assets.js.");
33
- process.exit(1);
40
+ const message =
41
+ "No libraries file specified. Use --libraries or configure it in .miyagi-assets.js.";
42
+ log("error", message);
43
+ return {
44
+ success: false,
45
+ code: EXIT_CODES.CLI_USAGE_ERROR,
46
+ shouldExit: true,
47
+ message,
48
+ };
34
49
  }
35
50
 
36
51
  let yamlContent;
37
52
  try {
38
53
  yamlContent = await readFile(config.libraries, "utf8");
39
54
  } catch {
40
- log("error", `Could not read libraries file: ${config.libraries}`);
41
- process.exit(1);
55
+ const message = `Could not read libraries file: ${config.libraries}`;
56
+ log("error", message);
57
+ return {
58
+ success: false,
59
+ code: EXIT_CODES.CONFIG_ERROR,
60
+ shouldExit: true,
61
+ message,
62
+ };
42
63
  }
43
64
 
44
65
  const librariesMap = parseLibrariesYaml(yamlContent);
@@ -89,8 +110,24 @@ export default async function drupalAssets(args) {
89
110
  }
90
111
 
91
112
  if (!config.dryRun) {
92
- log("success", `Done. Updated ${updatedCount} component(s).`);
113
+ const message = `Done. Updated ${updatedCount} component(s).`;
114
+ log("success", message);
115
+ return {
116
+ success: true,
117
+ code: EXIT_CODES.SUCCESS,
118
+ shouldExit: true,
119
+ message,
120
+ updatedCount,
121
+ };
93
122
  }
123
+
124
+ return {
125
+ success: true,
126
+ code: EXIT_CODES.SUCCESS,
127
+ shouldExit: true,
128
+ message: "Dry run completed.",
129
+ updatedCount,
130
+ };
94
131
  }
95
132
 
96
133
  /**
package/lib/cli/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import lintImport from "./lint.js";
2
2
  import componentImport from "./component.js";
3
3
  import drupalAssetsImport from "./drupal-assets.js";
4
+ import doctorImport from "./doctor.js";
4
5
 
5
6
  export const lint = lintImport;
6
7
  export const component = componentImport;
7
8
  export const drupalAssets = drupalAssetsImport;
9
+ export const doctor = doctorImport;
package/lib/cli/lint.js CHANGED
@@ -10,14 +10,16 @@ import {
10
10
  validateSchemas,
11
11
  } from "../validator/schemas.js";
12
12
  import { t } from "../i18n/index.js";
13
+ import { EXIT_CODES } from "../errors.js";
13
14
 
14
15
  /**
15
16
  * @param {object} args
17
+ * @returns {Promise<object>}
16
18
  */
17
19
  export default async function lint(args) {
18
20
  process.env.NODE_ENV = "development";
19
21
 
20
- const componentArg = args._.slice(1)[0];
22
+ const componentArg = args.component;
21
23
  const config = await getConfig(args);
22
24
  process.env.MIYAGI_LOG_CONTEXT = "lint";
23
25
  process.env.MIYAGI_LOG_LEVEL = config.lint?.logLevel || "error";
@@ -36,26 +38,63 @@ export default async function lint(args) {
36
38
 
37
39
  if (schemaValidation.errors.length > 0) {
38
40
  reportSchemaErrors(schemaValidation.errors);
39
- process.exit(1);
41
+ return {
42
+ success: false,
43
+ code: EXIT_CODES.VALIDATION_ERROR,
44
+ shouldExit: true,
45
+ };
40
46
  }
41
47
 
42
48
  log("success", "All schemas valid.");
43
49
 
44
- await validateComponentMockData({
50
+ return await validateComponentMockData({
45
51
  component,
46
52
  validSchemas: schemaValidation.validSchemas,
47
53
  });
48
54
  } else {
49
- log("error", `The component ${componentArg} does not seem to exist.`);
50
- process.exit(1);
55
+ const message = `The component ${componentArg} does not seem to exist.`;
56
+ log("error", message);
57
+ return {
58
+ success: false,
59
+ code: EXIT_CODES.CLI_USAGE_ERROR,
60
+ shouldExit: true,
61
+ message,
62
+ };
51
63
  }
52
64
  } else {
53
- await validateAllMockData();
65
+ return await validateAllMockData();
54
66
  }
55
67
  }
56
68
 
69
+ /**
70
+ * @param {object} options
71
+ * @param {boolean} options.success
72
+ * @param {boolean} options.shouldExit
73
+ * @param {boolean} [options.valid]
74
+ * @param {string} [options.type]
75
+ * @returns {object}
76
+ */
77
+ function createLintResult({ success, shouldExit, valid, type }) {
78
+ const result = {
79
+ success,
80
+ code: success ? EXIT_CODES.SUCCESS : EXIT_CODES.VALIDATION_ERROR,
81
+ shouldExit,
82
+ };
83
+
84
+ if (valid !== undefined) {
85
+ result.valid = valid;
86
+ }
87
+
88
+ if (type) {
89
+ result.type = type;
90
+ }
91
+
92
+ return result;
93
+ }
94
+
57
95
  /**
58
96
  * @param {boolean} exitProcess
97
+ * @returns {Promise<object>}
59
98
  */
60
99
  async function validateAllMockData(exitProcess = true) {
61
100
  log("info", t("linter.all.start"));
@@ -86,9 +125,17 @@ async function validateAllMockData(exitProcess = true) {
86
125
  ),
87
126
  );
88
127
  if (exitProcess) {
89
- process.exit(1);
128
+ return {
129
+ success: false,
130
+ code: EXIT_CODES.VALIDATION_ERROR,
131
+ shouldExit: true,
132
+ };
90
133
  }
91
- return;
134
+ return {
135
+ success: false,
136
+ code: EXIT_CODES.VALIDATION_ERROR,
137
+ shouldExit: false,
138
+ };
92
139
  }
93
140
 
94
141
  const results = await Promise.all(
@@ -134,15 +181,16 @@ async function validateAllMockData(exitProcess = true) {
134
181
 
135
182
  if (mockInvalidResults.length === 0 && schemaValidation.errors.length === 0) {
136
183
  log("success", t("linter.all.valid"));
137
- if (exitProcess) {
138
- process.exit(0);
139
- }
140
- return;
184
+ return createLintResult({
185
+ success: true,
186
+ shouldExit: exitProcess,
187
+ });
141
188
  }
142
189
 
143
- if (exitProcess) {
144
- process.exit(1);
145
- }
190
+ return createLintResult({
191
+ success: false,
192
+ shouldExit: exitProcess,
193
+ });
146
194
  }
147
195
 
148
196
  /**
@@ -188,23 +236,19 @@ async function validateComponentMockData({
188
236
  log("success", t("linter.component.valid"));
189
237
  }
190
238
 
191
- if (exitProcess) {
192
- process.exit(0);
193
- }
194
-
195
- return {
239
+ return createLintResult({
196
240
  valid: true,
197
- };
198
- }
199
-
200
- if (exitProcess) {
201
- process.exit(1);
241
+ success: true,
242
+ shouldExit: exitProcess,
243
+ });
202
244
  }
203
245
 
204
- return {
246
+ return createLintResult({
205
247
  valid: false,
248
+ success: false,
249
+ shouldExit: exitProcess,
206
250
  type: results[0].type,
207
- };
251
+ });
208
252
  }
209
253
 
210
254
  /**