tshex-cli 1.0.21 → 1.0.23
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/build/main.js +92 -15
- package/package.json +1 -1
- package/readme.md +245 -0
- package/source/main.ts +115 -15
- package/templates/ctx-react/languages/en.json +1 -0
- package/templates/ctx-react/languages/es.json +1 -0
- package/templates/tests/content.ts +16 -0
- package/README.md +0 -168
- /package/templates/ctx-react/{adapters/api → api}/.gitkeep +0 -0
- /package/templates/ctx-react/{adapters/hooks → assets}/.gitkeep +0 -0
- /package/templates/ctx-react/{adapters/schemas → components}/.gitkeep +0 -0
- /package/templates/ctx-react/{application → core}/.gitkeep +0 -0
- /package/templates/ctx-react/{domain → hooks}/.gitkeep +0 -0
- /package/templates/ctx-react/{languages → schemas}/.gitkeep +0 -0
package/build/main.js
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
7
|
function readPackageJson() {
|
|
6
8
|
const filePath = path.join(import.meta.dirname, '..', 'package.json');
|
|
7
9
|
const fileContents = fs.readFileSync(filePath, 'utf-8');
|
|
8
10
|
return JSON.parse(fileContents);
|
|
9
11
|
}
|
|
10
|
-
function
|
|
12
|
+
function executeCreateProject(templatesDir, projectDir) {
|
|
11
13
|
try {
|
|
12
|
-
fs.cpSync(path.join(templatesDir, 'lib'),
|
|
14
|
+
fs.cpSync(path.join(templatesDir, 'lib'), projectDir, {
|
|
13
15
|
recursive: true,
|
|
14
16
|
filter: (src) => src.endsWith('.gitkeep') === false
|
|
15
17
|
});
|
|
16
|
-
console.log('
|
|
18
|
+
console.log('Project created successfully');
|
|
17
19
|
}
|
|
18
20
|
catch (err) {
|
|
19
21
|
console.error(err);
|
|
@@ -43,7 +45,53 @@ function executeCreateReactContext(templatesDir, contextDir) {
|
|
|
43
45
|
console.error(err);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
function
|
|
48
|
+
function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir) {
|
|
49
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
50
|
+
if (fs.existsSync(destinationDir) === false) {
|
|
51
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
55
|
+
const destinationPath = path.join(destinationDir, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
if (entry.name === 'shared') {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
|
|
70
|
+
fs.writeFileSync(destinationPath, fileContents);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function ensureTestsDirectory(testsRootDir) {
|
|
75
|
+
if (fs.existsSync(testsRootDir)) {
|
|
76
|
+
if (fs.statSync(testsRootDir).isDirectory() === false) {
|
|
77
|
+
throw new Error(`Tests path exists but is not a directory: ${testsRootDir}`);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const rl = readline.createInterface({ input, output });
|
|
82
|
+
try {
|
|
83
|
+
const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `);
|
|
84
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
91
|
+
fs.mkdirSync(testsRootDir, { recursive: true });
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
async function main(program) {
|
|
47
95
|
const templatesDir = path.join(import.meta.dirname, '..', 'templates');
|
|
48
96
|
const options = program.opts();
|
|
49
97
|
let targetDir = path.resolve(options.dir ?? process.cwd());
|
|
@@ -53,15 +101,15 @@ function main(program) {
|
|
|
53
101
|
if (fs.existsSync(targetDir) === false) {
|
|
54
102
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
55
103
|
}
|
|
56
|
-
if (options.
|
|
57
|
-
targetDir = path.join(targetDir, options.
|
|
58
|
-
|
|
104
|
+
if (options.project !== undefined) {
|
|
105
|
+
targetDir = path.join(targetDir, options.project);
|
|
106
|
+
executeCreateProject(templatesDir, targetDir);
|
|
59
107
|
}
|
|
60
|
-
if (options.react === true && options.
|
|
61
|
-
program.error('Option --react requires --
|
|
108
|
+
if (options.react === true && options.context === undefined) {
|
|
109
|
+
program.error('Option --react requires --context <name>');
|
|
62
110
|
}
|
|
63
|
-
if (options.
|
|
64
|
-
targetDir = path.join(targetDir, options.
|
|
111
|
+
if (options.context !== undefined) {
|
|
112
|
+
targetDir = path.join(targetDir, options.context);
|
|
65
113
|
if (options.react === true) {
|
|
66
114
|
executeCreateReactContext(templatesDir, targetDir);
|
|
67
115
|
}
|
|
@@ -69,14 +117,43 @@ function main(program) {
|
|
|
69
117
|
executeCreateContext(templatesDir, targetDir);
|
|
70
118
|
}
|
|
71
119
|
}
|
|
120
|
+
if (options.tests !== undefined) {
|
|
121
|
+
const sourceDir = path.resolve(options.tests);
|
|
122
|
+
const testsTemplateFile = path.join(templatesDir, 'tests', 'content.ts');
|
|
123
|
+
if (fs.existsSync(sourceDir) === false) {
|
|
124
|
+
program.error(`Tests source directory does not exist: ${sourceDir}`);
|
|
125
|
+
}
|
|
126
|
+
if (fs.statSync(sourceDir).isDirectory() === false) {
|
|
127
|
+
program.error(`Tests source path is not a directory: ${sourceDir}`);
|
|
128
|
+
}
|
|
129
|
+
if (fs.existsSync(testsTemplateFile) === false) {
|
|
130
|
+
program.error(`Tests template file does not exist: ${testsTemplateFile}`);
|
|
131
|
+
}
|
|
132
|
+
const testsTemplateContents = fs.readFileSync(testsTemplateFile, 'utf-8');
|
|
133
|
+
const testsRootDir = path.join(targetDir, 'tests');
|
|
134
|
+
const testsDirectoryCreated = await ensureTestsDirectory(testsRootDir);
|
|
135
|
+
if (testsDirectoryCreated === false) {
|
|
136
|
+
program.error('Tests directory creation cancelled');
|
|
137
|
+
}
|
|
138
|
+
const testsDir = path.join(testsRootDir, path.basename(sourceDir));
|
|
139
|
+
if (testsDir === sourceDir) {
|
|
140
|
+
program.error('Tests destination directory cannot be the same as the source directory');
|
|
141
|
+
}
|
|
142
|
+
executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir);
|
|
143
|
+
console.log('Tests structure created successfully');
|
|
144
|
+
}
|
|
72
145
|
}
|
|
73
146
|
const packageJson = readPackageJson();
|
|
74
147
|
program
|
|
75
148
|
.name('tshex')
|
|
76
149
|
.version(packageJson.version)
|
|
77
|
-
.option('--
|
|
78
|
-
.option('--
|
|
79
|
-
.option('-R, --react', 'creates a React context with --
|
|
150
|
+
.option('-P, --project <name>', "creates a new project with it's shared directory")
|
|
151
|
+
.option('-C, --context <name>', 'creates a new context')
|
|
152
|
+
.option('-R, --react', 'creates a React context with --context')
|
|
153
|
+
.option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
|
|
80
154
|
.option('--dir <path>', 'sets the directory to create the new item')
|
|
81
155
|
.parse(process.argv);
|
|
82
|
-
main(program)
|
|
156
|
+
void main(program).catch((err) => {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
program.error(message);
|
|
159
|
+
});
|
package/package.json
CHANGED
package/readme.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Hexagonal Architecture CLI `tshex-cli`
|
|
2
|
+
|
|
3
|
+
`tshex-cli` creates the base structure of a project organized by contexts. The structure groups shared contracts, domain concepts, use cases, and adapters into directories with defined responsibilities.
|
|
4
|
+
|
|
5
|
+
In this guide, we will build a project named `core` with a context named `users`. The walkthrough starts with the CLI and then explains the purpose of the generated components.
|
|
6
|
+
|
|
7
|
+
The examples in this guide are intentionally simple. They are designed to show the responsibility of each component, not to cover real infrastructure or production scenarios.
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
Hexagonal architecture is a software design pattern that separates the core domain of the application from the external dependencies. This separation is achieved by dividing the application into layers. Each layer has a specific responsibility and interacts with the other layers in a specific way.
|
|
12
|
+
|
|
13
|
+
The goal is to separate the domain code from installed dependencies. A Hexagonal Architecture framework would work against that goal, since it couples the domain code to the framework itself. That is why this CLI exists: it scaffolds the structure for you, generating a codebase you own and control, instead of a framework.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
|
|
19
|
+
Install the package in your Node.js project:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install tshex-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install it globally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g tshex-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package registers the `tshex` executable. You can invoke it with `npx` from the project directory.
|
|
32
|
+
|
|
33
|
+
### Help
|
|
34
|
+
|
|
35
|
+
View the available commands and options with:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx tshex --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Create a project
|
|
42
|
+
|
|
43
|
+
The `--project` option receives the name of the project's root directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx tshex --project core
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You can also use the short form:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx tshex -P core
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
In this example, `core` will contain the shared code, the contexts, and the project's main implementation.
|
|
56
|
+
|
|
57
|
+
### Create a context
|
|
58
|
+
|
|
59
|
+
The `--context` option receives the context name:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx tshex --context users
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also use the short form:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx tshex -C users
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A context groups the rules and operations of an application capability. `users`, `sales`, `billing`, and `inventory` are examples of contexts.
|
|
72
|
+
|
|
73
|
+
### Create the project and the first context
|
|
74
|
+
|
|
75
|
+
You can generate both components in a single execution:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx tshex --project core --context users
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The command creates this structure:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
core/
|
|
85
|
+
|-- index.d.ts
|
|
86
|
+
|-- main.ts
|
|
87
|
+
|-- shared/
|
|
88
|
+
| |-- application/
|
|
89
|
+
| | |-- data/
|
|
90
|
+
| | | |-- drivers.ts
|
|
91
|
+
| | | |-- managers.ts
|
|
92
|
+
| | | `-- repositories.ts
|
|
93
|
+
| | |-- events.ts
|
|
94
|
+
| | |-- http.ts
|
|
95
|
+
| | |-- loggers.ts
|
|
96
|
+
| | |-- services.ts
|
|
97
|
+
| | `-- validations.ts
|
|
98
|
+
| `-- domain/
|
|
99
|
+
| |-- aggregates.ts
|
|
100
|
+
| |-- entities.ts
|
|
101
|
+
| |-- errors.ts
|
|
102
|
+
| `-- value-objects.ts
|
|
103
|
+
`-- users/
|
|
104
|
+
|-- adapters/
|
|
105
|
+
|-- application/
|
|
106
|
+
|-- domain/
|
|
107
|
+
`-- example-ports.ts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Choose the destination directory
|
|
111
|
+
|
|
112
|
+
The `--dir` option specifies the directory from which the structure is created:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx tshex --dir ./src --project core --context users
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The example project is created at `src/core`.
|
|
119
|
+
|
|
120
|
+
To add a context to an existing project, use the project as the destination directory:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx tshex --dir ./core --context billing
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The context is created at `core/billing`.
|
|
127
|
+
|
|
128
|
+
### Create a context for React
|
|
129
|
+
|
|
130
|
+
The `--react` option creates a context intended for a React application.
|
|
131
|
+
|
|
132
|
+
A React context is a consumer by nature. It does not provide a hexagonal capability to other systems. Instead, it consumes existing capabilities and organizes that consumption as a collection of adapters for the UI layer.
|
|
133
|
+
|
|
134
|
+
Use this mode when the generated context will call APIs, validate interface data, expose hooks, compose components, and localize messages.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx tshex --context users --react
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
You can also use its short form:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx tshex -C users -R
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The command creates this structure:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
users/
|
|
150
|
+
|-- api/
|
|
151
|
+
|-- assets/
|
|
152
|
+
|-- components/
|
|
153
|
+
|-- core/
|
|
154
|
+
|-- hooks/
|
|
155
|
+
|-- languages/
|
|
156
|
+
`-- schemas/
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Each directory represents a consumption adapter or a resource used by those adapters:
|
|
160
|
+
|
|
161
|
+
| Directory | Responsibility |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `api/` | Adapters that call external services or backend contexts. |
|
|
164
|
+
| `assets/` | Static resources used by the React context. |
|
|
165
|
+
| `components/` | Visual react adapters that render the consumed capability. |
|
|
166
|
+
| `core/` | Local support code and project modules adapters shared by the adapters in this context. |
|
|
167
|
+
| `hooks/` | React hook adapters that expose behavior to components. |
|
|
168
|
+
| `languages/` | Translation resources for the interface. Can be json, ts files, etc. |
|
|
169
|
+
| `schemas/` | Validation and parsing adapters for UI input and output. |
|
|
170
|
+
|
|
171
|
+
This layout is intentionally different from the default context template. The standard context separates `domain`, `application`, and `adapters` because it models and provides a capability. The React context generated with `--react` assumes the opposite role: it always consumes capabilities and groups the code around the adapters required by that consumption.
|
|
172
|
+
|
|
173
|
+
### Create a tests structure
|
|
174
|
+
|
|
175
|
+
The `--tests` option receives a source directory and creates a matching tests structure.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx tshex --tests ./core
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
You can also use the short form:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npx tshex -T ./core
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The command creates the output inside a `tests/` directory. If `tests/` does not exist, the CLI asks whether it should be created.
|
|
188
|
+
|
|
189
|
+
By default, the `tests/` directory is resolved from the current execution directory:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx tshex -T ./core
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
This generates a structure like this:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
tests/
|
|
199
|
+
`-- core/
|
|
200
|
+
|-- users/
|
|
201
|
+
| |-- adapters/
|
|
202
|
+
| |-- application/
|
|
203
|
+
| | `-- create-user.ts
|
|
204
|
+
| `-- domain/
|
|
205
|
+
`-- billing/
|
|
206
|
+
|-- application/
|
|
207
|
+
`-- domain/
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
To choose another base directory for `tests/`, combine `--tests` with `--dir`:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx tshex -T ./core --dir ./output
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
That command creates or reuses `./output/tests/`.
|
|
217
|
+
|
|
218
|
+
This command helps you prepare a tests workspace that follows the shape of your source directory while fitting naturally into the place where you are working. You can use it in the current directory for a quick setup, or combine it with `--dir` when you want the tests structure to be created somewhere else. If `tests/` already contains content, the command continues working with what is already there instead of interrupting your flow.
|
|
219
|
+
|
|
220
|
+
## Documentation index
|
|
221
|
+
|
|
222
|
+
From this point on, the guide is split into dedicated documents under `docs/`.
|
|
223
|
+
|
|
224
|
+
### General
|
|
225
|
+
|
|
226
|
+
- [Project structure](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-structure.md)
|
|
227
|
+
- [Project types](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-types.md)
|
|
228
|
+
- [Context ports](https://github.com/virtualitems/tshex-cli/blob/main/docs/context-ports.md)
|
|
229
|
+
- [Generated file reference](https://github.com/virtualitems/tshex-cli/blob/main/docs/generated-file-reference.md)
|
|
230
|
+
|
|
231
|
+
### Shared application
|
|
232
|
+
|
|
233
|
+
- [shared/application/data](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/data.md)
|
|
234
|
+
- [shared/application/events.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/events.md)
|
|
235
|
+
- [shared/application/http.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http.md)
|
|
236
|
+
- [shared/application/loggers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/loggers.md)
|
|
237
|
+
- [shared/application/services.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/services.md)
|
|
238
|
+
- [shared/application/validations.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/validations.md)
|
|
239
|
+
|
|
240
|
+
### Shared domain
|
|
241
|
+
|
|
242
|
+
- [shared/domain/aggregates.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/aggregates.md)
|
|
243
|
+
- [shared/domain/entities.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/entities.md)
|
|
244
|
+
- [shared/domain/errors.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/errors.md)
|
|
245
|
+
- [shared/domain/value-objects.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/value-objects.md)
|
package/source/main.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { program } from 'commander'
|
|
|
8
8
|
|
|
9
9
|
import fs from 'node:fs'
|
|
10
10
|
import path from 'node:path'
|
|
11
|
+
import readline from 'node:readline/promises'
|
|
12
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
11
13
|
|
|
12
14
|
// FUNCTIONS
|
|
13
15
|
|
|
@@ -17,13 +19,13 @@ function readPackageJson() {
|
|
|
17
19
|
return JSON.parse(fileContents)
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
function
|
|
22
|
+
function executeCreateProject(templatesDir: string, projectDir: string) {
|
|
21
23
|
try {
|
|
22
|
-
fs.cpSync(path.join(templatesDir, 'lib'),
|
|
24
|
+
fs.cpSync(path.join(templatesDir, 'lib'), projectDir, {
|
|
23
25
|
recursive: true,
|
|
24
26
|
filter: (src) => src.endsWith('.gitkeep') === false
|
|
25
27
|
})
|
|
26
|
-
console.log('
|
|
28
|
+
console.log('Project created successfully')
|
|
27
29
|
} catch (err) {
|
|
28
30
|
console.error(err)
|
|
29
31
|
}
|
|
@@ -53,7 +55,66 @@ function executeCreateReactContext(templatesDir: string, contextDir: string) {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function
|
|
58
|
+
function executeCreateTests(sourceDir: string, destinationDir: string, fileContents: string, ignoredSourceDir?: string) {
|
|
59
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(destinationDir) === false) {
|
|
62
|
+
fs.mkdirSync(destinationDir, { recursive: true })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const sourcePath = path.join(sourceDir, entry.name)
|
|
67
|
+
const destinationPath = path.join(destinationDir, entry.name)
|
|
68
|
+
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
if (entry.name === 'shared') {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
|
|
87
|
+
fs.writeFileSync(destinationPath, fileContents)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function ensureTestsDirectory(testsRootDir: string) {
|
|
93
|
+
if (fs.existsSync(testsRootDir)) {
|
|
94
|
+
if (fs.statSync(testsRootDir).isDirectory() === false) {
|
|
95
|
+
throw new Error(`Tests path exists but is not a directory: ${testsRootDir}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rl = readline.createInterface({ input, output })
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `)
|
|
105
|
+
|
|
106
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
rl.close()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fs.mkdirSync(testsRootDir, { recursive: true })
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function main(program: typeof import('commander').program) {
|
|
57
118
|
const templatesDir = path.join(import.meta.dirname, '..', 'templates')
|
|
58
119
|
|
|
59
120
|
const options = program.opts()
|
|
@@ -68,17 +129,17 @@ function main(program: typeof import('commander').program) {
|
|
|
68
129
|
fs.mkdirSync(targetDir, { recursive: true })
|
|
69
130
|
}
|
|
70
131
|
|
|
71
|
-
if (options.
|
|
72
|
-
targetDir = path.join(targetDir, options.
|
|
73
|
-
|
|
132
|
+
if (options.project !== undefined) {
|
|
133
|
+
targetDir = path.join(targetDir, options.project)
|
|
134
|
+
executeCreateProject(templatesDir, targetDir)
|
|
74
135
|
}
|
|
75
136
|
|
|
76
|
-
if (options.react === true && options.
|
|
77
|
-
program.error('Option --react requires --
|
|
137
|
+
if (options.react === true && options.context === undefined) {
|
|
138
|
+
program.error('Option --react requires --context <name>')
|
|
78
139
|
}
|
|
79
140
|
|
|
80
|
-
if (options.
|
|
81
|
-
targetDir = path.join(targetDir, options.
|
|
141
|
+
if (options.context !== undefined) {
|
|
142
|
+
targetDir = path.join(targetDir, options.context)
|
|
82
143
|
|
|
83
144
|
if (options.react === true) {
|
|
84
145
|
executeCreateReactContext(templatesDir, targetDir)
|
|
@@ -86,6 +147,41 @@ function main(program: typeof import('commander').program) {
|
|
|
86
147
|
executeCreateContext(templatesDir, targetDir)
|
|
87
148
|
}
|
|
88
149
|
}
|
|
150
|
+
|
|
151
|
+
if (options.tests !== undefined) {
|
|
152
|
+
const sourceDir = path.resolve(options.tests)
|
|
153
|
+
const testsTemplateFile = path.join(templatesDir, 'tests', 'content.ts')
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(sourceDir) === false) {
|
|
156
|
+
program.error(`Tests source directory does not exist: ${sourceDir}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (fs.statSync(sourceDir).isDirectory() === false) {
|
|
160
|
+
program.error(`Tests source path is not a directory: ${sourceDir}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (fs.existsSync(testsTemplateFile) === false) {
|
|
164
|
+
program.error(`Tests template file does not exist: ${testsTemplateFile}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const testsTemplateContents = fs.readFileSync(testsTemplateFile, 'utf-8')
|
|
168
|
+
|
|
169
|
+
const testsRootDir = path.join(targetDir, 'tests')
|
|
170
|
+
const testsDirectoryCreated = await ensureTestsDirectory(testsRootDir)
|
|
171
|
+
|
|
172
|
+
if (testsDirectoryCreated === false) {
|
|
173
|
+
program.error('Tests directory creation cancelled')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const testsDir = path.join(testsRootDir, path.basename(sourceDir))
|
|
177
|
+
|
|
178
|
+
if (testsDir === sourceDir) {
|
|
179
|
+
program.error('Tests destination directory cannot be the same as the source directory')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir)
|
|
183
|
+
console.log('Tests structure created successfully')
|
|
184
|
+
}
|
|
89
185
|
}
|
|
90
186
|
|
|
91
187
|
const packageJson = readPackageJson()
|
|
@@ -93,10 +189,14 @@ const packageJson = readPackageJson()
|
|
|
93
189
|
program
|
|
94
190
|
.name('tshex')
|
|
95
191
|
.version(packageJson.version)
|
|
96
|
-
.option('--
|
|
97
|
-
.option('--
|
|
98
|
-
.option('-R, --react', 'creates a React context with --
|
|
192
|
+
.option('-P, --project <name>', "creates a new project with it's shared directory")
|
|
193
|
+
.option('-C, --context <name>', 'creates a new context')
|
|
194
|
+
.option('-R, --react', 'creates a React context with --context')
|
|
195
|
+
.option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
|
|
99
196
|
.option('--dir <path>', 'sets the directory to create the new item')
|
|
100
197
|
.parse(process.argv)
|
|
101
198
|
|
|
102
|
-
main(program)
|
|
199
|
+
void main(program).catch((err: unknown) => {
|
|
200
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
201
|
+
program.error(message)
|
|
202
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
describe('math', () => {
|
|
5
|
+
test('sum', () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const a = 2
|
|
8
|
+
const b = 3
|
|
9
|
+
|
|
10
|
+
// Act
|
|
11
|
+
const result = a * b
|
|
12
|
+
|
|
13
|
+
// Assert
|
|
14
|
+
assert.strictEqual(result, 6)
|
|
15
|
+
})
|
|
16
|
+
})
|
package/README.md
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Hexagonal Architecture CLI `tshex-cli`
|
|
2
|
-
|
|
3
|
-
`tshex-cli` creates the base structure of a library organized by contexts. The structure groups shared contracts, domain concepts, use cases, and adapters into directories with defined responsibilities.
|
|
4
|
-
|
|
5
|
-
In this guide, we will build a library named `core` with a context named `users`. The walkthrough starts with the CLI and then explains the purpose of the generated components.
|
|
6
|
-
|
|
7
|
-
The examples in this guide are intentionally simple. They are designed to show the responsibility of each component, not to cover real infrastructure or production scenarios.
|
|
8
|
-
|
|
9
|
-
## Why?
|
|
10
|
-
|
|
11
|
-
Hexagonal architecture is a software design pattern that separates the core domain of the application from the external dependencies. This separation is achieved by dividing the application into layers. Each layer has a specific responsibility and interacts with the other layers in a specific way.
|
|
12
|
-
|
|
13
|
-
The goal is to separate the domain code from installed dependencies. A Hexagonal Architecture framework would work against that goal, since it couples the domain code to the framework itself. That is why this CLI exists: it scaffolds the structure for you, generating a codebase you own and control, instead of a framework.
|
|
14
|
-
|
|
15
|
-
## Getting started
|
|
16
|
-
|
|
17
|
-
### Installation
|
|
18
|
-
|
|
19
|
-
Install the package in your Node.js project:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install tshex-cli
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Or install it globally:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm install -g tshex-cli
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The package registers the `tshex` executable. You can invoke it with `npx` from the project directory.
|
|
32
|
-
|
|
33
|
-
### Help
|
|
34
|
-
|
|
35
|
-
View the available commands and options with:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npx tshex --help
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Create a library
|
|
42
|
-
|
|
43
|
-
The `--lib` option receives the name of the library's root directory:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx tshex --lib core
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
In this example, `core` will contain the shared code, the contexts, and the library's main implementation.
|
|
50
|
-
|
|
51
|
-
### Create a context
|
|
52
|
-
|
|
53
|
-
The `--ctx` option receives the context name:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
npx tshex --ctx users
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
A context groups the rules and operations of an application capability. `users`, `sales`, `billing`, and `inventory` are examples of contexts.
|
|
60
|
-
|
|
61
|
-
### Create the library and the first context
|
|
62
|
-
|
|
63
|
-
You can generate both components in a single execution:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npx tshex --lib core --ctx users
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
The command creates this structure:
|
|
70
|
-
|
|
71
|
-
```text
|
|
72
|
-
core/
|
|
73
|
-
├── index.d.ts
|
|
74
|
-
├── main.ts
|
|
75
|
-
├── shared/
|
|
76
|
-
│ ├── application/
|
|
77
|
-
│ │ ├── data/
|
|
78
|
-
│ │ │ ├── drivers.ts
|
|
79
|
-
│ │ │ ├── managers.ts
|
|
80
|
-
│ │ │ └── repositories.ts
|
|
81
|
-
│ │ ├── events.ts
|
|
82
|
-
│ │ ├── http.ts
|
|
83
|
-
│ │ ├── loggers.ts
|
|
84
|
-
│ │ ├── services.ts
|
|
85
|
-
│ │ └── validations.ts
|
|
86
|
-
│ └── domain/
|
|
87
|
-
│ ├── aggregates.ts
|
|
88
|
-
│ ├── entities.ts
|
|
89
|
-
│ ├── errors.ts
|
|
90
|
-
│ └── value-objects.ts
|
|
91
|
-
└── users/
|
|
92
|
-
├── adapters/
|
|
93
|
-
├── application/
|
|
94
|
-
├── domain/
|
|
95
|
-
└── example-ports.ts
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Choose the destination directory
|
|
99
|
-
|
|
100
|
-
The `--dir` option specifies the directory from which the structure is created:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
npx tshex --dir ./src --lib core --ctx users
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
The example library is created at `src/core`.
|
|
107
|
-
|
|
108
|
-
To add a context to an existing library, use the library as the destination directory:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npx tshex --dir ./core --ctx billing
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
The context is created at `core/billing`.
|
|
115
|
-
|
|
116
|
-
### Create a context for React
|
|
117
|
-
|
|
118
|
-
The `--react` option creates a context with directories intended for a React application:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
npx tshex --ctx users --react
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
You can also use its short form:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
npx tshex --ctx users -R
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
The context structure includes adapters for APIs, hooks, and schemas, together with application, domain, and language resources:
|
|
131
|
-
|
|
132
|
-
```text
|
|
133
|
-
users/
|
|
134
|
-
├── adapters/
|
|
135
|
-
│ ├── api/
|
|
136
|
-
│ ├── hooks/
|
|
137
|
-
│ └── schemas/
|
|
138
|
-
├── application/
|
|
139
|
-
├── domain/
|
|
140
|
-
└── languages/
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Documentation index
|
|
144
|
-
|
|
145
|
-
From this point on, the guide is split into dedicated documents under `docs/`.
|
|
146
|
-
|
|
147
|
-
### General
|
|
148
|
-
|
|
149
|
-
- [Library structure](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-structure.md)
|
|
150
|
-
- [Library types](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-types.md)
|
|
151
|
-
- [Context ports](https://github.com/virtualitems/tshex-cli/blob/main/docs/context-ports.md)
|
|
152
|
-
- [Generated file reference](https://github.com/virtualitems/tshex-cli/blob/main/docs/generated-file-reference.md)
|
|
153
|
-
|
|
154
|
-
### Shared application
|
|
155
|
-
|
|
156
|
-
- [shared/application/data](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/data.md)
|
|
157
|
-
- [shared/application/events.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/events.md)
|
|
158
|
-
- [shared/application/http.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http.md)
|
|
159
|
-
- [shared/application/loggers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/loggers.md)
|
|
160
|
-
- [shared/application/services.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/services.md)
|
|
161
|
-
- [shared/application/validations.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/validations.md)
|
|
162
|
-
|
|
163
|
-
### Shared domain
|
|
164
|
-
|
|
165
|
-
- [shared/domain/aggregates.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/aggregates.md)
|
|
166
|
-
- [shared/domain/entities.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/entities.md)
|
|
167
|
-
- [shared/domain/errors.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/errors.md)
|
|
168
|
-
- [shared/domain/value-objects.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/domain/value-objects.md)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|