@horka/app-forge 0.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/LICENSE +32 -0
- package/README.md +99 -0
- package/bin/cli.js +371 -0
- package/bin/cli.test.js +91 -0
- package/package.json +43 -0
- package/templates/core/CLAUDE.md +36 -0
- package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
- package/templates/core/claude/memory/COMMANDS.md +13 -0
- package/templates/core/claude/memory/DECISIONS.md +5 -0
- package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
- package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
- package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
- package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
- package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
- package/templates/core/claude/skills/save-context/SKILL.md +35 -0
- package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
- package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
- package/templates/core/docs-architecture/DELIVERY.md +68 -0
- package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
- package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
- package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
- package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
- package/templates/core/gitignore +15 -0
- package/templates/core/mcp.json +8 -0
- package/templates/packs/nuxt-web/CLAUDE.md +74 -0
- package/templates/packs/nuxt-web/app/app.vue +5 -0
- package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
- package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
- package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
- package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
- package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
- package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
- package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
- package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
- package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
- package/templates/packs/nuxt-web/gitignore +18 -0
- package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
- package/templates/packs/nuxt-web/pack.json +11 -0
- package/templates/packs/nuxt-web/package.json +31 -0
- package/templates/packs/nuxt-web/playwright.config.ts +39 -0
- package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
- package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
- package/templates/packs/nuxt-web/tsconfig.json +4 -0
- package/templates/packs/nuxt-web/vitest.config.ts +23 -0
- package/templates/packs/swift-ios/CLAUDE.md +64 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
- package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
- package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
- package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
- package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
- package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
- package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
- package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
- package/templates/packs/swift-ios/gitignore +5 -0
- package/templates/packs/swift-ios/mcp.json +8 -0
- package/templates/packs/swift-ios/pack.json +11 -0
- package/templates/packs/swift-ios/project.yml +33 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
- package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
- package/templates/packs/ts-sdk/CLAUDE.md +72 -0
- package/templates/packs/ts-sdk/MIGRATION.md +28 -0
- package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
- package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
- package/templates/packs/ts-sdk/gitignore +6 -0
- package/templates/packs/ts-sdk/pack.json +11 -0
- package/templates/packs/ts-sdk/package.json +55 -0
- package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
- package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
- package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
- package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
- package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
- package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
- package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
- package/templates/packs/ts-sdk/src/index.ts +62 -0
- package/templates/packs/ts-sdk/src/types/index.ts +33 -0
- package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
- package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
- package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
- package/templates/packs/ts-sdk/tsconfig.json +15 -0
- package/templates/packs/ts-sdk/tsup.config.ts +22 -0
- package/templates/packs/ts-sdk/vitest.config.ts +8 -0
- package/templates/packs/vapor-api/CLAUDE.md +73 -0
- package/templates/packs/vapor-api/Dockerfile +80 -0
- package/templates/packs/vapor-api/Package.swift +68 -0
- package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
- package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
- package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
- package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
- package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
- package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
- package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
- package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
- package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
- package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
- package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
- package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
- package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
- package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
- package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
- package/templates/packs/vapor-api/env_dist +29 -0
- package/templates/packs/vapor-api/gitignore +7 -0
- package/templates/packs/vapor-api/pack.json +11 -0
- package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
- package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "swift-ios",
|
|
3
|
+
"label": "iOS app — Swift 6.2 / SwiftUI / CloudKit",
|
|
4
|
+
"languages": ["swift"],
|
|
5
|
+
"idPrompt": "Bundle identifier",
|
|
6
|
+
"requirements": [
|
|
7
|
+
"Xcode 26+",
|
|
8
|
+
"XcodeGen (brew install xcodegen) — run `xcodegen generate` to create the .xcodeproj"
|
|
9
|
+
],
|
|
10
|
+
"notes": "Packages build day one: swift test --package-path Packages/{{PROJECT_NAME}}Core"
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# XcodeGen manifest — run `xcodegen generate` to (re)create {{PROJECT_NAME}}.xcodeproj.
|
|
2
|
+
# The .xcodeproj is gitignored: this file is the source of truth (merge-conflict-free).
|
|
3
|
+
name: {{PROJECT_NAME}}
|
|
4
|
+
options:
|
|
5
|
+
bundleIdPrefix: {{BUNDLE_ID}}
|
|
6
|
+
deploymentTarget:
|
|
7
|
+
iOS: "26.0"
|
|
8
|
+
packages:
|
|
9
|
+
{{PROJECT_NAME}}DS:
|
|
10
|
+
path: Packages/{{PROJECT_NAME}}DS
|
|
11
|
+
{{PROJECT_NAME}}Core:
|
|
12
|
+
path: Packages/{{PROJECT_NAME}}Core
|
|
13
|
+
DataLayer:
|
|
14
|
+
path: Packages/DataLayer
|
|
15
|
+
targets:
|
|
16
|
+
{{PROJECT_NAME}}:
|
|
17
|
+
type: application
|
|
18
|
+
platform: iOS
|
|
19
|
+
sources: [{{PROJECT_NAME}}]
|
|
20
|
+
dependencies:
|
|
21
|
+
- package: {{PROJECT_NAME}}DS
|
|
22
|
+
- package: {{PROJECT_NAME}}Core
|
|
23
|
+
- package: DataLayer
|
|
24
|
+
settings:
|
|
25
|
+
base:
|
|
26
|
+
PRODUCT_BUNDLE_IDENTIFIER: {{BUNDLE_ID}}
|
|
27
|
+
SWIFT_VERSION: "6.2"
|
|
28
|
+
SWIFT_STRICT_CONCURRENCY: complete
|
|
29
|
+
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
|
|
30
|
+
GENERATE_INFOPLIST_FILE: true
|
|
31
|
+
INFOPLIST_KEY_UILaunchScreen_Generation: true
|
|
32
|
+
CURRENT_PROJECT_VERSION: 1
|
|
33
|
+
MARKETING_VERSION: 0.1.0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import {{PROJECT_NAME}}DS
|
|
3
|
+
|
|
4
|
+
@main
|
|
5
|
+
struct {{PROJECT_NAME}}App: SwiftUI.App { // qualified: `App` (unqualified) is our namespace enum
|
|
6
|
+
var body: some Scene {
|
|
7
|
+
WindowGroup {
|
|
8
|
+
RootView()
|
|
9
|
+
// Dark-first: lock the scheme so system chrome (sheets, alerts, keyboard) matches
|
|
10
|
+
// the dark token palette — see docs-architecture/DESIGN_SYSTEM.md.
|
|
11
|
+
.preferredColorScheme(.dark)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Root gate — grows at kickoff into: !isReady → Splash, needsOnboarding → Welcome, else → tabs.
|
|
17
|
+
/// (See docs-architecture/NAVIGATION.md.)
|
|
18
|
+
struct RootView: View {
|
|
19
|
+
var body: some View {
|
|
20
|
+
ZStack {
|
|
21
|
+
Color.DS.background.ignoresSafeArea()
|
|
22
|
+
VStack(spacing: DS.Padding.m) {
|
|
23
|
+
Text("{{PROJECT_NAME}}")
|
|
24
|
+
.designSystem(font: .largeTitle)
|
|
25
|
+
.foregroundStyle(DS.Gradients.brand)
|
|
26
|
+
Text("Hello 👋")
|
|
27
|
+
.designSystem(font: .callout)
|
|
28
|
+
.foregroundStyle(Color.DS.textSecondary)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/// Namespace for app screens: `extension App { struct Feed: View }` → call sites read `App.Feed`.
|
|
2
|
+
/// IMPORTANT: this enum collides with the `SwiftUI.App` protocol by design — the @main entry point
|
|
3
|
+
/// must declare `struct {{PROJECT_NAME}}App: SwiftUI.App` (fully qualified). See CONVENTIONS.md §4.
|
|
4
|
+
enum App {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog — {{BUNDLE_ID}}
|
|
2
|
+
|
|
3
|
+
All notable changes, newest first. Versions are git tags (`vX.Y.Z`); clients pin them.
|
|
4
|
+
|
|
5
|
+
## [0.1.0]
|
|
6
|
+
|
|
7
|
+
- Initial scaffold: fetch transport with normalized `ApiError`, injected storage
|
|
8
|
+
(`SDKContext`) and logger (`SDKLogger`) ports, `AuthClient` with single-flight token
|
|
9
|
+
refresh, types-only `./types` subpath, dist verification gate.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — Claude Code Operating Manual
|
|
2
|
+
|
|
3
|
+
This project was scaffolded by **AppForge** (pack: TypeScript SDK): a Claude-Code-first
|
|
4
|
+
architecture extracted from production SDKs. You (Claude) are the team lead AND the
|
|
5
|
+
primary developer. Follow this manual exactly — it encodes hard-won lessons, not preferences.
|
|
6
|
+
|
|
7
|
+
## Identity
|
|
8
|
+
- Package: **`{{BUNDLE_ID}}`** ({{PROJECT_NAME}}) · TypeScript, strict · Node 20+ / evergreen browsers
|
|
9
|
+
- This is a **typed SDK**: the single client library between an API and all of its consumers
|
|
10
|
+
(web, SSR, CLI, CI…).
|
|
11
|
+
|
|
12
|
+
## Multi-repo position — this repo is the CONTRACT layer
|
|
13
|
+
The product spans repos: `backend → SDK (this repo) → clients`. Everything here follows
|
|
14
|
+
`docs-architecture/MULTI_REPO_CONTRACT.md`:
|
|
15
|
+
- **Backend changes land here second** — never start SDK work against a backend whose tests
|
|
16
|
+
are red; never let clients start before this SDK builds, type-checks, tests green and is
|
|
17
|
+
**tagged**.
|
|
18
|
+
- **Clients consume tagged releases only** (`#vX.Y.Z` or a registry pin) — never a branch.
|
|
19
|
+
- **Breaking change ⇒ MIGRATION.md entry + version bump + tag**, in the same change.
|
|
20
|
+
- The public surface is the `exports` map. Anything not exported does not exist.
|
|
21
|
+
|
|
22
|
+
## Session protocol (MANDATORY)
|
|
23
|
+
1. **Session start**: run the `restore-context` skill — read `.claude/memory/*.md` before doing anything. Never invent project facts.
|
|
24
|
+
2. **Empty project / new idea**: run the `kickoff` skill — it interviews the user, writes the PRD, plans slices, then builds autonomously.
|
|
25
|
+
3. **After significant work**: update `.claude/memory/PROJECT_STATE.md` (and DECISIONS/NEXT_STEPS when relevant) — `save-context` skill.
|
|
26
|
+
|
|
27
|
+
## Architecture (read the docs before coding)
|
|
28
|
+
The knowledge base lives in `docs-architecture/`. Read the relevant doc BEFORE touching that area:
|
|
29
|
+
|
|
30
|
+
| You are about to… | Read first |
|
|
31
|
+
|---|---|
|
|
32
|
+
| understand the layer model (stack-agnostic) | `ARCHITECTURE_PRINCIPLES.md` |
|
|
33
|
+
| plan/deliver slices, validate, update memory | `DELIVERY.md` |
|
|
34
|
+
| coordinate with the backend or a client repo | `MULTI_REPO_CONTRACT.md` |
|
|
35
|
+
| anything SDK-shaped (this whole repo) | `SDK_CONTRACT.md` — this pack instantiates it |
|
|
36
|
+
| add/move any file, add a resource client | `ARCHITECTURE.md` |
|
|
37
|
+
| write any TypeScript code | `CONVENTIONS_TS.md` |
|
|
38
|
+
| accept user-supplied URLs (webhooks…) | `SECURITY_USER_URLS.md` |
|
|
39
|
+
| write any documentation | `DOCS_PLACEMENT.md` |
|
|
40
|
+
| add caching/feature flags/auth shortcuts | `ANTI_PATTERNS.md` |
|
|
41
|
+
|
|
42
|
+
Layer summary (universal contract in ARCHITECTURE_PRINCIPLES.md, TS mapping in ARCHITECTURE.md):
|
|
43
|
+
L0 `src/types/` + `src/errors/` (wire types, ApiError — import nothing above) · L1 logger port ·
|
|
44
|
+
L2 `src/core/` (fetch transport, storage adapters) · L3 `src/clients/` (resource clients +
|
|
45
|
+
single-flight auth use case) · L5 `src/index.ts` (composition root, sole public surface).
|
|
46
|
+
From the consumer's seat, this whole package is **their L2 Data brick**.
|
|
47
|
+
|
|
48
|
+
## Non-negotiable rules
|
|
49
|
+
- **Never claim done without proof**: `npm run typecheck` + `npx vitest run` + `npm run build`
|
|
50
|
+
(which runs the dist/types verification) all green, outputs shown.
|
|
51
|
+
- **The single-flight tests are the spec.** `tests/singleFlight.test.ts` must never be
|
|
52
|
+
weakened or skipped — the concurrency bug it pins reappears every time someone
|
|
53
|
+
"simplifies" the auth use case.
|
|
54
|
+
- **Zero `console.*` in `src/`** — logging goes through the injected `SDKLogger` port,
|
|
55
|
+
debug-gated, and never logs token values.
|
|
56
|
+
- **`.ts` sources only** — never author `.d.ts` files in `src/` (declaration generators
|
|
57
|
+
silently skip them; see SDK_CONTRACT.md §4). Never commit `dist/`.
|
|
58
|
+
- **`dependencies` stays `{}`** unless a DECISIONS.md entry justifies a runtime dep.
|
|
59
|
+
Build tooling lives in `devDependencies`, always.
|
|
60
|
+
- **Tagged releases only**; every breaking change ships its MIGRATION.md entry.
|
|
61
|
+
|
|
62
|
+
## Build commands
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm run typecheck # tsc --noEmit — fastest signal
|
|
66
|
+
npx vitest run # full suite, includes the single-flight regression test
|
|
67
|
+
npm run build # tsup (esm+cjs+d.ts) THEN scripts/verify-dist.mjs gate
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Git
|
|
71
|
+
- Never push without explicit user approval. Feature branches; commit format `add/update/fix(scope) - description`.
|
|
72
|
+
- No AI attribution in commits or file headers.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Migration Guide — {{BUNDLE_ID}}
|
|
2
|
+
|
|
3
|
+
One dated section per **breaking change**, newest first, written in the same change that
|
|
4
|
+
breaks (CONVENTIONS_TS.md §8, MULTI_REPO_CONTRACT.md breaking-change protocol). Every code
|
|
5
|
+
sample must compile against the real SDK surface — invented samples are worse than no docs.
|
|
6
|
+
|
|
7
|
+
Template:
|
|
8
|
+
|
|
9
|
+
```markdown
|
|
10
|
+
## vX.Y.0 — <one-line summary> (YYYY-MM-DD)
|
|
11
|
+
|
|
12
|
+
### What changed
|
|
13
|
+
Before / after code blocks, copied from real consumer usage.
|
|
14
|
+
|
|
15
|
+
### Why
|
|
16
|
+
The constraint that forced the break.
|
|
17
|
+
|
|
18
|
+
### Impact
|
|
19
|
+
What consumers must do, per platform if it differs.
|
|
20
|
+
|
|
21
|
+
### Security implications
|
|
22
|
+
Mandatory when auth, tokens or cookies are involved — state the new tradeoff and the
|
|
23
|
+
required mitigations.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
*No entries yet — v0.1.0 is the initial scaffold.*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} — Commands
|
|
2
|
+
|
|
3
|
+
> Only commands proven to work in THIS project, with exact flags.
|
|
4
|
+
|
|
5
|
+
## Fast loop (always in this order)
|
|
6
|
+
npm run typecheck # tsc --noEmit — fastest signal
|
|
7
|
+
npx vitest run # full suite (CI mode)
|
|
8
|
+
npx vitest run tests/singleFlight.test.ts # the auth concurrency regression suite alone
|
|
9
|
+
|
|
10
|
+
## Build + dist verification
|
|
11
|
+
npm run build # tsup (esm+cjs+d.ts, "." and "./types" entries) THEN scripts/verify-dist.mjs
|
|
12
|
+
ls dist dist/types # eyeball the artifacts when in doubt
|
|
13
|
+
|
|
14
|
+
## Release gate (before tagging — see CONVENTIONS_TS.md §8)
|
|
15
|
+
npm pkg get dependencies # must be the expected object (ideally {})
|
|
16
|
+
npm pack --dry-run # tarball must contain dist + manifest only
|
|
17
|
+
git tag v0.X.Y # clients pin THIS, never a branch
|
|
18
|
+
|
|
19
|
+
## Consumer-side linking (lives in the CLIENT repo — SDK_CONTRACT.md §6)
|
|
20
|
+
# sdk:local / sdk:prod scripts switch between file:../ and the pinned tag;
|
|
21
|
+
# a file: link must never reach a shared branch.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# ARCHITECTURE — Typed TypeScript SDK (the contract package)
|
|
2
|
+
|
|
3
|
+
This package is the **contract layer** of a multi-repo product: `backend → SDK → clients`
|
|
4
|
+
(MULTI_REPO_CONTRACT.md). From any consumer's seat, the whole package is **their L2 Data
|
|
5
|
+
brick** — the network client behind their repository implementations. Internally it runs
|
|
6
|
+
the universal L0–L5 lattice (ARCHITECTURE_PRINCIPLES.md) in miniature, exactly as
|
|
7
|
+
SDK_CONTRACT.md prescribes. Read both before touching this repo; this file maps them onto
|
|
8
|
+
TypeScript files.
|
|
9
|
+
|
|
10
|
+
## 1. Layer model — TS instantiation
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
L5 src/index.ts composition root: constructs adapters, wires transport → auth →
|
|
14
|
+
resource clients, re-exports the ENTIRE public surface (and only it)
|
|
15
|
+
L3 src/clients/ one typed client per API resource + the secure-request use case
|
|
16
|
+
(single-flight token refresh lives in AuthClient)
|
|
17
|
+
L2 src/core/ adapters & ports: HttpClient (fetch transport), SDKContext
|
|
18
|
+
(storage port + adapters), withTimeout primitive
|
|
19
|
+
L1 src/core/Logger.ts ops port: SDKLogger, no-op by default, debug-gated at the root
|
|
20
|
+
L0 src/types/ wire types mirroring backend DTOs — import NOTHING in the package
|
|
21
|
+
src/errors/ ApiError + fromResponse normalization — imports types only
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Tests live in `tests/`, stub the ports, and exercise the **public surface**
|
|
25
|
+
(`import … from '../src/index'`) so they double as compile-time proof of the exports.
|
|
26
|
+
|
|
27
|
+
## 2. Dependency direction
|
|
28
|
+
|
|
29
|
+
| Directory | may import | must NEVER import |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `types/` | nothing | anything — a `types/` file with an import is a bug |
|
|
32
|
+
| `errors/` | `types/` | `core/`, `clients/`, `index.ts` |
|
|
33
|
+
| `core/` | `types/`, `errors/` | `clients/`, `index.ts` |
|
|
34
|
+
| `clients/` | core **ports** (`HttpTransport`, `SDKContext`, `SDKLogger`), `types/`, `errors/` | `fetch`/`document` directly, sibling clients, `index.ts` |
|
|
35
|
+
| `index.ts` | everything | — (sole assembly point) |
|
|
36
|
+
|
|
37
|
+
The import graph is the architecture — every violation is grep-visible:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
grep -rn "fetch(" src/clients/ src/errors/ src/types/ # must be empty (transport port only)
|
|
41
|
+
grep -rn "console\." src/ # must be empty (injected logger only)
|
|
42
|
+
grep -rn "from '.*clients" src/types/ src/errors/ src/core/ # must be empty (no upward imports)
|
|
43
|
+
grep -rln "document\.\|window\." src/ --include='*.ts' # browser APIs only inside a storage adapter
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> ⚠️ **Gotcha:** Symptom — a "tiny helper" in `types/` starts importing `ApiError`, then a
|
|
47
|
+
> client, and suddenly the types-only subpath drags the HTTP runtime into the consumer's
|
|
48
|
+
> pure L3 layer. Cause — runtime code creeping into `types/`. Fix — `types/` holds
|
|
49
|
+
> declarations only (interfaces, type aliases, string-literal unions); anything with a
|
|
50
|
+
> runtime body lives in `core/` or above.
|
|
51
|
+
|
|
52
|
+
## 3. The SDK from the consumer's seat
|
|
53
|
+
|
|
54
|
+
- Consumers import **`{{BUNDLE_ID}}`** (runtime: clients, errors, adapters) or
|
|
55
|
+
**`{{BUNDLE_ID}}/types`** (wire types only, erased at compile time). Nothing else exists —
|
|
56
|
+
the `exports` map is the law (MULTI_REPO_CONTRACT.md "Never bypass the SDK").
|
|
57
|
+
- Consumer L3 declares repository contracts and may reference `{{BUNDLE_ID}}/types`;
|
|
58
|
+
consumer L2 implements those contracts by calling this SDK and mapping wire → domain.
|
|
59
|
+
No consumer L3+ brick instantiates the SDK; their composition root (L5) wires it.
|
|
60
|
+
- Clients pin a **tag**, never a branch (MULTI_REPO_CONTRACT.md). Cutting a release here is
|
|
61
|
+
what unblocks client work — see the release checklist in CONVENTIONS_TS.md.
|
|
62
|
+
|
|
63
|
+
## 4. Adding a resource client — the recipe
|
|
64
|
+
|
|
65
|
+
Backend first (its tests green, DTOs frozen), then here, then clients — always in that order.
|
|
66
|
+
|
|
67
|
+
1. **`src/types/`** — add wire types mirroring the new DTOs (plain `.ts`, no imports).
|
|
68
|
+
2. **`src/clients/FooClient.ts`** — thin, typed methods; secure endpoints take the
|
|
69
|
+
`AuthClient`, public endpoints take the `HttpTransport` port directly:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export class ProjectClient {
|
|
73
|
+
constructor(private readonly auth: AuthClient) {}
|
|
74
|
+
list(): Promise<Project[]> { return this.auth.request('GET', '/projects'); }
|
|
75
|
+
create(draft: ProjectDraft): Promise<Project> { return this.auth.request('POST', '/projects', draft); }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
No URL building beyond the path, no response re-parsing, no error catching — the
|
|
80
|
+
transport normalizes errors (`ApiError`), the consumer maps them to UX (SDK_CONTRACT.md §2).
|
|
81
|
+
3. **`src/index.ts`** — construct it in the composition root, expose it as a readonly
|
|
82
|
+
field, re-export the class and its wire types.
|
|
83
|
+
4. **`scripts/verify-dist.mjs`** — add the new public names to the surface list so the
|
|
84
|
+
build gate proves the types actually shipped.
|
|
85
|
+
5. **`tests/`** — stub `HttpTransport`, assert method/path/payload and error mapping.
|
|
86
|
+
6. Build green → version bump → tag (CONVENTIONS_TS.md §8).
|
|
87
|
+
|
|
88
|
+
## 5. Auth — why `clients/AuthClient.ts` looks the way it does
|
|
89
|
+
|
|
90
|
+
The single-flight refresh contract (shared inflight promise, bounded wait, retry exactly
|
|
91
|
+
once, clear-tokens-on-failure) is specified in **SDK_CONTRACT.md §3** — read it before
|
|
92
|
+
touching `AuthClient`. Local invariants on top:
|
|
93
|
+
|
|
94
|
+
- `tests/singleFlight.test.ts` is the executable spec: N concurrent 401s ⇒ **exactly one**
|
|
95
|
+
refresh call against a fake API that enforces *single-use* rotating refresh tokens. It
|
|
96
|
+
also pins the timeout and failure paths. Never weaken or skip these tests.
|
|
97
|
+
- The refresh call itself goes through the raw transport, never through `request()` —
|
|
98
|
+
a refresh that 401s must not trigger another refresh.
|
|
99
|
+
- Token storage goes through the `SDKContext` port. The skeleton ships the **memory
|
|
100
|
+
adapter** only; adding a browser/SSR adapter is a recorded decision (token-storage
|
|
101
|
+
tradeoff table in SDK_CONTRACT.md §3 → `DECISIONS.md`), never a copied default.
|
|
102
|
+
|
|
103
|
+
## 6. Build pipeline — dual output, verified types
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
npm run build = tsup (entries: src/index.ts + src/types/index.ts → esm + cjs + .d.ts)
|
|
107
|
+
THEN node scripts/verify-dist.mjs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`verify-dist.mjs` fails the build unless every expected `dist/` artifact exists **and**
|
|
111
|
+
every name on the public-surface list appears in the emitted declarations. "It built" is
|
|
112
|
+
not proof the types shipped (SDK_CONTRACT.md §4) — this gate makes the proof mechanical.
|
|
113
|
+
|
|
114
|
+
> ⚠️ **Gotcha:** Symptom — consumers report "module has no exported member" for types that
|
|
115
|
+
> visibly exist in `src/`. Cause — the types were authored as `.d.ts` files; declaration
|
|
116
|
+
> generators treat those as already-compiled and emit nothing for them. Fix — author all
|
|
117
|
+
> types as plain `.ts`; the verify-dist gate catches any regression at build time.
|
|
118
|
+
|
|
119
|
+
> ⚠️ **Gotcha:** Symptom — the SDK behaves differently in the consumer than in this repo's
|
|
120
|
+
> tests, and `git diff` is clean. Cause — a stale committed `dist/` shadowing fresh sources.
|
|
121
|
+
> Fix — `dist/` is gitignored here and must stay that way; artifacts are built at tag time,
|
|
122
|
+
> never reviewed, never committed.
|
|
123
|
+
|
|
124
|
+
## 7. Why this works with AI agents
|
|
125
|
+
|
|
126
|
+
- **Seconds-fast ground truth**: `npm run typecheck` and `npx vitest run` give precise
|
|
127
|
+
feedback without any consumer app — iterate here before touching client repos.
|
|
128
|
+
- **Ports make tests cheap**: every client is testable by stubbing `HttpTransport`; no
|
|
129
|
+
network, no mock servers, no global-fetch patching.
|
|
130
|
+
- **Grep-visible boundaries** (§2): an agent can verify the architecture with four greps.
|
|
131
|
+
- **Mechanical release gates**: verify-dist + the single-flight suite encode the two
|
|
132
|
+
historical production failures as automated checks, so no session re-learns them.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# CONVENTIONS — TypeScript for a typed SDK
|
|
2
|
+
|
|
3
|
+
House rules for every line of TypeScript in this repo. They instantiate SDK_CONTRACT.md
|
|
4
|
+
for this package — when in doubt, that file wins. Each rule below was paid for in
|
|
5
|
+
production; none is style preference.
|
|
6
|
+
|
|
7
|
+
## 1. Compiler discipline
|
|
8
|
+
|
|
9
|
+
- `strict: true`, no `any` (use `unknown` + narrowing). A cast (`as`) needs a comment
|
|
10
|
+
explaining why narrowing is impossible.
|
|
11
|
+
- `verbatimModuleSyntax: true` — type-only imports are written `import type { … }`. This
|
|
12
|
+
keeps the types-only subpath genuinely runtime-free.
|
|
13
|
+
- `npm run typecheck` (`tsc --noEmit`) is the fastest signal — run it before the test
|
|
14
|
+
suite, run both before any build.
|
|
15
|
+
|
|
16
|
+
### `.ts` sources only — never author `.d.ts`
|
|
17
|
+
|
|
18
|
+
All of `src/` is plain `.ts`, **including every file in `src/types/`**. Declarations are
|
|
19
|
+
*generated* into `dist/` by the build, never written by hand.
|
|
20
|
+
|
|
21
|
+
> ⚠️ **Gotcha:** Symptom — consumers get "module has no exported member" for roughly half
|
|
22
|
+
> the public types while the source visibly contains them. Cause — types authored as
|
|
23
|
+
> `.d.ts` files: declaration generators treat them as already-compiled output and silently
|
|
24
|
+
> emit nothing, and a committed `dist/` masked the hole for months (a production team
|
|
25
|
+
> shipped 13 of 21 type modules missing this way). Fix — `.ts` sources only, `dist/`
|
|
26
|
+
> gitignored, and the `scripts/verify-dist.mjs` gate (run by `npm run build`) failing the
|
|
27
|
+
> build when a public name is absent from the emitted declarations.
|
|
28
|
+
|
|
29
|
+
## 2. Exports map — the public surface, and the types-only subpath
|
|
30
|
+
|
|
31
|
+
`package.json#exports` defines what exists. This SDK exposes exactly two entries:
|
|
32
|
+
|
|
33
|
+
- `"."` — the runtime surface (composition root, clients, errors, adapters).
|
|
34
|
+
- `"./types"` — **types only**, so a consumer's pure L3 layer can reference wire types
|
|
35
|
+
without dragging the HTTP client into it (type imports erase at compile time).
|
|
36
|
+
|
|
37
|
+
Rules:
|
|
38
|
+
- Adding/removing/renaming a subpath is an architectural decision → `DECISIONS.md` entry
|
|
39
|
+
+ semver impact assessed. Deep paths (`/src/*`, `/dist/internal/*`) are never exposed.
|
|
40
|
+
- Everything public is re-exported from `src/index.ts` (or `src/types/index.ts`); a type
|
|
41
|
+
reachable only through a deep import is a bug.
|
|
42
|
+
- `"sideEffects": false` stays true: no module-level statements with effects — module load
|
|
43
|
+
must be free (no env reads, no global patching, no top-level `await`).
|
|
44
|
+
|
|
45
|
+
## 3. Dependencies policy — runtime deps target zero
|
|
46
|
+
|
|
47
|
+
- `"dependencies": {}` — the transport is built-in `fetch`; an SDK drags its runtime deps
|
|
48
|
+
into every consumer's tree. Adding one requires a `DECISIONS.md` entry justifying it.
|
|
49
|
+
- Build/test tooling (`tsup`, `typescript`, `vitest`) lives in `devDependencies`, always.
|
|
50
|
+
- Release audit (it's in the checklist, §8): `npm pkg get dependencies` must print the
|
|
51
|
+
expected object — ideally `{}`.
|
|
52
|
+
|
|
53
|
+
> ⚠️ **Gotcha:** Symptom — every consumer install pulls a bundler plugin (and its whole
|
|
54
|
+
> dependency tree) they never use. Cause — a declaration-generator plugin added under
|
|
55
|
+
> `dependencies` instead of `devDependencies`; nothing fails, so it ships for months.
|
|
56
|
+
> Fix — `dependencies` may contain only what `src/` actually imports at runtime; audit at
|
|
57
|
+
> every release (see SDK_CONTRACT.md §1).
|
|
58
|
+
|
|
59
|
+
## 4. Logging — injected port, silent by default
|
|
60
|
+
|
|
61
|
+
- **Zero `console.*` in `src/`** — grep-enforced (`grep -rn "console\." src/` must be
|
|
62
|
+
empty); add `no-console: error` if you introduce a linter. Build scripts (`scripts/`)
|
|
63
|
+
may print — they run on developer machines, not inside consumers.
|
|
64
|
+
- All logging goes through the `SDKLogger` port; the default is a no-op, and `debug()`
|
|
65
|
+
only fires when the consumer passes `debug: true`. The *consumer* owns the switch.
|
|
66
|
+
- **Never log token values, `Authorization` headers, or token-presence booleans.** A
|
|
67
|
+
production team once logged method + URL + which auth tokens were present on every
|
|
68
|
+
request: unfilterable console noise for every consumer and a free map of the auth
|
|
69
|
+
topology in devtools (SDK_CONTRACT.md §5). Log method + path, nothing from headers.
|
|
70
|
+
|
|
71
|
+
## 5. Errors — normalize once, never re-parse
|
|
72
|
+
|
|
73
|
+
- Every non-2xx response becomes an `ApiError` via `ApiError.fromResponse(status, text)` —
|
|
74
|
+
thrown by the transport, the single place that reads response bodies on failure.
|
|
75
|
+
- The wire error shape `{ code, name, description }` is frozen contract: renaming a field
|
|
76
|
+
is a **breaking change in two repos** (SDK_CONTRACT.md §2) — major bump + MIGRATION entry.
|
|
77
|
+
- Clients (`src/clients/`) never catch-and-rewrap, never inspect bodies; consumers map
|
|
78
|
+
`statusCode` → UX in one shared handler, never re-parse raw responses.
|
|
79
|
+
- Genuine network failures (DNS, refused connection) propagate as-is — inventing a fake
|
|
80
|
+
status code for them would lie to the consumer's error handler.
|
|
81
|
+
|
|
82
|
+
## 6. Auth invariants — single-flight or nothing
|
|
83
|
+
|
|
84
|
+
The contract is SDK_CONTRACT.md §3; the local law:
|
|
85
|
+
|
|
86
|
+
- One shared inflight refresh promise; latecomers await it; the wait is **bounded** by a
|
|
87
|
+
timeout that rejects (never hang waiters forever).
|
|
88
|
+
- On 401: join/start the refresh, then retry the original request **exactly once**. Two
|
|
89
|
+
retries hide real auth breakage; zero retries logs users out on every token expiry.
|
|
90
|
+
- On any refresh failure: **fail every waiter** — a half-authenticated session is worse than
|
|
91
|
+
a clean error. But **clear tokens ONLY on a real auth rejection** (a 401, or a 400 carrying
|
|
92
|
+
`invalid_grant`): that is the server's verdict that the refresh token is dead. A transport
|
|
93
|
+
failure — offline, DNS, connection refused, timeout (`RefreshTimeout`/408), or 5xx — never
|
|
94
|
+
reached a verdict, so the tokens are **kept**; clearing them would force a gratuitous logout
|
|
95
|
+
on a session that may still be valid (this matches §5: network failures propagate as-is).
|
|
96
|
+
- The refresh call uses the raw transport, never the secure `request()` path (a 401 from
|
|
97
|
+
the refresh endpoint must not recurse).
|
|
98
|
+
- `tests/singleFlight.test.ts` pins all of this against a fake API with *single-use*
|
|
99
|
+
rotating refresh tokens. It is the regression test for a real production bug
|
|
100
|
+
(N parallel 401s → N refreshes → N−1 failures → random logouts) and is non-negotiable.
|
|
101
|
+
|
|
102
|
+
> ⚠️ **Gotcha:** Symptom — intermittent logouts return months after the fix. Cause —
|
|
103
|
+
> someone "simplified" the auth use case and dropped the shared inflight promise; the bug
|
|
104
|
+
> is invisible in manual testing because it needs concurrent expiry. Fix — the concurrency
|
|
105
|
+
> test stays, and any `AuthClient` refactor runs it first.
|
|
106
|
+
|
|
107
|
+
## 7. Testing conventions
|
|
108
|
+
|
|
109
|
+
- **vitest**, plain `node` environment. `npx vitest run` (CI mode) is the gate; watch mode
|
|
110
|
+
is for development only.
|
|
111
|
+
- **Stub the `HttpTransport` port** in client/use-case tests — never patch global `fetch`
|
|
112
|
+
there (only a transport adapter test may). Fakes enforce realistic API behavior (e.g.
|
|
113
|
+
single-use refresh tokens), not just canned responses.
|
|
114
|
+
- Concurrency tests use `Promise.all` / `Promise.allSettled` over real async boundaries —
|
|
115
|
+
no fake timers for the single-flight suite (timing is the point; keep timeouts short).
|
|
116
|
+
- Every test asserts observable behavior (calls, payloads, thrown `ApiError` fields).
|
|
117
|
+
A test without a meaningful assertion is deleted, not kept for coverage.
|
|
118
|
+
- Tests import from `../src/index` wherever possible — they double as proof that the
|
|
119
|
+
public surface actually exports what consumers need.
|
|
120
|
+
|
|
121
|
+
## 8. Releases & versioning
|
|
122
|
+
|
|
123
|
+
- **Semver, tagged.** While `0.x`: breaking ⇒ **minor** bump; from `1.0.0`: breaking ⇒
|
|
124
|
+
**major**. (This convention is stated here so clients can rely on it — changing it is a
|
|
125
|
+
DECISIONS.md entry.)
|
|
126
|
+
- **Never commit `dist/`** — built at tag time. Consumers pin tags or registry versions,
|
|
127
|
+
never a branch (MULTI_REPO_CONTRACT.md — branch pins broke a production deploy with an
|
|
128
|
+
empty diff).
|
|
129
|
+
- Every breaking change ships a dated **MIGRATION.md** section in the same change: what
|
|
130
|
+
changed (before/after code), why, impact, and security implications when auth/cookies
|
|
131
|
+
are involved. Every sample in it must compile against the real surface
|
|
132
|
+
(SDK_CONTRACT.md §7 — invented samples convert knowledge gaps into false confidence).
|
|
133
|
+
|
|
134
|
+
Release checklist (all outputs shown, per DELIVERY.md proof discipline):
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm run typecheck && npx vitest run # green
|
|
138
|
+
npm run build # tsup + verify-dist gate green
|
|
139
|
+
npm pkg get dependencies # expected (ideally {})
|
|
140
|
+
npm pack --dry-run # tarball = dist + manifest, nothing else
|
|
141
|
+
# CHANGELOG entry (+ MIGRATION entry if breaking) → bump version → git tag vX.Y.Z
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 9. Naming & style
|
|
145
|
+
|
|
146
|
+
- One exported class per file, file named after it: `AuthClient.ts`, `ProjectClient.ts`.
|
|
147
|
+
Resource clients end in `Client`; ports are interfaces named for the capability
|
|
148
|
+
(`HttpTransport`, `SDKContext`, `SDKLogger`).
|
|
149
|
+
- Names express intent, not technology (`ProjectClient.list()`, not `getProjectsHTTP`).
|
|
150
|
+
- String-literal unions over `enum` (erasable, tree-shakeable, JSON-friendly).
|
|
151
|
+
- `async/await` over `.then()` chains; no default exports; no module-level mutable state
|
|
152
|
+
outside class instances (the inflight refresh promise lives on the client instance).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ts-sdk",
|
|
3
|
+
"label": "TypeScript SDK — typed client between API and front(s)",
|
|
4
|
+
"languages": ["typescript"],
|
|
5
|
+
"idPrompt": "Package name (npm-style, e.g. @org/myapp-sdk)",
|
|
6
|
+
"requirements": [
|
|
7
|
+
"Node.js 20+ (built-in fetch)",
|
|
8
|
+
"npm 10+"
|
|
9
|
+
],
|
|
10
|
+
"notes": "SDK builds and tests day one: npm install && npm run build && npm test"
|
|
11
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{BUNDLE_ID}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "{{PROJECT_NAME}} — typed SDK between the API and its clients",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"typesVersions": {
|
|
14
|
+
"*": {
|
|
15
|
+
"types": ["./dist/types/index.d.ts"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"./types": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/types/index.d.ts",
|
|
32
|
+
"default": "./dist/types/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/types/index.d.cts",
|
|
36
|
+
"default": "./dist/types/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup && node scripts/verify-dist.mjs",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"tsup": "^8.5.0",
|
|
52
|
+
"typescript": "^5.8.3",
|
|
53
|
+
"vitest": "^3.2.4"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build gate: "it built" is NOT proof the types shipped (SDK_CONTRACT.md §4).
|
|
4
|
+
* A production team once shipped 13 of 21 type modules silently missing from
|
|
5
|
+
* dist/ because the declaration generator skipped .d.ts sources and a committed
|
|
6
|
+
* dist/ masked the hole. This script makes the proof mechanical:
|
|
7
|
+
* 1. every expected dist/ artifact exists;
|
|
8
|
+
* 2. every public name is present in the emitted declarations.
|
|
9
|
+
*
|
|
10
|
+
* Runs as part of `npm run build`. Add new public names when you extend index.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const REQUIRED_FILES = [
|
|
15
|
+
'dist/index.js',
|
|
16
|
+
'dist/index.cjs',
|
|
17
|
+
'dist/index.d.ts',
|
|
18
|
+
'dist/index.d.cts',
|
|
19
|
+
'dist/types/index.js',
|
|
20
|
+
'dist/types/index.cjs',
|
|
21
|
+
'dist/types/index.d.ts',
|
|
22
|
+
'dist/types/index.d.cts',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Names that consumers import — keep in sync with src/index.ts and src/types/index.ts.
|
|
26
|
+
const PUBLIC_SURFACE = {
|
|
27
|
+
'dist/index.d.ts': [
|
|
28
|
+
'createSdk',
|
|
29
|
+
'SDKOptions',
|
|
30
|
+
'ApiError',
|
|
31
|
+
'AuthClient',
|
|
32
|
+
'HttpClient',
|
|
33
|
+
'HttpTransport',
|
|
34
|
+
'createMemoryContext',
|
|
35
|
+
'SDKContext',
|
|
36
|
+
'SDKLogger',
|
|
37
|
+
'TokenPair',
|
|
38
|
+
],
|
|
39
|
+
'dist/types/index.d.ts': ['HttpMethod', 'TokenPair', 'ApiErrorBody'],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let failed = false;
|
|
43
|
+
|
|
44
|
+
for (const file of REQUIRED_FILES) {
|
|
45
|
+
if (!existsSync(file)) {
|
|
46
|
+
console.error(`✗ missing build artifact: ${file}`);
|
|
47
|
+
failed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!failed) {
|
|
52
|
+
for (const [file, names] of Object.entries(PUBLIC_SURFACE)) {
|
|
53
|
+
const dts = readFileSync(file, 'utf8');
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
if (!dts.includes(name)) {
|
|
56
|
+
console.error(`✗ ${file}: public name "${name}" not found in emitted declarations`);
|
|
57
|
+
failed = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (failed) {
|
|
64
|
+
console.error('\ndist verification FAILED — do not tag this build.');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
console.log(`✓ dist verification OK (${REQUIRED_FILES.length} artifacts, declarations contain the public surface)`);
|