@caleuche/cli 0.1.3 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @caleuche/cli
2
2
 
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - a471981: CLI now properly handles errors.
8
+
3
9
  ## 0.1.3
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1,103 +1,102 @@
1
- # Caleuche CLI
2
-
3
- Caleuche CLI is a command-line tool for compiling code samples and generating project files from templates. It supports multiple languages and flexible sample definitions, including inline templates and external template files.
4
-
5
- ## Installation
6
-
7
- ```sh
8
- npm install @caleuche/cli
9
- ```
10
-
11
- ## Usage
12
-
13
- ```
14
- che compile <sample-directory|sample-file> <data-file> <output-directory> [options]
15
- ```
16
-
17
- - `<sample-directory|sample-file>`: Path to a directory containing a `sample.yaml` or a direct path to a sample YAML file.
18
- - `<data-file>`: Path to the data file (JSON or YAML).
19
- - `<output-directory>`: Directory where the generated project will be placed.
20
-
21
- ### Options
22
-
23
- - `-p, --project` Generate project file (e.g., csproj, go.mod, etc.)
24
-
25
- ## Examples
26
-
27
- ### 1. Sample with Inline Template
28
-
29
- **sample.yaml**
30
-
31
- ```yaml
32
- template: |
33
- Hello, <%= name %>!
34
- type: python
35
- dependencies: []
36
- input:
37
- - name: name
38
- type: string
39
- required: true
40
- ```
41
-
42
- **data.yaml**
43
-
44
- ```yaml
45
- name: World
46
- ```
47
-
48
- **Command:**
49
-
50
- ```sh
51
- che compile ./my-sample ./data.yaml ./output
52
- ```
53
-
54
- ### 2. Sample with Template File Reference
55
-
56
- **Directory structure:**
57
-
58
- ```
59
- my-sample/
60
- sample.yaml
61
- main.py.tmpl
62
- ```
63
-
64
- **sample.yaml**
65
-
66
- ```yaml
67
- template: main.py.tmpl
68
- type: python
69
- dependencies: []
70
- input:
71
- - name: name
72
- type: string
73
- required: true
74
- ```
75
-
76
- **main.py.tmpl**
77
-
78
- ```python
79
- print("Hello, <%= name %>!")
80
- ```
81
-
82
- **data.yaml**
83
-
84
- ```yaml
85
- name: Alice
86
- ```
87
-
88
- **Command:**
89
-
90
- ```sh
91
- che compile ./my-sample ./data.yaml ./output
92
- ```
93
-
94
- ## Sample and Data File Structure
95
-
96
- - **Sample file**: YAML describing the sample, including the template (inline or file reference), language, dependencies, and input fields.
97
-
98
- - **Data file**: JSON or YAML with the data to inject into the sample.
99
-
100
-
101
- ## License
102
-
103
- MIT
1
+ # Caleuche CLI
2
+
3
+ Caleuche CLI is a command-line tool for compiling code samples and generating project files from templates. It supports multiple languages and flexible sample definitions, including inline templates and external template files.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @caleuche/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ che compile <sample-directory|sample-file> <data-file> <output-directory> [options]
15
+ ```
16
+
17
+ - `<sample-directory|sample-file>`: Path to a directory containing a `sample.yaml` or a direct path to a sample YAML file.
18
+ - `<data-file>`: Path to the data file (JSON or YAML).
19
+ - `<output-directory>`: Directory where the generated project will be placed.
20
+
21
+ ### Options
22
+
23
+ - `-p, --project` Generate project file (e.g., csproj, go.mod, etc.)
24
+
25
+ ## Examples
26
+
27
+ ### 1. Sample with Inline Template
28
+
29
+ **sample.yaml**
30
+
31
+ ```yaml
32
+ template: |
33
+ Hello, <%= name %>!
34
+ type: python
35
+ dependencies: []
36
+ input:
37
+ - name: name
38
+ type: string
39
+ required: true
40
+ ```
41
+
42
+ **data.yaml**
43
+
44
+ ```yaml
45
+ name: World
46
+ ```
47
+
48
+ **Command:**
49
+
50
+ ```sh
51
+ che compile ./my-sample ./data.yaml ./output
52
+ ```
53
+
54
+ ### 2. Sample with Template File Reference
55
+
56
+ **Directory structure:**
57
+
58
+ ```
59
+ my-sample/
60
+ sample.yaml
61
+ main.py.tmpl
62
+ ```
63
+
64
+ **sample.yaml**
65
+
66
+ ```yaml
67
+ template: main.py.tmpl
68
+ type: python
69
+ dependencies: []
70
+ input:
71
+ - name: name
72
+ type: string
73
+ required: true
74
+ ```
75
+
76
+ **main.py.tmpl**
77
+
78
+ ```python
79
+ print("Hello, <%= name %>!")
80
+ ```
81
+
82
+ **data.yaml**
83
+
84
+ ```yaml
85
+ name: Alice
86
+ ```
87
+
88
+ **Command:**
89
+
90
+ ```sh
91
+ che compile ./my-sample ./data.yaml ./output
92
+ ```
93
+
94
+ ## Sample and Data File Structure
95
+
96
+ - **Sample file**: YAML describing the sample, including the template (inline or file reference), language, dependencies, and input fields.
97
+
98
+ - **Data file**: JSON or YAML with the data to inject into the sample.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compile = compile;
7
+ const core_1 = require("@caleuche/core");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const utils_1 = require("./utils");
11
+ function compile(samplePath, dataPath, outputPath, options) {
12
+ const sampleFilePath = (0, utils_1.resolveSampleFile)(samplePath);
13
+ if (!fs_1.default.existsSync(sampleFilePath)) {
14
+ console.error(`Sample file not found: ${sampleFilePath}`);
15
+ process.exit(1);
16
+ }
17
+ const sample = (0, utils_1.parse)(sampleFilePath);
18
+ if (!sample) {
19
+ console.error(`Failed to parse sample file: ${sampleFilePath}`);
20
+ process.exit(1);
21
+ }
22
+ sample.template = (0, utils_1.resolveTemplate)(samplePath, sample);
23
+ const inputData = (0, utils_1.parse)(dataPath);
24
+ if (!inputData || !(0, utils_1.isObject)(inputData)) {
25
+ console.error(`Failed to parse input data file: ${dataPath}`);
26
+ process.exit(1);
27
+ }
28
+ const output = (() => {
29
+ try {
30
+ return (0, core_1.compileSample)(sample, inputData, {
31
+ project: options.project || false,
32
+ });
33
+ }
34
+ catch (error) {
35
+ if (error instanceof Error) {
36
+ console.error(`Error during compilation: ${error.message}`);
37
+ }
38
+ else {
39
+ console.error("An unknown error occurred during compilation.");
40
+ }
41
+ return null;
42
+ }
43
+ })();
44
+ if (!output) {
45
+ process.exit(1);
46
+ }
47
+ if (!(0, utils_1.isObject)(inputData)) {
48
+ console.error("Input data must be an object.");
49
+ process.exit(1);
50
+ }
51
+ (0, utils_1.createOutputDirectory)(outputPath);
52
+ for (const { fileName, content } of output.items) {
53
+ const itemOutputPath = path_1.default.join(outputPath, fileName);
54
+ fs_1.default.writeFileSync(itemOutputPath, content);
55
+ }
56
+ }
package/dist/index.js CHANGED
@@ -1,50 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
4
  const commander_1 = require("commander");
8
- const core_1 = require("@caleuche/core");
9
- const fs_1 = __importDefault(require("fs"));
10
- const path_1 = __importDefault(require("path"));
11
- const utils_1 = require("./utils");
5
+ const compile_1 = require("./compile");
6
+ const package_json_1 = require("../package.json");
12
7
  commander_1.program
13
8
  .name("@caleuche/cli")
14
9
  .description("Caleuche CLI for compiling samples")
15
- .version("1.0.0");
16
- function compile(samplePath, dataPath, outputPath, options) {
17
- const sampleFilePath = (0, utils_1.resolveSampleFile)(samplePath);
18
- if (!fs_1.default.existsSync(sampleFilePath)) {
19
- console.error(`Sample file not found: ${sampleFilePath}`);
20
- process.exit(1);
21
- }
22
- const sample = (0, utils_1.parse)(sampleFilePath);
23
- if (!sample) {
24
- console.error(`Failed to parse sample file: ${sampleFilePath}`);
25
- process.exit(1);
26
- }
27
- sample.template = (0, utils_1.resolveTemplate)(samplePath, sample);
28
- const inputData = (0, utils_1.parse)(dataPath);
29
- if (!inputData || !(0, utils_1.isObject)(inputData)) {
30
- console.error(`Failed to parse input data file: ${dataPath}`);
31
- process.exit(1);
32
- }
33
- const output = (0, core_1.compileSample)(sample, inputData, {
34
- project: options.project || false,
35
- });
36
- if (!(0, utils_1.isObject)(inputData)) {
37
- console.error("Input data must be an object.");
38
- process.exit(1);
39
- }
40
- (0, utils_1.createOutputDirectory)(outputPath);
41
- for (const { fileName, content } of output.items) {
42
- const itemOutputPath = path_1.default.join(outputPath, fileName);
43
- fs_1.default.writeFileSync(itemOutputPath, content);
44
- }
45
- }
10
+ .version(package_json_1.version);
46
11
  commander_1.program
47
12
  .command("compile <sample-directory> <data-file> <output-directory>")
48
13
  .option("-p, --project", "Generate project file")
49
- .action(compile);
14
+ .action(compile_1.compile);
50
15
  commander_1.program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caleuche/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "che": "dist/index.js"
@@ -11,7 +11,9 @@
11
11
  "build:clean": "rimraf dist && tsc",
12
12
  "build:clean:watch": "rimraf dist && tsc --watch",
13
13
  "clean": "rimraf dist",
14
- "format": "prettier --write ."
14
+ "format": "prettier --write .",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest"
15
17
  },
16
18
  "author": "",
17
19
  "license": "MIT",
@@ -25,10 +27,8 @@
25
27
  "@types/node": "^24.0.0",
26
28
  "prettier": "^3.5.3",
27
29
  "rimraf": "^6.0.1",
28
- "typescript": "^5.8.3"
29
- },
30
- "publishConfig": {
31
- "access": "public"
30
+ "typescript": "^5.8.3",
31
+ "vitest": "^3.2.3"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/compile.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { compileSample, Sample } from "@caleuche/core";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ createOutputDirectory,
6
+ isObject,
7
+ parse,
8
+ resolveSampleFile,
9
+ resolveTemplate,
10
+ } from "./utils";
11
+
12
+ export function compile(
13
+ samplePath: string,
14
+ dataPath: string,
15
+ outputPath: string,
16
+ options: { project?: boolean },
17
+ ) {
18
+ const sampleFilePath = resolveSampleFile(samplePath);
19
+ if (!fs.existsSync(sampleFilePath)) {
20
+ console.error(`Sample file not found: ${sampleFilePath}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const sample = parse<Sample>(sampleFilePath);
25
+ if (!sample) {
26
+ console.error(`Failed to parse sample file: ${sampleFilePath}`);
27
+ process.exit(1);
28
+ }
29
+ sample.template = resolveTemplate(samplePath, sample);
30
+
31
+ const inputData = parse<Record<string, any>>(dataPath);
32
+ if (!inputData || !isObject(inputData)) {
33
+ console.error(`Failed to parse input data file: ${dataPath}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const output = (() => {
38
+ try {
39
+ return compileSample(sample, inputData, {
40
+ project: options.project || false,
41
+ });
42
+ } catch (error) {
43
+ if (error instanceof Error) {
44
+ console.error(`Error during compilation: ${error.message}`);
45
+ } else {
46
+ console.error("An unknown error occurred during compilation.");
47
+ }
48
+ return null;
49
+ }
50
+ })();
51
+
52
+ if (!output) {
53
+ process.exit(1);
54
+ }
55
+
56
+ if (!isObject(inputData)) {
57
+ console.error("Input data must be an object.");
58
+ process.exit(1);
59
+ }
60
+
61
+ createOutputDirectory(outputPath);
62
+
63
+ for (const { fileName, content } of output.items) {
64
+ const itemOutputPath = path.join(outputPath, fileName);
65
+ fs.writeFileSync(itemOutputPath, content);
66
+ }
67
+ }
package/src/index.ts CHANGED
@@ -1,67 +1,17 @@
1
- #!/usr/bin/env node
2
-
3
- import { program } from "commander";
4
- import { compileSample, Sample } from "@caleuche/core";
5
- import fs from "fs";
6
- import path from "path";
7
- import {
8
- createOutputDirectory,
9
- isObject,
10
- parse,
11
- resolveSampleFile,
12
- resolveTemplate,
13
- } from "./utils";
14
-
15
- program
16
- .name("@caleuche/cli")
17
- .description("Caleuche CLI for compiling samples")
18
- .version("1.0.0");
19
-
20
- function compile(
21
- samplePath: string,
22
- dataPath: string,
23
- outputPath: string,
24
- options: { project?: boolean },
25
- ) {
26
- const sampleFilePath = resolveSampleFile(samplePath);
27
- if (!fs.existsSync(sampleFilePath)) {
28
- console.error(`Sample file not found: ${sampleFilePath}`);
29
- process.exit(1);
30
- }
31
-
32
- const sample = parse<Sample>(sampleFilePath);
33
- if (!sample) {
34
- console.error(`Failed to parse sample file: ${sampleFilePath}`);
35
- process.exit(1);
36
- }
37
- sample.template = resolveTemplate(samplePath, sample);
38
-
39
- const inputData = parse<Record<string, any>>(dataPath);
40
- if (!inputData || !isObject(inputData)) {
41
- console.error(`Failed to parse input data file: ${dataPath}`);
42
- process.exit(1);
43
- }
44
-
45
- const output = compileSample(sample, inputData, {
46
- project: options.project || false,
47
- });
48
-
49
- if (!isObject(inputData)) {
50
- console.error("Input data must be an object.");
51
- process.exit(1);
52
- }
53
-
54
- createOutputDirectory(outputPath);
55
-
56
- for (const { fileName, content } of output.items) {
57
- const itemOutputPath = path.join(outputPath, fileName);
58
- fs.writeFileSync(itemOutputPath, content);
59
- }
60
- }
61
-
62
- program
63
- .command("compile <sample-directory> <data-file> <output-directory>")
64
- .option("-p, --project", "Generate project file")
65
- .action(compile);
66
-
67
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import { compile } from "./compile";
5
+ import { version } from "../package.json";
6
+
7
+ program
8
+ .name("@caleuche/cli")
9
+ .description("Caleuche CLI for compiling samples")
10
+ .version(version);
11
+
12
+ program
13
+ .command("compile <sample-directory> <data-file> <output-directory>")
14
+ .option("-p, --project", "Generate project file")
15
+ .action(compile);
16
+
17
+ program.parse();
package/src/utils.ts CHANGED
@@ -1,50 +1,50 @@
1
- import { parse as parseYaml } from "yaml";
2
- import fs from "fs";
3
- import path from "path";
4
- import { Sample } from "@caleuche/core";
5
-
6
- export function parse<T>(filePath: string): T | null {
7
- try {
8
- const fileContent = fs.readFileSync(filePath, "utf-8");
9
- if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
10
- return parseYaml(fileContent) as T;
11
- }
12
- return JSON.parse(fileContent) as T;
13
- } catch (error) {
14
- return null;
15
- }
16
- }
17
-
18
- export function resolveSampleFile(samplePath: string): string {
19
- if (isDirectory(samplePath)) {
20
- return path.join(samplePath, "sample.yaml");
21
- }
22
- return samplePath;
23
- }
24
-
25
- export function isDirectory(path: string): boolean {
26
- return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
27
- }
28
-
29
- export function createOutputDirectory(outputPath: string) {
30
- if (!fs.existsSync(outputPath)) {
31
- fs.mkdirSync(outputPath, { recursive: true });
32
- }
33
- }
34
-
35
- export function resolveTemplate(samplePath: string, sample: Sample): string {
36
- try {
37
- const templatePath = path.join(samplePath, sample.template);
38
- if (!fs.existsSync(templatePath)) {
39
- return sample.template;
40
- }
41
- return fs.readFileSync(templatePath, "utf-8");
42
- } catch (error) {
43
- console.error("Error reading template file.");
44
- process.exit(1);
45
- }
46
- }
47
-
48
- export function isObject(value: any): value is Record<string, any> {
49
- return value !== null && typeof value === "object" && !Array.isArray(value);
50
- }
1
+ import { parse as parseYaml } from "yaml";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { Sample } from "@caleuche/core";
5
+
6
+ export function parse<T>(filePath: string): T | null {
7
+ try {
8
+ const fileContent = fs.readFileSync(filePath, "utf-8");
9
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
10
+ return parseYaml(fileContent) as T;
11
+ }
12
+ return JSON.parse(fileContent) as T;
13
+ } catch (error) {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ export function resolveSampleFile(samplePath: string): string {
19
+ if (isDirectory(samplePath)) {
20
+ return path.join(samplePath, "sample.yaml");
21
+ }
22
+ return samplePath;
23
+ }
24
+
25
+ export function isDirectory(path: string): boolean {
26
+ return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
27
+ }
28
+
29
+ export function createOutputDirectory(outputPath: string) {
30
+ if (!fs.existsSync(outputPath)) {
31
+ fs.mkdirSync(outputPath, { recursive: true });
32
+ }
33
+ }
34
+
35
+ export function resolveTemplate(samplePath: string, sample: Sample): string {
36
+ try {
37
+ const templatePath = path.join(samplePath, sample.template);
38
+ if (!fs.existsSync(templatePath)) {
39
+ return sample.template;
40
+ }
41
+ return fs.readFileSync(templatePath, "utf-8");
42
+ } catch (error) {
43
+ console.error("Error reading template file.");
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ export function isObject(value: any): value is Record<string, any> {
49
+ return value !== null && typeof value === "object" && !Array.isArray(value);
50
+ }
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import path from "path";
3
+
4
+ vi.mock("fs");
5
+ import fs from "fs";
6
+ const mockFs = vi.mocked(fs);
7
+
8
+ vi.mock("@caleuche/core");
9
+ import { compileSample, Sample } from "@caleuche/core";
10
+ const mockCompileSample = vi.mocked(compileSample);
11
+
12
+ vi.mock("../src/utils");
13
+ import {
14
+ parse,
15
+ resolveSampleFile,
16
+ createOutputDirectory,
17
+ resolveTemplate,
18
+ isObject,
19
+ } from "../src/utils";
20
+ const mockParse = vi.mocked(parse);
21
+ const mockResolveSampleFile = vi.mocked(resolveSampleFile);
22
+ const mockCreateOutputDirectory = vi.mocked(createOutputDirectory);
23
+ const mockResolveTemplate = vi.mocked(resolveTemplate);
24
+ const mockIsObject = vi.mocked(isObject);
25
+
26
+ import { compile } from "../src/compile";
27
+
28
+ describe("compile", () => {
29
+ let mockExit: any;
30
+ let mockConsoleError: any;
31
+
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
35
+ throw new Error("process.exit");
36
+ });
37
+ mockConsoleError = vi.spyOn(console, "error").mockImplementation(() => {});
38
+ });
39
+
40
+ afterEach(() => {
41
+ vi.restoreAllMocks();
42
+ });
43
+
44
+ describe("error handling", () => {
45
+ it("should exit when sample file does not exist", () => {
46
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
47
+ mockFs.existsSync.mockReturnValue(false);
48
+
49
+ expect(() => {
50
+ compile("sample", "data.json", "output", {});
51
+ }).toThrow("process.exit");
52
+
53
+ expect(mockConsoleError).toHaveBeenCalledWith(
54
+ "Sample file not found: /path/to/sample.yaml",
55
+ );
56
+ expect(mockExit).toHaveBeenCalledWith(1);
57
+ });
58
+
59
+ it("should exit when sample file cannot be parsed", () => {
60
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
61
+ mockFs.existsSync.mockReturnValue(true);
62
+ mockParse.mockReturnValueOnce(null);
63
+
64
+ expect(() => {
65
+ compile("sample", "data.json", "output", {});
66
+ }).toThrow("process.exit");
67
+
68
+ expect(mockConsoleError).toHaveBeenCalledWith(
69
+ "Failed to parse sample file: /path/to/sample.yaml",
70
+ );
71
+ expect(mockExit).toHaveBeenCalledWith(1);
72
+ });
73
+
74
+ it("should exit when data file cannot be parsed", () => {
75
+ const mockSample: Sample = {
76
+ template: "test template",
77
+ type: "javascript",
78
+ dependencies: [],
79
+ input: [],
80
+ };
81
+
82
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
83
+ mockFs.existsSync.mockReturnValue(true);
84
+ mockParse.mockReturnValueOnce(mockSample);
85
+ mockResolveTemplate.mockReturnValue("resolved template");
86
+ mockParse.mockReturnValueOnce(null);
87
+
88
+ expect(() => {
89
+ compile("sample", "data.json", "output", {});
90
+ }).toThrow("process.exit");
91
+
92
+ expect(mockConsoleError).toHaveBeenCalledWith(
93
+ "Failed to parse input data file: data.json",
94
+ );
95
+ expect(mockExit).toHaveBeenCalledWith(1);
96
+ });
97
+
98
+ it("should exit when data is not an object", () => {
99
+ const mockSample: Sample = {
100
+ template: "test template",
101
+ type: "javascript",
102
+ dependencies: [],
103
+ input: [],
104
+ };
105
+
106
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
107
+ mockFs.existsSync.mockReturnValue(true);
108
+ mockParse.mockReturnValueOnce(mockSample);
109
+ mockResolveTemplate.mockReturnValue("resolved template");
110
+ mockParse.mockReturnValueOnce("not an object");
111
+ mockIsObject.mockReturnValue(false);
112
+
113
+ expect(() => {
114
+ compile("sample", "data.json", "output", {});
115
+ }).toThrow("process.exit");
116
+
117
+ expect(mockConsoleError).toHaveBeenCalledWith(
118
+ "Failed to parse input data file: data.json",
119
+ );
120
+ expect(mockExit).toHaveBeenCalledWith(1);
121
+ });
122
+
123
+ it("should exit when compilation fails", () => {
124
+ const mockSample: Sample = {
125
+ template: "test template",
126
+ type: "javascript",
127
+ dependencies: [],
128
+ input: [],
129
+ };
130
+ const mockData = { name: "test" };
131
+
132
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
133
+ mockFs.existsSync.mockReturnValue(true);
134
+ mockParse.mockReturnValueOnce(mockSample);
135
+ mockResolveTemplate.mockReturnValue("resolved template");
136
+ mockParse.mockReturnValueOnce(mockData);
137
+ mockIsObject.mockReturnValue(true);
138
+ mockCompileSample.mockImplementation(() => {
139
+ throw new Error("Compilation error");
140
+ });
141
+
142
+ expect(() => {
143
+ compile("sample", "data.json", "output", {});
144
+ }).toThrow("process.exit");
145
+
146
+ expect(mockConsoleError).toHaveBeenCalledWith(
147
+ "Error during compilation: Compilation error",
148
+ );
149
+ expect(mockExit).toHaveBeenCalledWith(1);
150
+ });
151
+
152
+ it("should handle unknown compilation errors", () => {
153
+ const mockSample: Sample = {
154
+ template: "test template",
155
+ type: "javascript",
156
+ dependencies: [],
157
+ input: [],
158
+ };
159
+ const mockData = { name: "test" };
160
+
161
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
162
+ mockFs.existsSync.mockReturnValue(true);
163
+ mockParse.mockReturnValueOnce(mockSample);
164
+ mockResolveTemplate.mockReturnValue("resolved template");
165
+ mockParse.mockReturnValueOnce(mockData);
166
+ mockIsObject.mockReturnValue(true);
167
+ mockCompileSample.mockImplementation(() => {
168
+ throw "Unknown error";
169
+ });
170
+
171
+ expect(() => {
172
+ compile("sample", "data.json", "output", {});
173
+ }).toThrow("process.exit");
174
+
175
+ expect(mockConsoleError).toHaveBeenCalledWith(
176
+ "An unknown error occurred during compilation.",
177
+ );
178
+ expect(mockExit).toHaveBeenCalledWith(1);
179
+ });
180
+ });
181
+
182
+ describe("successful compilation", () => {
183
+ it("should compile successfully and write output files", () => {
184
+ const mockSample: Sample = {
185
+ template: "test template",
186
+ type: "javascript",
187
+ dependencies: [],
188
+ input: [],
189
+ };
190
+ const mockData = { name: "test" };
191
+ const mockOutput = {
192
+ items: [
193
+ { fileName: "sample.js", content: "console.log('test');" },
194
+ { fileName: "package.json", content: '{"name": "test"}' },
195
+ ],
196
+ };
197
+
198
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
199
+ mockFs.existsSync.mockReturnValue(true);
200
+ mockParse.mockReturnValueOnce(mockSample);
201
+ mockResolveTemplate.mockReturnValue("resolved template");
202
+ mockParse.mockReturnValueOnce(mockData);
203
+ mockIsObject.mockReturnValue(true);
204
+ mockCompileSample.mockReturnValue(mockOutput);
205
+ mockCreateOutputDirectory.mockImplementation(() => {});
206
+ mockFs.writeFileSync.mockImplementation(() => {});
207
+
208
+ // This should not throw
209
+ compile("sample", "data.json", "output", { project: true });
210
+
211
+ expect(mockCreateOutputDirectory).toHaveBeenCalledWith("output");
212
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
213
+ path.join("output", "sample.js"),
214
+ "console.log('test');",
215
+ );
216
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
217
+ path.join("output", "package.json"),
218
+ '{"name": "test"}',
219
+ );
220
+ });
221
+
222
+ it("should pass correct options to compileSample", () => {
223
+ const mockSample: Sample = {
224
+ template: "test template",
225
+ type: "javascript",
226
+ dependencies: [],
227
+ input: [],
228
+ };
229
+ const mockData = { name: "test" };
230
+ const mockOutput = {
231
+ items: [{ fileName: "sample.js", content: "console.log('test');" }],
232
+ };
233
+
234
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
235
+ mockFs.existsSync.mockReturnValue(true);
236
+ mockParse.mockReturnValueOnce(mockSample);
237
+ mockResolveTemplate.mockReturnValue("resolved template");
238
+ mockParse.mockReturnValueOnce(mockData);
239
+ mockIsObject.mockReturnValue(true);
240
+ mockCompileSample.mockReturnValue(mockOutput);
241
+ mockCreateOutputDirectory.mockImplementation(() => {});
242
+ mockFs.writeFileSync.mockImplementation(() => {});
243
+
244
+ compile("sample", "data.json", "output", { project: true });
245
+
246
+ expect(mockCompileSample).toHaveBeenCalledWith(
247
+ { ...mockSample, template: "resolved template" },
248
+ mockData,
249
+ { project: true },
250
+ );
251
+ });
252
+
253
+ it("should default project option to false", () => {
254
+ const mockSample: Sample = {
255
+ template: "test template",
256
+ type: "javascript",
257
+ dependencies: [],
258
+ input: [],
259
+ };
260
+ const mockData = { name: "test" };
261
+ const mockOutput = {
262
+ items: [{ fileName: "sample.js", content: "console.log('test');" }],
263
+ };
264
+
265
+ mockResolveSampleFile.mockReturnValue("/path/to/sample.yaml");
266
+ mockFs.existsSync.mockReturnValue(true);
267
+ mockParse.mockReturnValueOnce(mockSample);
268
+ mockResolveTemplate.mockReturnValue("resolved template");
269
+ mockParse.mockReturnValueOnce(mockData);
270
+ mockIsObject.mockReturnValue(true);
271
+ mockCompileSample.mockReturnValue(mockOutput);
272
+ mockCreateOutputDirectory.mockImplementation(() => {});
273
+ mockFs.writeFileSync.mockImplementation(() => {});
274
+
275
+ compile("sample", "data.json", "output", {});
276
+
277
+ expect(mockCompileSample).toHaveBeenCalledWith(
278
+ { ...mockSample, template: "resolved template" },
279
+ mockData,
280
+ { project: false },
281
+ );
282
+ });
283
+ });
284
+ });
@@ -0,0 +1,267 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { tmpdir } from "os";
5
+ import {
6
+ parse,
7
+ resolveSampleFile,
8
+ isDirectory,
9
+ createOutputDirectory,
10
+ resolveTemplate,
11
+ isObject,
12
+ } from "../src/utils";
13
+ import { Sample } from "@caleuche/core";
14
+
15
+ function multiline(strings: TemplateStringsArray, ...values: any[]) {
16
+ let result = strings[0];
17
+ for (let i = 0; i < values.length; i++) {
18
+ result += values[i] + strings[i + 1];
19
+ }
20
+
21
+ const lines = result.split("\n");
22
+
23
+ if (lines[0].trim() === "") lines.shift();
24
+ if (lines[lines.length - 1].trim() === "") lines.pop();
25
+
26
+ const nonEmptyLines = lines.filter((line) => line.trim() !== "");
27
+ if (nonEmptyLines.length === 0) return "";
28
+
29
+ const minIndent = Math.min(
30
+ ...nonEmptyLines.map((line) => line.match(/^ */)?.[0].length || 0),
31
+ );
32
+
33
+ return lines.map((line) => line.slice(minIndent)).join("\n");
34
+ }
35
+
36
+ describe("utils", () => {
37
+ let tempDir: string;
38
+
39
+ beforeEach(() => {
40
+ tempDir = fs.mkdtempSync(path.join(tmpdir(), "caleuche-cli-test-"));
41
+ });
42
+
43
+ afterEach(() => {
44
+ if (fs.existsSync(tempDir)) {
45
+ fs.rmSync(tempDir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ describe("parse", () => {
50
+ it("should parse valid JSON file", () => {
51
+ const jsonFile = path.join(tempDir, "test.json");
52
+ const testData = { name: "test", value: 42 };
53
+ fs.writeFileSync(jsonFile, JSON.stringify(testData));
54
+
55
+ const result = parse(jsonFile);
56
+ expect(result).toEqual(testData);
57
+ });
58
+
59
+ it("should parse valid YAML file", () => {
60
+ const yamlFile = path.join(tempDir, "test.yaml");
61
+ const yamlContent = multiline`
62
+ name: test
63
+ value: 42`;
64
+ fs.writeFileSync(yamlFile, yamlContent);
65
+
66
+ const result = parse(yamlFile);
67
+ expect(result).toEqual({ name: "test", value: 42 });
68
+ });
69
+
70
+ it("should parse valid YML file", () => {
71
+ const ymlFile = path.join(tempDir, "test.yml");
72
+ const ymlContent = multiline`
73
+ name: test
74
+ value: 42`;
75
+ fs.writeFileSync(ymlFile, ymlContent);
76
+
77
+ const result = parse(ymlFile);
78
+ expect(result).toEqual({ name: "test", value: 42 });
79
+ });
80
+
81
+ it("should return null for invalid JSON", () => {
82
+ const invalidJsonFile = path.join(tempDir, "invalid.json");
83
+ fs.writeFileSync(invalidJsonFile, "{ invalid json }");
84
+
85
+ const result = parse(invalidJsonFile);
86
+ expect(result).toBeNull();
87
+ });
88
+
89
+ it("should return null for invalid YAML", () => {
90
+ const invalidYamlFile = path.join(tempDir, "invalid.yaml");
91
+ fs.writeFileSync(
92
+ invalidYamlFile,
93
+ multiline`
94
+ invalid: yaml: structure:
95
+ broken
96
+ `,
97
+ );
98
+
99
+ const result = parse(invalidYamlFile);
100
+ expect(result).toBeNull();
101
+ });
102
+
103
+ it("should return null for non-existent file", () => {
104
+ const nonExistentFile = path.join(tempDir, "nonexistent.json");
105
+ const result = parse(nonExistentFile);
106
+ expect(result).toBeNull();
107
+ });
108
+ });
109
+
110
+ describe("resolveSampleFile", () => {
111
+ it("should return the file path if it's a file", () => {
112
+ const filePath = path.join(tempDir, "sample.yaml");
113
+ fs.writeFileSync(filePath, "test content");
114
+
115
+ const result = resolveSampleFile(filePath);
116
+ expect(result).toBe(filePath);
117
+ });
118
+
119
+ it("should return sample.yaml path if input is a directory", () => {
120
+ const dirPath = path.join(tempDir, "sample-dir");
121
+ fs.mkdirSync(dirPath);
122
+
123
+ const result = resolveSampleFile(dirPath);
124
+ expect(result).toBe(path.join(dirPath, "sample.yaml"));
125
+ });
126
+
127
+ it("should return the path as-is if it doesn't exist", () => {
128
+ const nonExistentPath = path.join(tempDir, "nonexistent");
129
+
130
+ const result = resolveSampleFile(nonExistentPath);
131
+ expect(result).toBe(nonExistentPath);
132
+ });
133
+ });
134
+
135
+ describe("isDirectory", () => {
136
+ it("should return true for directories", () => {
137
+ const dirPath = path.join(tempDir, "testdir");
138
+ fs.mkdirSync(dirPath);
139
+
140
+ expect(isDirectory(dirPath)).toBe(true);
141
+ });
142
+
143
+ it("should return false for files", () => {
144
+ const filePath = path.join(tempDir, "testfile.txt");
145
+ fs.writeFileSync(filePath, "test content");
146
+
147
+ expect(isDirectory(filePath)).toBe(false);
148
+ });
149
+
150
+ it("should return false for non-existent paths", () => {
151
+ const nonExistentPath = path.join(tempDir, "nonexistent");
152
+ expect(isDirectory(nonExistentPath)).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe("createOutputDirectory", () => {
157
+ it("should create directory if it doesn't exist", () => {
158
+ const newDir = path.join(tempDir, "new-output");
159
+ expect(fs.existsSync(newDir)).toBe(false);
160
+
161
+ createOutputDirectory(newDir);
162
+ expect(fs.existsSync(newDir)).toBe(true);
163
+ expect(fs.lstatSync(newDir).isDirectory()).toBe(true);
164
+ });
165
+
166
+ it("should create nested directories", () => {
167
+ const nestedDir = path.join(tempDir, "nested", "output", "dir");
168
+ expect(fs.existsSync(nestedDir)).toBe(false);
169
+
170
+ createOutputDirectory(nestedDir);
171
+ expect(fs.existsSync(nestedDir)).toBe(true);
172
+ expect(fs.lstatSync(nestedDir).isDirectory()).toBe(true);
173
+ });
174
+
175
+ it("should not throw if directory already exists", () => {
176
+ const existingDir = path.join(tempDir, "existing");
177
+ fs.mkdirSync(existingDir);
178
+
179
+ expect(() => createOutputDirectory(existingDir)).not.toThrow();
180
+ });
181
+ });
182
+
183
+ describe("resolveTemplate", () => {
184
+ it("should read template file from sample directory", () => {
185
+ const sampleDir = path.join(tempDir, "sample");
186
+ fs.mkdirSync(sampleDir);
187
+
188
+ const templateContent = "Hello, <%= name %>!";
189
+ const templateFile = path.join(sampleDir, "template.txt");
190
+ fs.writeFileSync(templateFile, templateContent);
191
+
192
+ const sample: Sample = {
193
+ template: "template.txt",
194
+ type: "javascript",
195
+ dependencies: [],
196
+ input: [],
197
+ };
198
+
199
+ const result = resolveTemplate(sampleDir, sample);
200
+ expect(result).toBe(templateContent);
201
+ });
202
+
203
+ it("should return template string if file doesn't exist", () => {
204
+ const sampleDir = path.join(tempDir, "sample");
205
+ fs.mkdirSync(sampleDir);
206
+
207
+ const sample: Sample = {
208
+ template: "inline template content",
209
+ type: "javascript",
210
+ dependencies: [],
211
+ input: [],
212
+ };
213
+
214
+ const result = resolveTemplate(sampleDir, sample);
215
+ expect(result).toBe("inline template content");
216
+ });
217
+
218
+ it("should handle template files with various extensions", () => {
219
+ const sampleDir = path.join(tempDir, "sample");
220
+ fs.mkdirSync(sampleDir);
221
+
222
+ const templateContent = "function test() { return 'hello'; }";
223
+ const templateFile = path.join(sampleDir, "template.js");
224
+ fs.writeFileSync(templateFile, templateContent);
225
+
226
+ const sample: Sample = {
227
+ template: "template.js",
228
+ type: "javascript",
229
+ dependencies: [],
230
+ input: [],
231
+ };
232
+
233
+ const result = resolveTemplate(sampleDir, sample);
234
+ expect(result).toBe(templateContent);
235
+ });
236
+ });
237
+
238
+ describe("isObject", () => {
239
+ it("should return true for plain objects", () => {
240
+ expect(isObject({})).toBe(true);
241
+ expect(isObject({ a: 1, b: 2 })).toBe(true);
242
+ expect(isObject({ nested: { object: true } })).toBe(true);
243
+ });
244
+
245
+ it("should return false for arrays", () => {
246
+ expect(isObject([])).toBe(false);
247
+ expect(isObject([1, 2, 3])).toBe(false);
248
+ expect(isObject([{}])).toBe(false);
249
+ });
250
+
251
+ it("should return false for null", () => {
252
+ expect(isObject(null)).toBe(false);
253
+ });
254
+
255
+ it("should return false for primitives", () => {
256
+ expect(isObject("string")).toBe(false);
257
+ expect(isObject(42)).toBe(false);
258
+ expect(isObject(true)).toBe(false);
259
+ expect(isObject(undefined)).toBe(false);
260
+ });
261
+
262
+ it("should return false for functions", () => {
263
+ expect(isObject(() => {})).toBe(false);
264
+ expect(isObject(function () {})).toBe(false);
265
+ });
266
+ });
267
+ });
package/tsconfig.json CHANGED
@@ -1,11 +1,12 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2020",
4
- "module": "commonjs",
5
- "outDir": "dist",
6
- "rootDir": "src",
7
- "strict": true,
8
- "esModuleInterop": true
9
- },
10
- "include": ["src/**/*"]
11
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "outDir": "dist",
6
+ "rootDir": "src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "resolveJsonModule": true
10
+ },
11
+ "include": ["src/**/*"]
12
+ }