express-project-init 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -2
- package/dist/features.js +17 -0
- package/dist/features.js.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/services/file.service.js +73 -0
- package/dist/services/file.service.js.map +1 -0
- package/dist/services/project.service.js +44 -0
- package/dist/services/project.service.js.map +1 -0
- package/dist/services/prompts.service.js +92 -0
- package/dist/services/prompts.service.js.map +1 -0
- package/dist/services/template.service.js +200 -0
- package/dist/services/template.service.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -4
- package/templates/format/biome/both/biome.json +17 -0
- package/templates/format/biome/both/package.json +12 -0
- package/templates/format/biome/format/biome.json +14 -0
- package/templates/format/biome/format/package.json +9 -0
- package/templates/format/prettier/.prettierignore +3 -0
- package/templates/format/prettier/.prettierrc +6 -0
- package/templates/format/prettier/package.json +9 -0
- package/templates/javascript/base/package.json +13 -0
- package/templates/javascript/base/src/app.js +22 -0
- package/templates/javascript/base/src/server.js +8 -0
- package/templates/javascript/base/src/utils/helpers.js +3 -0
- package/templates/javascript/base/src/utils/responseHelpers.js +19 -0
- package/templates/javascript/error-handler/src/app.js +28 -0
- package/templates/javascript/error-handler/src/middleware/error.middleware.js +36 -0
- package/templates/javascript/error-handler/src/middleware/notFound.middleware.js +5 -0
- package/templates/javascript/error-handler/src/utils/AppError.js +30 -0
- package/templates/lint/biome/biome.json +15 -0
- package/templates/lint/biome/package.json +9 -0
- package/templates/lint/eslint/eslint.config.js +36 -0
- package/templates/lint/eslint/package.json +12 -0
- package/templates/lint/oxlint/.oxlintrc.json +13 -0
- package/templates/lint/oxlint/package.json +9 -0
- package/templates/typescript/base/package.json +20 -0
- package/templates/typescript/base/src/app.ts +22 -0
- package/templates/typescript/base/src/server.ts +8 -0
- package/templates/typescript/base/src/utils/helpers.ts +3 -0
- package/templates/typescript/base/src/utils/responseHelpers.ts +42 -0
- package/templates/typescript/base/tsconfig.json +30 -0
- package/templates/typescript/error-handler/src/app.ts +28 -0
- package/templates/typescript/error-handler/src/middleware/error.middleware.ts +42 -0
- package/templates/typescript/error-handler/src/middleware/notFound.middleware.ts +10 -0
- package/templates/typescript/error-handler/src/utils/AppError.ts +44 -0
package/README.md
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ExpressKit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Scaffold a clean Express API in seconds — JavaScript or TypeScript.**
|
|
4
|
+
|
|
5
|
+
[](docs/demo.mp4)
|
|
6
|
+
|
|
7
|
+
ExpressKit asks a few quick questions, then writes a ready-to-run Express 5 project. Choose JavaScript or TypeScript, optionally add centralized error handling, pick your linter and formatter, and start building.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Requires **Node.js 20+**.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create expresskit@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run it directly:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx create-expresskit@latest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Answer the prompts:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
? Project name › my-api
|
|
27
|
+
? Language › TypeScript
|
|
28
|
+
? Create central error handling middleware? › Yes
|
|
29
|
+
? Which linter would you like to use? › ESLint
|
|
30
|
+
? Which formatter would you like to use? › Prettier
|
|
31
|
+
? Install dependencies? › Yes
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd my-api
|
|
38
|
+
npm run dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Open <http://localhost:3000/health>.
|
|
42
|
+
|
|
43
|
+
Use `.` at the project name prompt to scaffold in the current directory. ExpressKit will not overwrite existing files.
|
|
44
|
+
|
|
45
|
+
## What you get
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
my-api/
|
|
49
|
+
├── package.json
|
|
50
|
+
├── tsconfig.json # TypeScript only
|
|
51
|
+
├── eslint.config.js # with ESLint
|
|
52
|
+
├── .oxlintrc.json # with Oxlint
|
|
53
|
+
├── biome.json # with Biome (linter, formatter, or both)
|
|
54
|
+
├── .prettierrc # with Prettier
|
|
55
|
+
├── .prettierignore # with Prettier
|
|
56
|
+
└── src/
|
|
57
|
+
├── server.ts
|
|
58
|
+
├── app.ts
|
|
59
|
+
├── middleware/ # with the optional error handler
|
|
60
|
+
│ ├── error.middleware.ts
|
|
61
|
+
│ └── notFound.middleware.ts
|
|
62
|
+
└── utils/
|
|
63
|
+
├── helpers.ts
|
|
64
|
+
├── responseHelpers.ts
|
|
65
|
+
└── AppError.ts # with the optional error handler
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
JavaScript projects use the same layout with `.js` files and no `tsconfig.json`.
|
|
69
|
+
|
|
70
|
+
### Scripts
|
|
71
|
+
|
|
72
|
+
| Command | Provider | Description |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `npm run dev` | Core | Start development server with file watching |
|
|
75
|
+
| `npm start` | Core | Run the application |
|
|
76
|
+
| `npm run typecheck` | Core (TS) | Run TypeScript compiler checks (`tsc --noEmit`) |
|
|
77
|
+
| `npm run lint` | ESLint / Biome / Oxlint | Lint source files |
|
|
78
|
+
| `npm run lint:fix` | ESLint / Biome / Oxlint | Auto-fix linting issues |
|
|
79
|
+
| `npm run format` | Prettier / Biome | Format source files |
|
|
80
|
+
| `npm run format:check` | Prettier / Biome | Check source files for formatting issues |
|
|
81
|
+
| `npm run check` | Biome (both) | Run formatting check, import sorting, and linting |
|
|
82
|
+
|
|
83
|
+
## Linters and formatters
|
|
84
|
+
|
|
85
|
+
ExpressKit supports modern, lightning-fast linting and formatting tools:
|
|
86
|
+
|
|
87
|
+
- **Linters**:
|
|
88
|
+
- **ESLint**: Industry-standard linter configured with ESLint 9+ flat config (`eslint.config.js`), Node globals, and TypeScript support via `typescript-eslint`.
|
|
89
|
+
- **Biome**: Rust-powered linter with instant feedback and safe fixes.
|
|
90
|
+
- **Oxlint**: High-speed Rust-based linter from the OXC project with `.oxlintrc.json`.
|
|
91
|
+
- **None**: Skip linter setup.
|
|
92
|
+
|
|
93
|
+
- **Formatters**:
|
|
94
|
+
- **Prettier**: Opinionated code formatter with `.prettierrc` and `.prettierignore`.
|
|
95
|
+
- **Biome**: High-performance code formatter configured with 2-space indentation.
|
|
96
|
+
- **None**: Skip formatter setup.
|
|
97
|
+
|
|
98
|
+
### Tool combinations
|
|
99
|
+
|
|
100
|
+
- **Biome for both linting & formatting**: Generates a unified `biome.json` with both features enabled, plus `lint`, `lint:fix`, `format`, `format:check`, and `check` scripts.
|
|
101
|
+
- **ESLint + Prettier**: Automatically installs and configures `eslint-config-prettier` to disable ESLint rules that might conflict with Prettier.
|
|
102
|
+
|
|
103
|
+
## Error handling
|
|
104
|
+
|
|
105
|
+
With the error handler enabled, throw `BadRequestError`, `UnauthorizedError`, or `NotFoundError` from any route and one middleware formats the response:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"success": false,
|
|
110
|
+
"message": "Route not found",
|
|
111
|
+
"code": "NOT_FOUND"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Successful responses use the same envelope with `success: true` and `data`.
|
|
116
|
+
|
|
117
|
+
## How it works
|
|
118
|
+
|
|
119
|
+
Projects are assembled from a base template plus optional feature and tool layers. Feature and tool files replace base files at the same path, and `package.json` fragments are merged into the base manifest.
|
|
120
|
+
|
|
121
|
+
## Local development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm install
|
|
125
|
+
npm run build
|
|
126
|
+
npm link
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then, in a separate directory:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
mkdir -p /tmp/expresskit-test
|
|
133
|
+
cd /tmp/expresskit-test
|
|
134
|
+
create-expresskit
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Rebuild with `npm run build` after changing `src/`; the linked command picks up the new `dist/` automatically. Remove the global link with `npm unlink -g create-expresskit`.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every optional feature lives here once.
|
|
3
|
+
*
|
|
4
|
+
* To add a feature:
|
|
5
|
+
* 1. Add templates/<language>/<id>/ with the files it adds or overrides.
|
|
6
|
+
* 2. Add an entry to this array.
|
|
7
|
+
*
|
|
8
|
+
* The prompt, file layering, and package.json merging all derive from this list.
|
|
9
|
+
*/
|
|
10
|
+
export const FEATURES = [
|
|
11
|
+
{
|
|
12
|
+
id: "error-handler",
|
|
13
|
+
prompt: "Create central error handling middleware?",
|
|
14
|
+
defaultValue: true,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB;QACE,EAAE,EAAE,eAAe;QACnB,MAAM,EAAE,2CAA2C;QACnD,YAAY,EAAE,IAAI;KACnB;CACO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spinner } from "@clack/prompts";
|
|
3
|
+
import { FileService } from "./services/file.service.js";
|
|
4
|
+
import { ProjectService } from "./services/project.service.js";
|
|
5
|
+
import { PromptService } from "./services/prompts.service.js";
|
|
6
|
+
import { TemplateService } from "./services/template.service.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
const promptService = new PromptService();
|
|
9
|
+
const fileService = new FileService();
|
|
10
|
+
const templateService = new TemplateService();
|
|
11
|
+
// 1. Collect every answer first — the spinner only starts after all prompts are done
|
|
12
|
+
const projectName = await promptService.askProjectName();
|
|
13
|
+
const language = await promptService.askLanguage();
|
|
14
|
+
const availableFeatures = await templateService.getAvailableFeatures(language);
|
|
15
|
+
const features = await promptService.askFeatureAnswers(availableFeatures);
|
|
16
|
+
const linter = await promptService.askLinter();
|
|
17
|
+
const formatter = await promptService.askFormatter();
|
|
18
|
+
const installDependencies = await promptService.askShouldInstall();
|
|
19
|
+
const answers = {
|
|
20
|
+
projectName,
|
|
21
|
+
language,
|
|
22
|
+
features,
|
|
23
|
+
linter,
|
|
24
|
+
formatter,
|
|
25
|
+
installDependencies,
|
|
26
|
+
};
|
|
27
|
+
// 2. Run the commands behind a spinner
|
|
28
|
+
const loading = spinner();
|
|
29
|
+
loading.start("Creating project");
|
|
30
|
+
try {
|
|
31
|
+
const projectPath = await fileService.createDirectory(projectName);
|
|
32
|
+
const projectService = new ProjectService(projectPath, answers);
|
|
33
|
+
await projectService.createProject();
|
|
34
|
+
if (installDependencies) {
|
|
35
|
+
loading.message("Installing dependencies");
|
|
36
|
+
await projectService.runInstallCommand();
|
|
37
|
+
}
|
|
38
|
+
loading.stop(`Created ${language} project at ${projectPath}`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
loading.error("Project creation failed");
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
main().catch((err) => {
|
|
46
|
+
const message = err instanceof Error ? err.message : "An unexpected error occurred";
|
|
47
|
+
console.error(`ExpressKit: ${message}`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,KAAK,UAAU,IAAI;IACjB,MAAM,aAAa,GAAG,IAAI,aAAa,EAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;IACtC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAE9C,qFAAqF;IACrF,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,cAAc,EAAE,CAAC;IACzD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC;IACnD,MAAM,iBAAiB,GACrB,MAAM,eAAe,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC;IAC/C,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,YAAY,EAAE,CAAC;IACrD,MAAM,mBAAmB,GAAG,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC;IAEnE,MAAM,OAAO,GAAmB;QAC9B,WAAW;QACX,QAAQ;QACR,QAAQ;QACR,MAAM;QACN,SAAS;QACT,mBAAmB;KACpB,CAAC;IAEF,uCAAuC;IACvC,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAElC,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,MAAM,WAAW,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACnE,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAChE,MAAM,cAAc,CAAC,aAAa,EAAE,CAAC;QAErC,IAAI,mBAAmB,EAAE,CAAC;YACxB,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;YAC3C,MAAM,cAAc,CAAC,iBAAiB,EAAE,CAAC;QAC3C,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,WAAW,QAAQ,eAAe,WAAW,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,MAAM,OAAO,GACX,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,8BAA8B,CAAC;IAEtE,OAAO,CAAC,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class FileService {
|
|
4
|
+
async createDirectory(projectName) {
|
|
5
|
+
const dirPath = path.resolve(process.cwd(), projectName);
|
|
6
|
+
try {
|
|
7
|
+
await mkdir(dirPath, { recursive: true });
|
|
8
|
+
return dirPath;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : "Unknown filesystem error";
|
|
12
|
+
throw new Error(`Failed to create project directory "${dirPath}": ${message}`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Writes the complete, already-composed project in one pass.
|
|
17
|
+
* Every destination is checked before the first file is written.
|
|
18
|
+
*/
|
|
19
|
+
async writeFiles(projectDirectory, files) {
|
|
20
|
+
const entries = Object.entries(files);
|
|
21
|
+
if (entries.length === 0) {
|
|
22
|
+
throw new Error("No files to create.");
|
|
23
|
+
}
|
|
24
|
+
const destinations = entries.map(([relativePath, contents]) => ({
|
|
25
|
+
relativePath,
|
|
26
|
+
contents,
|
|
27
|
+
absolutePath: this.resolveProjectPath(projectDirectory, relativePath),
|
|
28
|
+
}));
|
|
29
|
+
const existingFiles = await this.getExistingFiles(destinations);
|
|
30
|
+
if (existingFiles.length > 0) {
|
|
31
|
+
throw new Error(`Cannot create project: these files already exist in "${projectDirectory}": ${existingFiles.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
const results = await Promise.allSettled(destinations.map(({ absolutePath, contents }) => this.writeFile(absolutePath, contents)));
|
|
34
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
35
|
+
if (failures.length > 0) {
|
|
36
|
+
throw new Error(`Failed to create ${failures.length} of ${entries.length} files.`, { cause: failures.map((failure) => failure.reason) });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
resolveProjectPath(projectDirectory, relativePath) {
|
|
40
|
+
if (!relativePath.trim() || path.isAbsolute(relativePath)) {
|
|
41
|
+
throw new Error(`Generated file path must be a non-empty relative path: "${relativePath}".`);
|
|
42
|
+
}
|
|
43
|
+
const projectRoot = path.resolve(projectDirectory);
|
|
44
|
+
const destination = path.resolve(projectRoot, relativePath);
|
|
45
|
+
if (destination !== projectRoot &&
|
|
46
|
+
!destination.startsWith(`${projectRoot}${path.sep}`)) {
|
|
47
|
+
throw new Error(`Generated file path escapes the project directory: "${relativePath}".`);
|
|
48
|
+
}
|
|
49
|
+
return destination;
|
|
50
|
+
}
|
|
51
|
+
async getExistingFiles(files) {
|
|
52
|
+
const checks = files.map(async ({ relativePath, absolutePath }) => {
|
|
53
|
+
try {
|
|
54
|
+
await access(absolutePath);
|
|
55
|
+
return relativePath;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const results = await Promise.all(checks);
|
|
62
|
+
return results.filter((file) => file !== null);
|
|
63
|
+
}
|
|
64
|
+
async writeFile(filePath, contents) {
|
|
65
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
66
|
+
// "wx" fails if the file exists, so we never overwrite user files.
|
|
67
|
+
await writeFile(filePath, contents, {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
flag: "wx",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=file.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.service.js","sourceRoot":"","sources":["../../src/services/file.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { FileService } from "./file.service.js";
|
|
3
|
+
import { TemplateService } from "./template.service.js";
|
|
4
|
+
export class ProjectService {
|
|
5
|
+
projectPath;
|
|
6
|
+
answers;
|
|
7
|
+
fileService;
|
|
8
|
+
templateService;
|
|
9
|
+
constructor(projectPath, answers, fileService = new FileService(), templateService = new TemplateService()) {
|
|
10
|
+
this.projectPath = projectPath;
|
|
11
|
+
this.answers = answers;
|
|
12
|
+
this.fileService = fileService;
|
|
13
|
+
this.templateService = templateService;
|
|
14
|
+
}
|
|
15
|
+
async createProject() {
|
|
16
|
+
const files = await this.templateService.buildFilePlan(this.answers);
|
|
17
|
+
// Build the whole plan first, then write it in one pass.
|
|
18
|
+
await this.fileService.writeFiles(this.projectPath, files);
|
|
19
|
+
}
|
|
20
|
+
async runInstallCommand() {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn("npm", ["install"], {
|
|
23
|
+
cwd: this.projectPath, // run inside the NEW project, not where the CLI was launched
|
|
24
|
+
stdio: ["ignore", "ignore", "pipe"], // hide npm output from the spinner; stderr kept for errors
|
|
25
|
+
shell: process.platform === "win32",
|
|
26
|
+
});
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stderr?.on("data", (chunk) => {
|
|
29
|
+
stderr += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
child.on("error", reject);
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (code === 0) {
|
|
34
|
+
resolve();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const details = stderr.trim();
|
|
38
|
+
reject(new Error(`npm install failed with exit code ${code}${details ? `\n${details}` : ""}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=project.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.service.js","sourceRoot":"","sources":["../../src/services/project.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,OAAO,cAAc;IACR,WAAW,CAAS;IACpB,OAAO,CAAiB;IACxB,WAAW,CAAc;IACzB,eAAe,CAAkB;IAElD,YACE,WAAmB,EACnB,OAAuB,EACvB,WAAW,GAAG,IAAI,WAAW,EAAE,EAC/B,eAAe,GAAG,IAAI,eAAe,EAAE;QAEvC,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;IAEM,KAAK,CAAC,aAAa;QACxB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAErE,yDAAyD;QACzD,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC7D,CAAC;IAEM,KAAK,CAAC,iBAAiB;QAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE;gBACtC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,6DAA6D;gBACpF,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,2DAA2D;gBAChG,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;aACpC,CAAC,CAAC;YAEH,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACzC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7B,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAE1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE;gBACjC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,OAAO,EAAE,CAAC;gBACZ,CAAC;qBAAM,CAAC;oBACN,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;oBAE9B,MAAM,CACJ,IAAI,KAAK,CACP,qCAAqC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5E,CACF,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
2
|
+
import { FEATURES } from "../features.js";
|
|
3
|
+
export class PromptService {
|
|
4
|
+
LANGUAGE_OPTIONS = [
|
|
5
|
+
{ value: "javascript", label: "JavaScript" },
|
|
6
|
+
{ value: "typescript", label: "TypeScript" },
|
|
7
|
+
];
|
|
8
|
+
LINTER_OPTIONS = [
|
|
9
|
+
{ value: "none", label: "None" },
|
|
10
|
+
{ value: "eslint", label: "ESLint" },
|
|
11
|
+
{ value: "biome", label: "Biome" },
|
|
12
|
+
{ value: "oxlint", label: "Oxlint" },
|
|
13
|
+
];
|
|
14
|
+
FORMATTER_OPTIONS = [
|
|
15
|
+
{ value: "none", label: "None" },
|
|
16
|
+
{ value: "prettier", label: "Prettier" },
|
|
17
|
+
{ value: "biome", label: "Biome" },
|
|
18
|
+
];
|
|
19
|
+
async askProjectName() {
|
|
20
|
+
const response = await text({
|
|
21
|
+
message: "What would you like to name your project?",
|
|
22
|
+
placeholder: "my-api",
|
|
23
|
+
defaultValue: ".",
|
|
24
|
+
validate(value) {
|
|
25
|
+
if (!value?.trim()) {
|
|
26
|
+
return "Please enter a project name or '.' to use the current directory.";
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
this.handleCancel(response);
|
|
31
|
+
return response.trim();
|
|
32
|
+
}
|
|
33
|
+
async askLanguage() {
|
|
34
|
+
const response = await select({
|
|
35
|
+
message: "Which language would you like to use?",
|
|
36
|
+
options: this.LANGUAGE_OPTIONS,
|
|
37
|
+
initialValue: "javascript",
|
|
38
|
+
});
|
|
39
|
+
this.handleCancel(response);
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Asks one confirm per registered feature available for the chosen
|
|
44
|
+
* language. Unavailable features default to false.
|
|
45
|
+
*/
|
|
46
|
+
async askFeatureAnswers(availableFeatures) {
|
|
47
|
+
const answers = Object.fromEntries(FEATURES.map((feature) => [feature.id, false]));
|
|
48
|
+
for (const feature of availableFeatures) {
|
|
49
|
+
const response = await confirm({
|
|
50
|
+
message: feature.prompt,
|
|
51
|
+
initialValue: feature.defaultValue,
|
|
52
|
+
});
|
|
53
|
+
this.handleCancel(response);
|
|
54
|
+
answers[feature.id] = response;
|
|
55
|
+
}
|
|
56
|
+
return answers;
|
|
57
|
+
}
|
|
58
|
+
async askLinter() {
|
|
59
|
+
const response = await select({
|
|
60
|
+
message: "Which linter would you like to use?",
|
|
61
|
+
options: this.LINTER_OPTIONS,
|
|
62
|
+
initialValue: "oxlint",
|
|
63
|
+
});
|
|
64
|
+
this.handleCancel(response);
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
async askFormatter() {
|
|
68
|
+
const response = await select({
|
|
69
|
+
message: "Which formatter would you like to use?",
|
|
70
|
+
options: this.FORMATTER_OPTIONS,
|
|
71
|
+
initialValue: "biome",
|
|
72
|
+
});
|
|
73
|
+
this.handleCancel(response);
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
async askShouldInstall() {
|
|
77
|
+
const response = await confirm({
|
|
78
|
+
message: "Install dependencies?",
|
|
79
|
+
initialValue: true,
|
|
80
|
+
});
|
|
81
|
+
this.handleCancel(response);
|
|
82
|
+
return response;
|
|
83
|
+
}
|
|
84
|
+
// Catches Ctrl+C / Esc so the CLI exits cleanly instead of crashing.
|
|
85
|
+
handleCancel(response) {
|
|
86
|
+
if (isCancel(response)) {
|
|
87
|
+
cancel("Project setup cancelled.");
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=prompts.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.service.js","sourceRoot":"","sources":["../../src/services/prompts.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { FEATURES } from "../features.js";
|
|
5
|
+
const PACKAGE_SECTIONS = [
|
|
6
|
+
"dependencies",
|
|
7
|
+
"devDependencies",
|
|
8
|
+
"scripts",
|
|
9
|
+
];
|
|
10
|
+
export class TemplateService {
|
|
11
|
+
templateRoot;
|
|
12
|
+
constructor(templateRoot) {
|
|
13
|
+
if (templateRoot) {
|
|
14
|
+
this.templateRoot = path.resolve(templateRoot);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const serviceDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
// templates/ sits two levels up from src/services (and dist/services).
|
|
19
|
+
this.templateRoot = path.resolve(serviceDirectory, "../../templates");
|
|
20
|
+
}
|
|
21
|
+
async getAvailableFeatures(language) {
|
|
22
|
+
const languageDirectory = path.join(this.templateRoot, language);
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = await readdir(languageDirectory, { withFileTypes: true });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`Unable to read templates for "${language}" at "${languageDirectory}".`, { cause: error });
|
|
29
|
+
}
|
|
30
|
+
const folders = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
|
|
31
|
+
return FEATURES.filter((feature) => folders.has(feature.id));
|
|
32
|
+
}
|
|
33
|
+
resolveToolLayers(linter, formatter) {
|
|
34
|
+
if (linter === "biome" && formatter === "biome") {
|
|
35
|
+
return ["format/biome/both"];
|
|
36
|
+
}
|
|
37
|
+
const layers = [];
|
|
38
|
+
switch (linter) {
|
|
39
|
+
case "oxlint":
|
|
40
|
+
layers.push("lint/oxlint");
|
|
41
|
+
break;
|
|
42
|
+
case "eslint":
|
|
43
|
+
layers.push("lint/eslint");
|
|
44
|
+
break;
|
|
45
|
+
case "biome":
|
|
46
|
+
layers.push("lint/biome");
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
switch (formatter) {
|
|
50
|
+
case "prettier":
|
|
51
|
+
layers.push("format/prettier");
|
|
52
|
+
break;
|
|
53
|
+
case "biome":
|
|
54
|
+
layers.push("format/biome/format");
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
return layers;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Builds the complete project in memory.
|
|
61
|
+
*
|
|
62
|
+
* Base files come first. Each enabled feature is then layered on top:
|
|
63
|
+
* feature files replace base files with the same relative path, while new
|
|
64
|
+
* paths are added. Tool layers (linters and formatters) are applied next.
|
|
65
|
+
* package.json is the exception — feature and tool package.json files are
|
|
66
|
+
* fragments merged into the base package.json.
|
|
67
|
+
*/
|
|
68
|
+
async buildFilePlan(answers) {
|
|
69
|
+
const baseFiles = await this.readLayer(answers.language, "base");
|
|
70
|
+
const basePackageSource = baseFiles["package.json"];
|
|
71
|
+
if (basePackageSource === undefined) {
|
|
72
|
+
throw new Error(`Base template for "${answers.language}" is missing package.json.`);
|
|
73
|
+
}
|
|
74
|
+
let packageJson = this.parseJson(basePackageSource, `${answers.language}/base/package.json`);
|
|
75
|
+
let files = { ...baseFiles };
|
|
76
|
+
for (const feature of FEATURES) {
|
|
77
|
+
if (!answers.features[feature.id]) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const applied = await this.applyLayer(path.join(answers.language, feature.id), files, packageJson);
|
|
81
|
+
files = applied.files;
|
|
82
|
+
packageJson = applied.packageJson;
|
|
83
|
+
}
|
|
84
|
+
for (const toolLayer of this.resolveToolLayers(answers.linter, answers.formatter)) {
|
|
85
|
+
const applied = await this.applyLayer(toolLayer, files, packageJson);
|
|
86
|
+
files = applied.files;
|
|
87
|
+
packageJson = applied.packageJson;
|
|
88
|
+
}
|
|
89
|
+
if (answers.linter === "eslint" && answers.formatter === "prettier") {
|
|
90
|
+
packageJson = this.mergePackageJson(packageJson, {
|
|
91
|
+
devDependencies: {
|
|
92
|
+
"eslint-config-prettier": "^10.1.8",
|
|
93
|
+
},
|
|
94
|
+
}, "eslint-prettier-compatibility");
|
|
95
|
+
}
|
|
96
|
+
files["package.json"] = `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
async readLayer(...segments) {
|
|
100
|
+
const layerDirectory = path.join(this.templateRoot, ...segments);
|
|
101
|
+
try {
|
|
102
|
+
await access(layerDirectory);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`Template folder not found: "${segments.join("/")}".`, {
|
|
106
|
+
cause: error,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return this.readDirectory(layerDirectory);
|
|
110
|
+
}
|
|
111
|
+
async applyLayer(layerPath, files, packageJson) {
|
|
112
|
+
const layer = await this.readLayer(layerPath);
|
|
113
|
+
const fragmentSource = layer["package.json"];
|
|
114
|
+
let updatedPackageJson = packageJson;
|
|
115
|
+
if (fragmentSource !== undefined) {
|
|
116
|
+
const fragment = this.parsePackageFragment(fragmentSource, layerPath);
|
|
117
|
+
updatedPackageJson = this.mergePackageJson(packageJson, fragment, layerPath);
|
|
118
|
+
// The merged package.json is written last, not copied from the layer.
|
|
119
|
+
delete layer["package.json"];
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
files: { ...files, ...layer },
|
|
123
|
+
packageJson: updatedPackageJson,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async readDirectory(root, relativeDirectory = "") {
|
|
127
|
+
const directory = path.join(root, relativeDirectory);
|
|
128
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
129
|
+
const files = {};
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const relativePath = relativeDirectory
|
|
132
|
+
? path.posix.join(relativeDirectory, entry.name)
|
|
133
|
+
: entry.name;
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
Object.assign(files, await this.readDirectory(root, relativePath));
|
|
136
|
+
}
|
|
137
|
+
else if (entry.isFile()) {
|
|
138
|
+
files[relativePath] = await readFile(path.join(root, relativePath), "utf8");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return files;
|
|
142
|
+
}
|
|
143
|
+
parsePackageFragment(source, featureId) {
|
|
144
|
+
const parsed = this.parseJson(source, `${featureId}/package.json`);
|
|
145
|
+
const unknownKeys = Object.keys(parsed).filter((key) => !PACKAGE_SECTIONS.includes(key));
|
|
146
|
+
if (unknownKeys.length > 0) {
|
|
147
|
+
throw new Error(`Feature "${featureId}" has unsupported package.json keys: ${unknownKeys.join(", ")}. Only ${PACKAGE_SECTIONS.join(", ")} can be merged.`);
|
|
148
|
+
}
|
|
149
|
+
const fragment = {};
|
|
150
|
+
for (const section of PACKAGE_SECTIONS) {
|
|
151
|
+
const value = parsed[section];
|
|
152
|
+
if (value === undefined) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!this.isStringRecord(value)) {
|
|
156
|
+
throw new Error(`"${section}" in feature "${featureId}" package.json must be an object of string values.`);
|
|
157
|
+
}
|
|
158
|
+
fragment[section] = value;
|
|
159
|
+
}
|
|
160
|
+
return fragment;
|
|
161
|
+
}
|
|
162
|
+
mergePackageJson(base, fragment, featureId) {
|
|
163
|
+
const merged = { ...base };
|
|
164
|
+
for (const section of PACKAGE_SECTIONS) {
|
|
165
|
+
const additions = fragment[section];
|
|
166
|
+
if (!additions) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const target = merged[section]
|
|
170
|
+
? { ...merged[section] }
|
|
171
|
+
: {};
|
|
172
|
+
for (const [name, value] of Object.entries(additions)) {
|
|
173
|
+
const existing = target[name];
|
|
174
|
+
if (existing !== undefined && existing !== value) {
|
|
175
|
+
throw new Error(`Feature "${featureId}" conflicts with package.json ${section} entry "${name}": "${existing}" (existing) vs "${value}" (feature).`);
|
|
176
|
+
}
|
|
177
|
+
target[name] = value;
|
|
178
|
+
}
|
|
179
|
+
merged[section] = target;
|
|
180
|
+
}
|
|
181
|
+
return merged;
|
|
182
|
+
}
|
|
183
|
+
isStringRecord(value) {
|
|
184
|
+
return (typeof value === "object" &&
|
|
185
|
+
value !== null &&
|
|
186
|
+
!Array.isArray(value) &&
|
|
187
|
+
Object.values(value).every((item) => typeof item === "string"));
|
|
188
|
+
}
|
|
189
|
+
parseJson(source, templatePath) {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(source);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new Error(`Invalid JSON in template "${templatePath}".`, {
|
|
195
|
+
cause: error,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=template.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.service.js","sourceRoot":"","sources":["../../src/services/template.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAgB,MAAM,gBAAgB,CAAC;AAYxD,MAAM,gBAAgB,GAAG;IACvB,cAAc;IACd,iBAAiB;IACjB,SAAS;CACD,CAAC;AAIX,MAAM,OAAO,eAAe;IACT,YAAY,CAAS;IAEtC,YAAY,YAAqB;QAC/B,IAAI,YAAY,EAAE,CAAC;YACjB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YAC/C,OAAO;QACT,CAAC;QAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAEtE,uEAAuE;QACvE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAC;IACxE,CAAC;IAEM,KAAK,CAAC,oBAAoB,CAAC,QAAkB;QAClD,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAEjE,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,iBAAiB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,iCAAiC,QAAQ,SAAS,iBAAiB,IAAI,EACvE,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAC1E,CAAC;QAEF,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC;IAEM,iBAAiB,CAAC,MAAc,EAAE,SAAoB;QAC3D,IAAI,MAAM,KAAK,OAAO,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;YAChD,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC/B,CAAC;QAED,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,QAAQ,MAAM,EAAE,CAAC;YACf,KAAK,QAAQ;gBACX,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC3B,MAAM;YACR,KAAK,QAAQ;gBACX,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC3B,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBAC1B,MAAM;QACV,CAAC;QAED,QAAQ,SAAS,EAAE,CAAC;YAClB,KAAK,UAAU;gBACb,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;gBAC/B,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;gBACnC,MAAM;QACV,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;OAQG;IACI,KAAK,CAAC,aAAa,CAAC,OAAuB;QAChD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjE,MAAM,iBAAiB,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC;QAEpD,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,sBAAsB,OAAO,CAAC,QAAQ,4BAA4B,CACnE,CAAC;QACJ,CAAC;QAED,IAAI,WAAW,GAAG,IAAI,CAAC,SAAS,CAC9B,iBAAiB,EACjB,GAAG,OAAO,CAAC,QAAQ,oBAAoB,CACxC,CAAC;QAEF,IAAI,KAAK,GAAmB,EAAE,GAAG,SAAS,EAAE,CAAC;QAE7C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;gBAClC,SAAS;YACX,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CACnC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,EACvC,KAAK,EACL,WAAW,CACZ,CAAC;YACF,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;YACtB,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QACpC,CAAC;QAED,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,iBAAiB,CAC5C,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,SAAS,CAClB,EAAE,CAAC;YACF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;YACrE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;YACtB,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QACpC,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;YACpE,WAAW,GAAG,IAAI,CAAC,gBAAgB,CACjC,WAAW,EACX;gBACE,eAAe,EAAE;oBACf,wBAAwB,EAAE,SAAS;iBACpC;aACF,EACD,+BAA+B,CAChC,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,cAAc,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QAEpE,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,GAAG,QAAkB;QAC3C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,QAAQ,CAAC,CAAC;QAEjE,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;gBACrE,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,SAAiB,EACjB,KAAqB,EACrB,WAAwB;QAExB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;QAE7C,IAAI,kBAAkB,GAAG,WAAW,CAAC;QAErC,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;YAEtE,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,CACxC,WAAW,EACX,QAAQ,EACR,SAAS,CACV,CAAC;YAEF,sEAAsE;YACtE,OAAO,KAAK,CAAC,cAAc,CAAC,CAAC;QAC/B,CAAC;QAED,OAAO;YACL,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,EAAE;YAC7B,WAAW,EAAE,kBAAkB;SAChC,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,aAAa,CACzB,IAAY,EACZ,iBAAiB,GAAG,EAAE;QAEtB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,MAAM,KAAK,GAAmB,EAAE,CAAC;QAEjC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,YAAY,GAAG,iBAAiB;gBACpC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,IAAI,CAAC;gBAChD,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YAEf,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;YACrE,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC1B,KAAK,CAAC,YAAY,CAAC,GAAG,MAAM,QAAQ,CAClC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,EAC7B,MAAM,CACP,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,oBAAoB,CAC1B,MAAc,EACd,SAAiB;QAEjB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAC3B,MAAM,EACN,GAAG,SAAS,eAAe,CAC5B,CAAC;QAEF,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAC5C,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAqB,CAAC,CAC3D,CAAC;QAEF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,YAAY,SAAS,wCAAwC,WAAW,CAAC,IAAI,CAC3E,IAAI,CACL,UAAU,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CACxD,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAoB,EAAE,CAAC;QAErC,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAE9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,SAAS;YACX,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CACb,IAAI,OAAO,iBAAiB,SAAS,oDAAoD,CAC1F,CAAC;YACJ,CAAC;YAED,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QAC5B,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,gBAAgB,CACtB,IAAiB,EACjB,QAAyB,EACzB,SAAiB;QAEjB,MAAM,MAAM,GAAgB,EAAE,GAAG,IAAI,EAAE,CAAC;QAExC,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;YACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;YAEpC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAA2B,MAAM,CAAC,OAAO,CAAC;gBACpD,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE;gBACxB,CAAC,CAAC,EAAE,CAAC;YAEP,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;gBAE9B,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;oBACjD,MAAM,IAAI,KAAK,CACb,YAAY,SAAS,iCAAiC,OAAO,WAAW,IAAI,OAAO,QAAQ,oBAAoB,KAAK,cAAc,CACnI,CAAC;gBACJ,CAAC;gBAED,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YACvB,CAAC;YAED,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;QAC3B,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,cAAc,CAAC,KAAc;QACnC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACd,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACrB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAC/D,CAAC;IACJ,CAAC;IAEO,SAAS,CAAI,MAAc,EAAE,YAAoB;QACvD,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAM,CAAC;QACjC,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6BAA6B,YAAY,IAAI,EAAE;gBAC7D,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF"}
|