@caleuche/cli 0.1.2 → 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 +13 -0
- package/LICENSE +22 -0
- package/README.md +102 -103
- package/dist/compile.js +56 -0
- package/dist/index.js +4 -39
- package/package.json +38 -36
- package/src/compile.ts +67 -0
- package/src/index.ts +17 -67
- package/src/utils.ts +50 -50
- package/test/compile.test.ts +284 -0
- package/test/utils.test.ts +267 -0
- package/tsconfig.json +12 -11
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Brandon Miller
|
|
4
|
+
Copyright (c) 2025 Gerardo Lecaros
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
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
|
-
|
|
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
|
package/dist/compile.js
ADDED
|
@@ -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
|
|
9
|
-
const
|
|
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(
|
|
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,36 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@caleuche/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"bin": {
|
|
6
|
-
"che": "dist/index.js"
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "tsc",
|
|
10
|
-
"build:watch": "tsc --watch",
|
|
11
|
-
"build:clean": "rimraf dist && tsc",
|
|
12
|
-
"build:clean:watch": "rimraf dist && tsc --watch",
|
|
13
|
-
"clean": "rimraf dist",
|
|
14
|
-
"format": "prettier --write .",
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@caleuche/cli",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"che": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"build:watch": "tsc --watch",
|
|
11
|
+
"build:clean": "rimraf dist && tsc",
|
|
12
|
+
"build:clean:watch": "rimraf dist && tsc --watch",
|
|
13
|
+
"clean": "rimraf dist",
|
|
14
|
+
"format": "prettier --write .",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest"
|
|
17
|
+
},
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"description": "Caleuche CLI",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@caleuche/core": "*",
|
|
23
|
+
"commander": "^14.0.0",
|
|
24
|
+
"yaml": "^2.8.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^24.0.0",
|
|
28
|
+
"prettier": "^3.5.3",
|
|
29
|
+
"rimraf": "^6.0.1",
|
|
30
|
+
"typescript": "^5.8.3",
|
|
31
|
+
"vitest": "^3.2.3"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/brandor64/caleuche"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/brandor64/caleuche#readme"
|
|
38
|
+
}
|
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 {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
+
}
|