tshex-cli 1.0.22 → 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 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 executeCreateLib(templatesDir, libraryDir) {
12
+ function executeCreateProject(templatesDir, projectDir) {
11
13
  try {
12
- fs.cpSync(path.join(templatesDir, 'lib'), libraryDir, {
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('Library created successfully');
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 main(program) {
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.lib !== undefined) {
57
- targetDir = path.join(targetDir, options.lib);
58
- executeCreateLib(templatesDir, targetDir);
104
+ if (options.project !== undefined) {
105
+ targetDir = path.join(targetDir, options.project);
106
+ executeCreateProject(templatesDir, targetDir);
59
107
  }
60
- if (options.react === true && options.ctx === undefined) {
61
- program.error('Option --react requires --ctx <name>');
108
+ if (options.react === true && options.context === undefined) {
109
+ program.error('Option --react requires --context <name>');
62
110
  }
63
- if (options.ctx !== undefined) {
64
- targetDir = path.join(targetDir, options.ctx);
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('--lib <name>', "creates a new library with it's shared directory")
78
- .option('--ctx <name>', 'creates a new context')
79
- .option('-R, --react', 'creates a React context with --ctx')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -1,8 +1,8 @@
1
1
  # Hexagonal Architecture CLI `tshex-cli`
2
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.
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
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.
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
6
 
7
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
8
 
@@ -38,32 +38,44 @@ View the available commands and options with:
38
38
  npx tshex --help
39
39
  ```
40
40
 
41
- ### Create a library
41
+ ### Create a project
42
42
 
43
- The `--lib` option receives the name of the library's root directory:
43
+ The `--project` option receives the name of the project's root directory:
44
44
 
45
45
  ```bash
46
- npx tshex --lib core
46
+ npx tshex --project core
47
47
  ```
48
48
 
49
- In this example, `core` will contain the shared code, the contexts, and the library's main implementation.
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.
50
56
 
51
57
  ### Create a context
52
58
 
53
- The `--ctx` option receives the context name:
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:
54
66
 
55
67
  ```bash
56
- npx tshex --ctx users
68
+ npx tshex -C users
57
69
  ```
58
70
 
59
71
  A context groups the rules and operations of an application capability. `users`, `sales`, `billing`, and `inventory` are examples of contexts.
60
72
 
61
- ### Create the library and the first context
73
+ ### Create the project and the first context
62
74
 
63
75
  You can generate both components in a single execution:
64
76
 
65
77
  ```bash
66
- npx tshex --lib core --ctx users
78
+ npx tshex --project core --context users
67
79
  ```
68
80
 
69
81
  The command creates this structure:
@@ -100,15 +112,15 @@ core/
100
112
  The `--dir` option specifies the directory from which the structure is created:
101
113
 
102
114
  ```bash
103
- npx tshex --dir ./src --lib core --ctx users
115
+ npx tshex --dir ./src --project core --context users
104
116
  ```
105
117
 
106
- The example library is created at `src/core`.
118
+ The example project is created at `src/core`.
107
119
 
108
- To add a context to an existing library, use the library as the destination directory:
120
+ To add a context to an existing project, use the project as the destination directory:
109
121
 
110
122
  ```bash
111
- npx tshex --dir ./core --ctx billing
123
+ npx tshex --dir ./core --context billing
112
124
  ```
113
125
 
114
126
  The context is created at `core/billing`.
@@ -122,13 +134,13 @@ A React context is a consumer by nature. It does not provide a hexagonal capabil
122
134
  Use this mode when the generated context will call APIs, validate interface data, expose hooks, compose components, and localize messages.
123
135
 
124
136
  ```bash
125
- npx tshex --ctx users --react
137
+ npx tshex --context users --react
126
138
  ```
127
139
 
128
140
  You can also use its short form:
129
141
 
130
142
  ```bash
131
- npx tshex --ctx users -R
143
+ npx tshex -C users -R
132
144
  ```
133
145
 
134
146
  The command creates this structure:
@@ -158,14 +170,61 @@ Each directory represents a consumption adapter or a resource used by those adap
158
170
 
159
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.
160
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
+
161
220
  ## Documentation index
162
221
 
163
222
  From this point on, the guide is split into dedicated documents under `docs/`.
164
223
 
165
224
  ### General
166
225
 
167
- - [Library structure](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-structure.md)
168
- - [Library types](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-types.md)
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)
169
228
  - [Context ports](https://github.com/virtualitems/tshex-cli/blob/main/docs/context-ports.md)
170
229
  - [Generated file reference](https://github.com/virtualitems/tshex-cli/blob/main/docs/generated-file-reference.md)
171
230
 
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 executeCreateLib(templatesDir: string, libraryDir: string) {
22
+ function executeCreateProject(templatesDir: string, projectDir: string) {
21
23
  try {
22
- fs.cpSync(path.join(templatesDir, 'lib'), libraryDir, {
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('Library created successfully')
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 main(program: typeof import('commander').program) {
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.lib !== undefined) {
72
- targetDir = path.join(targetDir, options.lib)
73
- executeCreateLib(templatesDir, targetDir)
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.ctx === undefined) {
77
- program.error('Option --react requires --ctx <name>')
137
+ if (options.react === true && options.context === undefined) {
138
+ program.error('Option --react requires --context <name>')
78
139
  }
79
140
 
80
- if (options.ctx !== undefined) {
81
- targetDir = path.join(targetDir, options.ctx)
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('--lib <name>', "creates a new library with it's shared directory")
97
- .option('--ctx <name>', 'creates a new context')
98
- .option('-R, --react', 'creates a React context with --ctx')
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,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
+ })