@sha3/code 1.0.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/AGENTS.md +75 -0
- package/README.md +554 -0
- package/ai/adapters/codex.md +7 -0
- package/ai/adapters/copilot.md +7 -0
- package/ai/adapters/cursor.md +7 -0
- package/ai/adapters/windsurf.md +8 -0
- package/ai/constitution.md +12 -0
- package/bin/code-standards.mjs +47 -0
- package/biome.json +37 -0
- package/index.mjs +11 -0
- package/lib/cli/parse-args.mjs +416 -0
- package/lib/cli/post-run-guidance.mjs +43 -0
- package/lib/cli/run-init.mjs +123 -0
- package/lib/cli/run-profile.mjs +46 -0
- package/lib/cli/run-refactor.mjs +152 -0
- package/lib/cli/run-verify.mjs +67 -0
- package/lib/constants.mjs +167 -0
- package/lib/contract/load-rule-catalog.mjs +12 -0
- package/lib/contract/render-agents.mjs +79 -0
- package/lib/contract/render-contract-json.mjs +7 -0
- package/lib/contract/resolve-contract.mjs +52 -0
- package/lib/paths.mjs +50 -0
- package/lib/profile.mjs +108 -0
- package/lib/project/ai-instructions.mjs +28 -0
- package/lib/project/biome-ignore.mjs +14 -0
- package/lib/project/managed-files.mjs +105 -0
- package/lib/project/package-metadata.mjs +132 -0
- package/lib/project/prompt-files.mjs +111 -0
- package/lib/project/template-resolution.mjs +70 -0
- package/lib/refactor/materialize-refactor-context.mjs +106 -0
- package/lib/refactor/preservation-questions.mjs +33 -0
- package/lib/refactor/public-contract-extractor.mjs +22 -0
- package/lib/refactor/render-analysis-summary.mjs +50 -0
- package/lib/refactor/source-analysis.mjs +74 -0
- package/lib/utils/fs.mjs +220 -0
- package/lib/utils/prompts.mjs +63 -0
- package/lib/utils/text.mjs +43 -0
- package/lib/verify/change-audit-verifier.mjs +140 -0
- package/lib/verify/change-context.mjs +36 -0
- package/lib/verify/error-handling-verifier.mjs +164 -0
- package/lib/verify/explain-rule.mjs +54 -0
- package/lib/verify/issue-helpers.mjs +132 -0
- package/lib/verify/project-layout-verifier.mjs +259 -0
- package/lib/verify/project-verifier.mjs +267 -0
- package/lib/verify/readme-public-api.mjs +237 -0
- package/lib/verify/readme-verifier.mjs +216 -0
- package/lib/verify/render-json-report.mjs +3 -0
- package/lib/verify/render-text-report.mjs +34 -0
- package/lib/verify/source-analysis.mjs +126 -0
- package/lib/verify/source-rule-verifier.mjs +453 -0
- package/lib/verify/testing-verifier.mjs +113 -0
- package/lib/verify/tooling-verifier.mjs +82 -0
- package/lib/verify/typescript-style-verifier.mjs +407 -0
- package/package.json +55 -0
- package/profiles/default.profile.json +40 -0
- package/profiles/schema.json +96 -0
- package/prompts/init-contract.md +25 -0
- package/prompts/init-phase-2-implement.md +25 -0
- package/prompts/init-phase-3-verify.md +23 -0
- package/prompts/init.prompt.md +24 -0
- package/prompts/refactor-contract.md +26 -0
- package/prompts/refactor-phase-2-rebuild.md +25 -0
- package/prompts/refactor-phase-3-verify.md +24 -0
- package/prompts/refactor.prompt.md +26 -0
- package/resources/ai/AGENTS.md +18 -0
- package/resources/ai/adapters/codex.md +5 -0
- package/resources/ai/adapters/copilot.md +5 -0
- package/resources/ai/adapters/cursor.md +5 -0
- package/resources/ai/adapters/windsurf.md +5 -0
- package/resources/ai/contract.schema.json +68 -0
- package/resources/ai/rule-catalog.json +878 -0
- package/resources/ai/rule-catalog.schema.json +66 -0
- package/resources/ai/templates/adapters/codex.template.md +7 -0
- package/resources/ai/templates/adapters/copilot.template.md +7 -0
- package/resources/ai/templates/adapters/cursor.template.md +7 -0
- package/resources/ai/templates/adapters/windsurf.template.md +7 -0
- package/resources/ai/templates/agents.project.template.md +141 -0
- package/resources/ai/templates/examples/demo/src/billing/billing.service.ts +73 -0
- package/resources/ai/templates/examples/demo/src/config.ts +3 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.errors.ts +51 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.service.ts +96 -0
- package/resources/ai/templates/examples/demo/src/invoice/invoice.types.ts +9 -0
- package/resources/ai/templates/examples/rules/async-bad.ts +52 -0
- package/resources/ai/templates/examples/rules/async-good.ts +56 -0
- package/resources/ai/templates/examples/rules/class-first-bad.ts +36 -0
- package/resources/ai/templates/examples/rules/class-first-good.ts +74 -0
- package/resources/ai/templates/examples/rules/constructor-bad.ts +68 -0
- package/resources/ai/templates/examples/rules/constructor-good.ts +71 -0
- package/resources/ai/templates/examples/rules/control-flow-bad.ts +31 -0
- package/resources/ai/templates/examples/rules/control-flow-good.ts +54 -0
- package/resources/ai/templates/examples/rules/errors-bad.ts +42 -0
- package/resources/ai/templates/examples/rules/errors-good.ts +23 -0
- package/resources/ai/templates/examples/rules/functions-bad.ts +48 -0
- package/resources/ai/templates/examples/rules/functions-good.ts +58 -0
- package/resources/ai/templates/examples/rules/returns-bad.ts +38 -0
- package/resources/ai/templates/examples/rules/returns-good.ts +44 -0
- package/resources/ai/templates/examples/rules/testing-bad.ts +34 -0
- package/resources/ai/templates/examples/rules/testing-good.ts +54 -0
- package/resources/ai/templates/rules/architecture.md +41 -0
- package/resources/ai/templates/rules/async.md +13 -0
- package/resources/ai/templates/rules/class-first.md +45 -0
- package/resources/ai/templates/rules/control-flow.md +13 -0
- package/resources/ai/templates/rules/errors.md +18 -0
- package/resources/ai/templates/rules/functions.md +29 -0
- package/resources/ai/templates/rules/naming.md +13 -0
- package/resources/ai/templates/rules/readme.md +36 -0
- package/resources/ai/templates/rules/returns.md +13 -0
- package/resources/ai/templates/rules/testing.md +18 -0
- package/resources/ai/templates/rules.project.template.md +66 -0
- package/resources/ai/templates/skills/change-synchronization/SKILL.md +42 -0
- package/resources/ai/templates/skills/feature-shaping/SKILL.md +45 -0
- package/resources/ai/templates/skills/http-api-conventions/SKILL.md +171 -0
- package/resources/ai/templates/skills/init-workflow/SKILL.md +52 -0
- package/resources/ai/templates/skills/readme-authoring/SKILL.md +51 -0
- package/resources/ai/templates/skills/refactor-workflow/SKILL.md +50 -0
- package/resources/ai/templates/skills/simplicity-audit/SKILL.md +41 -0
- package/resources/ai/templates/skills/test-scope-selection/SKILL.md +50 -0
- package/resources/ai/templates/skills.index.template.md +25 -0
- package/standards/architecture.md +72 -0
- package/standards/changelog-policy.md +12 -0
- package/standards/manifest.json +36 -0
- package/standards/readme.md +56 -0
- package/standards/schema.json +124 -0
- package/standards/style.md +106 -0
- package/standards/testing.md +20 -0
- package/standards/tooling.md +38 -0
- package/templates/node-lib/.biomeignore +10 -0
- package/templates/node-lib/.vscode/extensions.json +1 -0
- package/templates/node-lib/.vscode/settings.json +9 -0
- package/templates/node-lib/README.md +172 -0
- package/templates/node-lib/biome.json +37 -0
- package/templates/node-lib/gitignore +6 -0
- package/templates/node-lib/package.json +32 -0
- package/templates/node-lib/scripts/release-publish.mjs +106 -0
- package/templates/node-lib/scripts/run-tests.mjs +65 -0
- package/templates/node-lib/src/config.ts +3 -0
- package/templates/node-lib/src/index.ts +2 -0
- package/templates/node-lib/src/logger.ts +7 -0
- package/templates/node-lib/src/package-info/package-info.service.ts +47 -0
- package/templates/node-lib/test/package-info.test.ts +10 -0
- package/templates/node-lib/tsconfig.build.json +1 -0
- package/templates/node-lib/tsconfig.json +5 -0
- package/templates/node-service/.biomeignore +10 -0
- package/templates/node-service/.vscode/extensions.json +1 -0
- package/templates/node-service/.vscode/settings.json +9 -0
- package/templates/node-service/README.md +244 -0
- package/templates/node-service/biome.json +37 -0
- package/templates/node-service/ecosystem.config.cjs +3 -0
- package/templates/node-service/gitignore +6 -0
- package/templates/node-service/package.json +42 -0
- package/templates/node-service/scripts/release-publish.mjs +106 -0
- package/templates/node-service/scripts/run-tests.mjs +65 -0
- package/templates/node-service/src/app/service-runtime.service.ts +57 -0
- package/templates/node-service/src/app-info/app-info.service.ts +47 -0
- package/templates/node-service/src/config.ts +11 -0
- package/templates/node-service/src/http/http-server.service.ts +66 -0
- package/templates/node-service/src/index.ts +2 -0
- package/templates/node-service/src/logger.ts +7 -0
- package/templates/node-service/src/main.ts +5 -0
- package/templates/node-service/test/service-runtime.test.ts +13 -0
- package/templates/node-service/tsconfig.build.json +1 -0
- package/templates/node-service/tsconfig.json +5 -0
- package/tsconfig/base.json +16 -0
- package/tsconfig/node-lib.json +5 -0
- package/tsconfig/node-service.json +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# 🚀 {{packageName}}
|
|
2
|
+
|
|
3
|
+
HTTP service package with one runtime entrypoint for composing or starting a Hono server.
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run check
|
|
10
|
+
npm run start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { ServiceRuntime } from "{{packageName}}";
|
|
15
|
+
|
|
16
|
+
const serviceRuntime = ServiceRuntime.createDefault();
|
|
17
|
+
const server = serviceRuntime.buildServer();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Why
|
|
21
|
+
|
|
22
|
+
Use this package when you want one small runtime surface for booting the service and one small HTTP boundary that is easy to test.
|
|
23
|
+
It keeps startup, route wiring, and the default response contract behind a public runtime class so callers do not need to understand the internal composition.
|
|
24
|
+
|
|
25
|
+
## Main Capabilities
|
|
26
|
+
|
|
27
|
+
- Builds the HTTP server without binding a port when tests or orchestration code need control.
|
|
28
|
+
- Handles a single request without opening a socket when tests only need the HTTP contract.
|
|
29
|
+
- Starts the default runtime with one public method when the process should listen immediately.
|
|
30
|
+
- Exposes the root endpoint payload as a public type so other code can depend on the response contract.
|
|
31
|
+
- Keeps transport concerns small and explicit around Hono.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Running Locally
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The default runtime listens on `http://localhost:3000`.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { ServiceRuntime } from "{{packageName}}";
|
|
51
|
+
|
|
52
|
+
const serviceRuntime = ServiceRuntime.createDefault();
|
|
53
|
+
const server = serviceRuntime.startServer();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This is the intended runtime integration path: construct the default runtime once, then either build or start the Hono-backed server depending on who owns process startup.
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
Build the HTTP server without listening:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { ServiceRuntime } from "{{packageName}}";
|
|
64
|
+
|
|
65
|
+
const serviceRuntime = ServiceRuntime.createDefault();
|
|
66
|
+
const server = serviceRuntime.buildServer();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Handle one request directly:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { ServiceRuntime } from "{{packageName}}";
|
|
73
|
+
|
|
74
|
+
const serviceRuntime = ServiceRuntime.createDefault();
|
|
75
|
+
const response = await serviceRuntime.handleRequest(new Request("http://localhost/"));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Call the default endpoint:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl http://localhost:3000/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## HTTP API
|
|
85
|
+
|
|
86
|
+
### `GET /`
|
|
87
|
+
|
|
88
|
+
Returns the default service info payload from the default Hono route.
|
|
89
|
+
|
|
90
|
+
Response shape:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"ok": true,
|
|
95
|
+
"serviceName": "service-name"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Behavior notes:
|
|
100
|
+
|
|
101
|
+
- responds with status `200`
|
|
102
|
+
- returns JSON
|
|
103
|
+
- is implemented with `hono`
|
|
104
|
+
- uses the `content-type` configured in `src/config.ts`
|
|
105
|
+
|
|
106
|
+
## Public API
|
|
107
|
+
|
|
108
|
+
### `ServiceRuntime`
|
|
109
|
+
|
|
110
|
+
Primary runtime entrypoint for composing or starting the service.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { ServiceRuntime } from "{{packageName}}";
|
|
114
|
+
|
|
115
|
+
const serviceRuntime = ServiceRuntime.createDefault();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `createDefault()`
|
|
119
|
+
|
|
120
|
+
Creates a `ServiceRuntime` with the default Hono HTTP server wiring.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
|
|
124
|
+
- none
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
|
|
128
|
+
- a ready-to-use `ServiceRuntime`
|
|
129
|
+
|
|
130
|
+
Behavior notes:
|
|
131
|
+
|
|
132
|
+
- wires the default Hono HTTP server stack
|
|
133
|
+
- keeps configuration centralized in `src/config.ts`
|
|
134
|
+
|
|
135
|
+
#### `buildServer()`
|
|
136
|
+
|
|
137
|
+
Builds the Hono Node server without binding a port.
|
|
138
|
+
|
|
139
|
+
Parameters:
|
|
140
|
+
|
|
141
|
+
- none
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
|
|
145
|
+
- a Hono Node `ServerType` instance
|
|
146
|
+
|
|
147
|
+
Behavior notes:
|
|
148
|
+
|
|
149
|
+
- useful for integration tests and manual orchestration
|
|
150
|
+
- does not call `listen()`
|
|
151
|
+
- returns the exact server instance that `startServer()` would later bind
|
|
152
|
+
|
|
153
|
+
#### `handleRequest(request)`
|
|
154
|
+
|
|
155
|
+
Runs one request through the default Hono pipeline without binding a port.
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
|
|
159
|
+
- `request`: standard `Request` object routed through the service
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
|
|
163
|
+
- a `Promise<Response>` with the route result
|
|
164
|
+
|
|
165
|
+
Behavior notes:
|
|
166
|
+
|
|
167
|
+
- useful for tests that only need to validate HTTP behavior
|
|
168
|
+
- avoids opening a socket during request handling checks
|
|
169
|
+
- uses the same route wiring as the default runtime
|
|
170
|
+
|
|
171
|
+
#### `startServer()`
|
|
172
|
+
|
|
173
|
+
Builds the HTTP server and starts listening on `config.DEFAULT_PORT`.
|
|
174
|
+
|
|
175
|
+
Parameters:
|
|
176
|
+
|
|
177
|
+
- none
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
|
|
181
|
+
- the started Hono Node `ServerType` instance
|
|
182
|
+
|
|
183
|
+
Behavior notes:
|
|
184
|
+
|
|
185
|
+
- logs the listening address through the package logger
|
|
186
|
+
- binds immediately to the configured default port
|
|
187
|
+
- keeps startup behavior centralized in one public method
|
|
188
|
+
|
|
189
|
+
### `AppInfoPayload`
|
|
190
|
+
|
|
191
|
+
Public type for the default root payload returned by the root endpoint.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
type AppInfoPayload = { ok: true; serviceName: string };
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Compatibility
|
|
198
|
+
|
|
199
|
+
- Node.js 20+
|
|
200
|
+
- ESM (`"type": "module"`)
|
|
201
|
+
- Strict TypeScript
|
|
202
|
+
|
|
203
|
+
## Configuration
|
|
204
|
+
|
|
205
|
+
Configuration is centralized in `src/config.ts`.
|
|
206
|
+
|
|
207
|
+
- `config.RESPONSE_CONTENT_TYPE`: response `content-type` header sent by the root endpoint.
|
|
208
|
+
- `config.DEFAULT_PORT`: port used by `startServer()` when the service binds locally.
|
|
209
|
+
- `config.SERVICE_NAME`: service name included in the root endpoint payload.
|
|
210
|
+
|
|
211
|
+
## Scripts
|
|
212
|
+
|
|
213
|
+
- `npm run standards:check`: verify deterministic project contract rules
|
|
214
|
+
- `npm run check`: standards + lint + format + typecheck + tests
|
|
215
|
+
- `npm run fix`: Biome autofix + format write
|
|
216
|
+
- `npm run start`: launch the service with `tsx`
|
|
217
|
+
- `npm run build`: compile the module output to `dist/`
|
|
218
|
+
|
|
219
|
+
## Structure
|
|
220
|
+
|
|
221
|
+
- `src/config.ts`: canonical runtime configuration
|
|
222
|
+
- `src/app-info/app-info.service.ts`: builds the root endpoint payload
|
|
223
|
+
- `src/http/http-server.service.ts`: constructs the Hono HTTP server
|
|
224
|
+
- `src/app/service-runtime.service.ts`: public runtime orchestration
|
|
225
|
+
- `src/main.ts`: bootstrap entrypoint
|
|
226
|
+
- `test/service-runtime.test.ts`: behavior test
|
|
227
|
+
|
|
228
|
+
## Troubleshooting
|
|
229
|
+
|
|
230
|
+
### Port conflicts
|
|
231
|
+
|
|
232
|
+
Override `PORT` in `.env` or your shell before running `npm run start`.
|
|
233
|
+
|
|
234
|
+
### Standards warnings
|
|
235
|
+
|
|
236
|
+
Run `npm run standards:check` to detect contract errors, README coverage gaps, and advisory warnings from the project verifier.
|
|
237
|
+
|
|
238
|
+
## AI Workflow
|
|
239
|
+
|
|
240
|
+
- Read `AGENTS.md`, `ai/contract.json`, and the relevant `ai/<assistant>.md` before coding.
|
|
241
|
+
- Keep managed contract/tooling files read-only during feature work.
|
|
242
|
+
- Keep this README focused on the runtime surface, HTTP contract, configuration, and public API.
|
|
243
|
+
- Rewrite examples and endpoint notes whenever exported behavior changes.
|
|
244
|
+
- Run `npm run check` before finalizing changes.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"ignoreUnknown": true,
|
|
4
|
+
"ignore": [".code-standards", "dist"]
|
|
5
|
+
},
|
|
6
|
+
"formatter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"indentStyle": "space",
|
|
9
|
+
"indentWidth": 2,
|
|
10
|
+
"lineWidth": 160
|
|
11
|
+
},
|
|
12
|
+
"linter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"rules": {
|
|
15
|
+
"recommended": true,
|
|
16
|
+
"correctness": {
|
|
17
|
+
"noUnusedFunctionParameters": "error",
|
|
18
|
+
"noUnusedImports": "error",
|
|
19
|
+
"noUnusedPrivateClassMembers": "error",
|
|
20
|
+
"noUnusedVariables": "error"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"javascript": {
|
|
25
|
+
"formatter": {
|
|
26
|
+
"quoteStyle": "double",
|
|
27
|
+
"semicolons": "always",
|
|
28
|
+
"arrowParentheses": "always",
|
|
29
|
+
"bracketSpacing": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"json": {
|
|
33
|
+
"formatter": {
|
|
34
|
+
"enabled": true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node --import tsx src/main.ts",
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"check": "npm run standards:check && npm run lint && npm run format:check && npm run typecheck && npm run test",
|
|
19
|
+
"fix": "npm run lint:fix && npm run format:write",
|
|
20
|
+
"lint": "biome check .",
|
|
21
|
+
"lint:fix": "biome check . --write",
|
|
22
|
+
"format:check": "biome format .",
|
|
23
|
+
"format:write": "biome format . --write",
|
|
24
|
+
"standards:check": "code-standards verify",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "node scripts/run-tests.mjs",
|
|
27
|
+
"publish": "node scripts/release-publish.mjs"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@hono/node-server": "^1.19.11",
|
|
31
|
+
"@sha3/logger": "^2.0.0",
|
|
32
|
+
"dotenv": "^16.6.1",
|
|
33
|
+
"hono": "^4.12.7"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@sha3/code": "^{{packageVersion}}",
|
|
37
|
+
"@biomejs/biome": "^1.9.4",
|
|
38
|
+
"@types/node": "^22.13.10",
|
|
39
|
+
"tsx": "^4.19.3",
|
|
40
|
+
"typescript": "^5.8.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
function run(command, args, cwd, stdio = "inherit") {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args, { cwd, stdio, shell: process.platform === "win32" });
|
|
9
|
+
let stdout = "";
|
|
10
|
+
let stderr = "";
|
|
11
|
+
|
|
12
|
+
if (stdio === "pipe") {
|
|
13
|
+
child.stdout.on("data", (chunk) => {
|
|
14
|
+
stdout += String(chunk);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.stderr.on("data", (chunk) => {
|
|
18
|
+
stderr += String(chunk);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
child.on("error", reject);
|
|
23
|
+
child.on("exit", (code) => {
|
|
24
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isNotFoundError(output) {
|
|
30
|
+
const lower = output.toLowerCase();
|
|
31
|
+
return lower.includes("e404") || lower.includes("404 not found") || lower.includes("no match found");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseSemver(version) {
|
|
35
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
36
|
+
if (!match) {
|
|
37
|
+
throw new Error(`Unsupported version format: ${version}. Expected x.y.z`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { major: Number.parseInt(match[1], 10), minor: Number.parseInt(match[2], 10), patch: Number.parseInt(match[3], 10) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function bumpMinor(version) {
|
|
44
|
+
const parsed = parseSemver(version);
|
|
45
|
+
return `${parsed.major}.${parsed.minor + 1}.0`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readPackageJson(packageJsonPath) {
|
|
49
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function writePackageJson(packageJsonPath, pkg) {
|
|
54
|
+
await writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function versionExistsOnNpm(packageName, version, cwd) {
|
|
58
|
+
const result = await run("npm", ["view", `${packageName}@${version}`, "version", "--json"], cwd, "pipe");
|
|
59
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
60
|
+
if (result.code === 0) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (isNotFoundError(combined)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`Unable to verify npm version ${packageName}@${version}: ${combined.trim()}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
72
|
+
const projectRoot = path.resolve(scriptDir, "..");
|
|
73
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
74
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
75
|
+
if (typeof packageJson.name !== "string" || packageJson.name.length === 0) {
|
|
76
|
+
throw new Error("package.json name is required.");
|
|
77
|
+
}
|
|
78
|
+
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
|
79
|
+
throw new Error("package.json version is required.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const exists = await versionExistsOnNpm(packageJson.name, packageJson.version, projectRoot);
|
|
83
|
+
if (exists) {
|
|
84
|
+
const nextVersion = bumpMinor(packageJson.version);
|
|
85
|
+
packageJson.version = nextVersion;
|
|
86
|
+
await writePackageJson(packageJsonPath, packageJson);
|
|
87
|
+
console.log(`Version ${packageJson.name}@${packageJson.version} already existed. Bumped to ${nextVersion}.`);
|
|
88
|
+
} else {
|
|
89
|
+
console.log(`Publishing new version ${packageJson.name}@${packageJson.version}.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const build = await run("npm", ["run", "build"], projectRoot);
|
|
93
|
+
if (build.code !== 0) {
|
|
94
|
+
throw new Error("npm run build failed.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const publish = await run("npm", ["publish", "--access", "public", "--ignore-scripts"], projectRoot);
|
|
98
|
+
if (publish.code !== 0) {
|
|
99
|
+
throw new Error("npm publish failed.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const rootDir = process.cwd();
|
|
6
|
+
const testRoots = ["test", "src"];
|
|
7
|
+
|
|
8
|
+
async function collectTestsFrom(directoryPath, collector) {
|
|
9
|
+
let entries;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
entries = await readdir(directoryPath, { withFileTypes: true });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const absolutePath = path.join(directoryPath, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
await collectTestsFrom(absolutePath, collector);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!entry.isFile()) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (entry.name.endsWith(".test.ts")) {
|
|
31
|
+
collector.push(path.relative(rootDir, absolutePath));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runNodeTests(files) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const args = ["--import", "tsx", "--test", ...files];
|
|
39
|
+
const child = spawn(process.execPath, args, { cwd: rootDir, stdio: "inherit", shell: process.platform === "win32" });
|
|
40
|
+
|
|
41
|
+
child.on("error", reject);
|
|
42
|
+
child.on("exit", (code) => {
|
|
43
|
+
resolve(code ?? 1);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const testFiles = [];
|
|
50
|
+
|
|
51
|
+
for (const root of testRoots) {
|
|
52
|
+
await collectTestsFrom(path.join(rootDir, root), testFiles);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const uniqueFiles = [...new Set(testFiles)].sort((left, right) => left.localeCompare(right));
|
|
56
|
+
const exitCode = await runNodeTests(uniqueFiles);
|
|
57
|
+
if (exitCode !== 0) {
|
|
58
|
+
process.exit(exitCode);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch((error) => {
|
|
63
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ServerType } from "@hono/node-server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @section imports:internals
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import config from "../config.ts";
|
|
8
|
+
import { HttpServerService } from "../http/http-server.service.ts";
|
|
9
|
+
import logger from "../logger.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @section class
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class ServiceRuntime {
|
|
16
|
+
/**
|
|
17
|
+
* @section private:attributes
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
private readonly httpServerService: HttpServerService;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @section constructor
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
public constructor(httpServerService: HttpServerService) {
|
|
27
|
+
this.httpServerService = httpServerService;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @section factory
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
public static createDefault(): ServiceRuntime {
|
|
35
|
+
return new ServiceRuntime(HttpServerService.createDefault());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @section public:methods
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
public buildServer(): ServerType {
|
|
43
|
+
return this.httpServerService.buildServer();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async handleRequest(request: Request): Promise<Response> {
|
|
47
|
+
return this.httpServerService.handleRequest(request);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public startServer(): ServerType {
|
|
51
|
+
const server = this.buildServer();
|
|
52
|
+
server.listen(config.DEFAULT_PORT, () => {
|
|
53
|
+
logger.info(`service listening on http://localhost:${config.DEFAULT_PORT}`);
|
|
54
|
+
});
|
|
55
|
+
return server;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section imports:internals
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import config from "../config.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @section types
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type AppInfoPayload = { ok: true; serviceName: string };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @section class
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class AppInfoService {
|
|
18
|
+
/**
|
|
19
|
+
* @section private:attributes
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
private readonly serviceName: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @section constructor
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
public constructor(serviceName: string) {
|
|
29
|
+
this.serviceName = serviceName;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @section factory
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
public static createDefault(): AppInfoService {
|
|
37
|
+
return new AppInfoService(config.SERVICE_NAME);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @section public:methods
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
public buildPayload(): AppInfoPayload {
|
|
45
|
+
return { ok: true, serviceName: this.serviceName };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
|
|
3
|
+
const ENV = process.env;
|
|
4
|
+
|
|
5
|
+
const config = {
|
|
6
|
+
RESPONSE_CONTENT_TYPE: ENV.RESPONSE_CONTENT_TYPE || "application/json",
|
|
7
|
+
DEFAULT_PORT: Number(ENV.PORT || 3000),
|
|
8
|
+
SERVICE_NAME: ENV.SERVICE_NAME || "{{packageName}}",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export default config;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @section imports:externals
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
6
|
+
import type { ServerType } from "@hono/node-server";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @section imports:internals
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AppInfoService } from "../app-info/app-info.service.ts";
|
|
14
|
+
import config from "../config.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @section class
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class HttpServerService {
|
|
21
|
+
/**
|
|
22
|
+
* @section private:attributes
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
private readonly appInfoService: AppInfoService;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @section constructor
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
public constructor(appInfoService: AppInfoService) {
|
|
32
|
+
this.appInfoService = appInfoService;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @section factory
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
public static createDefault(): HttpServerService {
|
|
40
|
+
return new HttpServerService(AppInfoService.createDefault());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @section public:methods
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
public async handleRequest(request: Request): Promise<Response> {
|
|
48
|
+
const app = new Hono();
|
|
49
|
+
app.get("/", (context) => {
|
|
50
|
+
const payload = this.appInfoService.buildPayload();
|
|
51
|
+
context.header("content-type", config.RESPONSE_CONTENT_TYPE);
|
|
52
|
+
return context.json(payload, 200);
|
|
53
|
+
});
|
|
54
|
+
return app.fetch(request);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public buildServer(): ServerType {
|
|
58
|
+
const app = new Hono();
|
|
59
|
+
app.get("/", (context) => {
|
|
60
|
+
const payload = this.appInfoService.buildPayload();
|
|
61
|
+
context.header("content-type", config.RESPONSE_CONTENT_TYPE);
|
|
62
|
+
return context.json(payload, 200);
|
|
63
|
+
});
|
|
64
|
+
return createAdaptorServer({ fetch: app.fetch });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Logger from "@sha3/logger";
|
|
2
|
+
|
|
3
|
+
const PACKAGE_NAME = "{{packageName}}";
|
|
4
|
+
const LOGGER_NAME = PACKAGE_NAME.startsWith("@") ? PACKAGE_NAME.split("/")[1] || PACKAGE_NAME : PACKAGE_NAME;
|
|
5
|
+
const logger = new Logger({ loggerName: LOGGER_NAME });
|
|
6
|
+
|
|
7
|
+
export default logger;
|