proteum 2.5.7 → 2.5.9
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 +21 -0
- package/README.md +208 -594
- package/cli/commands/dev.ts +14 -11
- package/cli/compiler/common/index.ts +4 -2
- package/cli/compiler/server/externals.ts +35 -0
- package/cli/compiler/server/index.ts +12 -3
- package/package.json +1 -1
- package/tests/dev-transpile-watch.test.cjs +64 -6
- package/tests/server-externals.test.cjs +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2026 Gaetan Le Gac
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,286 +1,122 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- Logo: drop a square mark at docs/assets/logo.png and uncomment the line below. -->
|
|
2
|
+
<!-- <p align="center"><img src="docs/assets/logo.png" width="120" alt="Proteum" /></p> -->
|
|
2
3
|
|
|
3
|
-
Proteum
|
|
4
|
+
<h1 align="center">Proteum</h1>
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
<p align="center">
|
|
7
|
+
<strong>The explicit, agent-native full-stack TypeScript framework.</strong><br />
|
|
8
|
+
Server-first SSR & SEO, zero runtime magic, and a codebase your AI agents can read without reverse-engineering.
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/proteum"><img src="https://img.shields.io/npm/v/proteum.svg?color=2563eb&label=npm" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/proteum"><img src="https://img.shields.io/npm/dm/proteum.svg?color=2563eb" alt="npm downloads" /></a>
|
|
14
|
+
<a href="#-requirements"><img src="https://img.shields.io/node/v/proteum.svg?color=2563eb" alt="node version" /></a>
|
|
15
|
+
<a href="./package.json"><img src="https://img.shields.io/badge/TypeScript-5.9-2563eb.svg?logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
16
|
+
<a href="#-built-for-ai-agents"><img src="https://img.shields.io/badge/MCP-ready-7c3aed.svg" alt="MCP ready" /></a>
|
|
17
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/proteum.svg?color=2563eb" alt="MIT license" /></a>
|
|
18
|
+
</p>
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="#-quick-start">Quick Start</a> ·
|
|
22
|
+
<a href="#-core-concepts">Concepts</a> ·
|
|
23
|
+
<a href="#-built-for-ai-agents">AI Agents</a> ·
|
|
24
|
+
<a href="#-the-cli">CLI</a> ·
|
|
25
|
+
<a href="#-documentation">Docs</a> ·
|
|
26
|
+
<a href="#-philosophy">Philosophy</a>
|
|
27
|
+
</p>
|
|
10
28
|
|
|
11
|
-
|
|
29
|
+
---
|
|
12
30
|
|
|
13
|
-
##
|
|
31
|
+
## What is Proteum?
|
|
14
32
|
|
|
15
|
-
Most full-stack frameworks optimize first for human convenience.
|
|
33
|
+
Most full-stack frameworks optimize first for human convenience and lean on ambient runtime magic to get there. **Proteum optimizes for explicitness** — typed, machine-readable contracts that stay legible to humans *and* to the AI agents that increasingly maintain real codebases.
|
|
16
34
|
|
|
17
|
-
|
|
35
|
+
Every route, controller, service, and layout is an exported **definition object**. The compiler reads them and emits deterministic contracts into `.proteum/`, so the framework can *tell you* what it discovered instead of asking you to guess. That same manifest powers the CLI, the dev profiler, and a built-in **Model Context Protocol** server — so an agent can answer "which controller handles this request, and why is it slow?" in a single low-token call.
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- server-first architecture with minimal client runtime
|
|
22
|
-
- deterministic generation instead of ambient magic
|
|
23
|
-
- codebases that stay explainable to humans and LLMs at the same time
|
|
24
|
-
|
|
25
|
-
Proteum combines:
|
|
26
|
-
|
|
27
|
-
- page-first SSR workflows similar to modern React meta-frameworks
|
|
28
|
-
- explicit controller and service layers inspired by backend frameworks
|
|
29
|
-
- generated manifests and contracts that make routes, services, layouts, and diagnostics easy to inspect
|
|
30
|
-
|
|
31
|
-
## Core Principles
|
|
32
|
-
|
|
33
|
-
- **Server-first by default.** Put data loading in the page data function and keep client code focused on UI.
|
|
34
|
-
- **Explicit request entrypoints.** Routes and controllers are exported definition objects.
|
|
35
|
-
- **Local validation.** Declare controller input on `defineAction({ input, handler })`; handlers receive parsed `input`.
|
|
36
|
-
- **Deterministic generation.** Proteum owns `.proteum/` and regenerates it from source.
|
|
37
|
-
- **Explainability matters.** `proteum explain`, `proteum doctor`, `proteum diagnose`, `proteum perf`, and `proteum trace` expose the framework view of your app and its live requests, and the profiler renders the same diagnostics and perf surfaces for humans in dev.
|
|
38
|
-
- **SEO is not an afterthought.** Identity, routes, layouts, and SSR data are part of the app contract.
|
|
39
|
-
|
|
40
|
-
## What a Proteum App Looks Like
|
|
41
|
-
|
|
42
|
-
```text
|
|
43
|
-
my-app/
|
|
44
|
-
identity.config.ts
|
|
45
|
-
proteum.config.ts
|
|
46
|
-
.env # optional file for required local env vars
|
|
47
|
-
package.json
|
|
48
|
-
commands/
|
|
49
|
-
client/
|
|
50
|
-
pages/
|
|
51
|
-
_layout/
|
|
52
|
-
components/
|
|
53
|
-
islands/
|
|
54
|
-
services/
|
|
55
|
-
server/
|
|
56
|
-
config/
|
|
57
|
-
index.ts
|
|
58
|
-
controllers/
|
|
59
|
-
services/
|
|
60
|
-
common/
|
|
61
|
-
models/
|
|
62
|
-
router/
|
|
63
|
-
errors/
|
|
64
|
-
.proteum/
|
|
65
|
-
manifest.json
|
|
66
|
-
client/
|
|
67
|
-
common/
|
|
68
|
-
server/
|
|
37
|
+
```bash
|
|
38
|
+
npx proteum init my-app --name "My App"
|
|
69
39
|
```
|
|
70
40
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- `identity.config.ts`: typed app identity, naming, locale, and SEO-facing metadata defaults via `Application.identity({ ... })`
|
|
74
|
-
- `proteum.config.ts`: typed Proteum compiler and connection settings such as `transpile` and `connect` via `Application.setup({ ... })`
|
|
75
|
-
- `process.env` / optional `.env`: `PORT`, `ENV_*`, `URL`, `URL_INTERNAL`, any app-chosen variables referenced by `proteum.config.ts`, and `TRACE_*` environment variables loaded by the app
|
|
76
|
-
- `server/config/*.ts`: plain typed config exports consumed by the explicit app bootstrap
|
|
77
|
-
- `server/index.ts`: default-exported `defineApplication({ services, router, models, commands })` application graph
|
|
78
|
-
- `client/pages/**`: SSR page entrypoints that default-export `definePageRoute({ path, options, data, render })`
|
|
79
|
-
- `server/controllers/**`: generated API definitions that default-export `defineController({ path, actions })`
|
|
80
|
-
- `commands/**`: dev-only internal commands that extend `Commands`
|
|
81
|
-
- `server/services/**`: business logic that extends `Service`
|
|
82
|
-
- `.proteum/**`: framework-owned generated contracts and manifests
|
|
83
|
-
|
|
84
|
-
Required Proteum env vars:
|
|
85
|
-
|
|
86
|
-
- `ENV_NAME`: `local` or `server`
|
|
87
|
-
- `ENV_PROFILE`: `dev`, `testing`, or `prod`
|
|
88
|
-
- `PORT`: default router port
|
|
89
|
-
- `URL`: canonical absolute base URL for `Router.url(..., true)`
|
|
90
|
-
- `URL_INTERNAL`: internal absolute base URL used by SSR and connected-project server calls
|
|
91
|
-
|
|
92
|
-
If `proteum.config.ts` declares `connect`, Proteum also requires:
|
|
93
|
-
|
|
94
|
-
- one explicit `connect.<Namespace>.source` value in `proteum.config.ts`
|
|
95
|
-
- one explicit `connect.<Namespace>.urlInternal` value in `proteum.config.ts`
|
|
96
|
-
|
|
97
|
-
Proteum does not provide defaults for required env vars. They must be defined explicitly in `process.env` or `.env`.
|
|
98
|
-
|
|
99
|
-
Use `proteum explain env` to see the required env vars, their allowed values, and whether each one is currently provided.
|
|
100
|
-
|
|
101
|
-
Optional trace env vars:
|
|
102
|
-
|
|
103
|
-
- `TRACE_ENABLE`
|
|
104
|
-
- `TRACE_REQUESTS_LIMIT`
|
|
105
|
-
- `TRACE_EVENTS_LIMIT`
|
|
106
|
-
- `TRACE_CAPTURE`
|
|
107
|
-
- `TRACE_PERSIST_ON_ERROR`
|
|
108
|
-
- `ENABLE_PROFILER`
|
|
109
|
-
|
|
110
|
-
Optional `proteum.config.ts` fields:
|
|
41
|
+
## ✨ Highlights
|
|
111
42
|
|
|
112
|
-
-
|
|
113
|
-
-
|
|
43
|
+
- **🧩 Explicit by design** — routes, controllers, services, and the app graph are typed definition objects, not decorators or filename conventions hiding behind a compiler.
|
|
44
|
+
- **⚡ Server-first SSR** — fast Preact / React 19 server rendering with a minimal client runtime and an islands model for interactivity exactly where you need it.
|
|
45
|
+
- **🔎 SEO as a primitive** — typed app identity, metadata, JSON-LD, and canonical URLs are part of the application contract, not a plugin you bolt on later.
|
|
46
|
+
- **🧠 Built for AI agents** — a machine-scope MCP router, compact JSON diagnostics, and generated manifests give LLMs a reliable map of your app. [Jump to details ↓](#-built-for-ai-agents)
|
|
47
|
+
- **📈 Live observability** — built-in request tracing, performance roll-ups, and an interactive dev profiler with charts over the same contracts the CLI reads.
|
|
48
|
+
- **🛡️ Validation at the edge** — declare action input once with `defineAction({ input, handler })`; handlers receive parsed, typed input.
|
|
49
|
+
- **🗄️ Prisma-first data layer** — typed Prisma 7 models (MySQL / MariaDB / Postgres) on `this.models`, with tagged-template SQL as an escape hatch when you need it.
|
|
50
|
+
- **🔗 Monorepo & connected apps** — compose multiple apps with typed cross-app controller contracts.
|
|
51
|
+
- **🚀 Production builds** — an `rspack` + Tailwind 4 pipeline with bundle analysis baked in.
|
|
114
52
|
|
|
115
|
-
|
|
53
|
+
## 📋 Requirements
|
|
116
54
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const PRODUCT_URL_INTERNAL = process.env.PRODUCT_URL_INTERNAL;
|
|
122
|
-
|
|
123
|
-
export default Application.setup({
|
|
124
|
-
transpile: ['@acme/components'],
|
|
125
|
-
connect: {
|
|
126
|
-
Product: {
|
|
127
|
-
source: PRODUCT_CONNECTED_SOURCE,
|
|
128
|
-
urlInternal: PRODUCT_URL_INTERNAL,
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Connected contract sources are provided explicitly through `proteum.config.ts` instead of being inferred from the namespace:
|
|
55
|
+
| Runtime | Version |
|
|
56
|
+
| ------- | ------------ |
|
|
57
|
+
| Node.js | `>= 20.19.0` |
|
|
58
|
+
| npm | `>= 3.10.10` |
|
|
135
59
|
|
|
136
|
-
|
|
137
|
-
- remote runtime-only source value: `github:owner/repo?ref=<sha-or-branch>&path=proteum.connected.json`
|
|
60
|
+
## 🚀 Quick Start
|
|
138
61
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
62
|
+
```bash
|
|
63
|
+
# Scaffold a new app from deterministic built-in templates
|
|
64
|
+
npx proteum init my-app --name "My App"
|
|
65
|
+
cd my-app
|
|
142
66
|
|
|
143
|
-
|
|
67
|
+
# Install and wire up agent instructions (AGENTS.md / CLAUDE.md)
|
|
68
|
+
npm install
|
|
69
|
+
npx proteum configure agents
|
|
144
70
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
import { Services, type ServiceConfig } from '@server/app';
|
|
148
|
-
import AppContainer from '@server/app/container';
|
|
149
|
-
import Router from '@server/services/router';
|
|
150
|
-
import Users from '@/server/services/Users';
|
|
151
|
-
|
|
152
|
-
type RouterBaseConfig = Omit<ServiceConfig<typeof Router>, 'plugins'>;
|
|
153
|
-
|
|
154
|
-
export const usersConfig = Services.config(Users, {});
|
|
155
|
-
|
|
156
|
-
export const routerBaseConfig = {
|
|
157
|
-
currentDomain: AppContainer.Environment.router.currentDomain,
|
|
158
|
-
http: {
|
|
159
|
-
domain: 'example.com',
|
|
160
|
-
port: AppContainer.Environment.router.port,
|
|
161
|
-
ssl: true,
|
|
162
|
-
upload: { maxSize: '10mb' },
|
|
163
|
-
},
|
|
164
|
-
context: () => ({}),
|
|
165
|
-
} satisfies RouterBaseConfig;
|
|
71
|
+
# Start the compiler, SSR server, and hot-reload loop
|
|
72
|
+
npx proteum dev
|
|
166
73
|
```
|
|
167
74
|
|
|
168
|
-
|
|
169
|
-
// server/index.ts
|
|
170
|
-
import { defineApplication, type Application } from '@server/app';
|
|
171
|
-
import Router from '@server/services/router';
|
|
172
|
-
import SchemaRouter from '@server/services/schema/router';
|
|
173
|
-
import Users from '@/server/services/Users';
|
|
174
|
-
import * as userConfig from '@/server/config/user';
|
|
175
|
-
|
|
176
|
-
type MyAppServices = {
|
|
177
|
-
Users: Users;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
type MyRouterPlugins = {
|
|
181
|
-
schema: SchemaRouter;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
export type MyRouter = Router<MyApp, MyRouterPlugins>;
|
|
185
|
-
export interface MyApp extends Application, MyAppServices {
|
|
186
|
-
Router: MyRouter;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const createRouter = (app: MyApp): MyRouter =>
|
|
190
|
-
new Router<MyApp, MyRouterPlugins>(
|
|
191
|
-
app,
|
|
192
|
-
{
|
|
193
|
-
...userConfig.routerBaseConfig,
|
|
194
|
-
plugins: {
|
|
195
|
-
schema: new SchemaRouter({}, app),
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
app
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const createServices = (app: MyApp): MyAppServices => ({
|
|
202
|
-
Users: new Users(app, userConfig.usersConfig, app),
|
|
203
|
-
});
|
|
75
|
+
Then the everyday loop:
|
|
204
76
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
export default MyApplication;
|
|
77
|
+
```bash
|
|
78
|
+
npx proteum check # refresh contracts, typecheck, and lint in one pass
|
|
79
|
+
npx proteum build --prod # production server + client bundles into bin/
|
|
80
|
+
node ./bin/server.js # run it
|
|
211
81
|
```
|
|
212
82
|
|
|
213
|
-
|
|
83
|
+
A typical app `package.json`:
|
|
214
84
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
port: AppContainer.Environment.router.port,
|
|
225
|
-
ssl: true,
|
|
226
|
-
upload: { maxSize: '10mb' },
|
|
227
|
-
cache: {
|
|
228
|
-
html: {
|
|
229
|
-
dynamic: {
|
|
230
|
-
cacheControl: 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
231
|
-
surrogateControl: 'no-store',
|
|
232
|
-
},
|
|
233
|
-
static: {
|
|
234
|
-
cacheControl: 'public, max-age=0, must-revalidate',
|
|
235
|
-
surrogateControl: false,
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
publicAssets: {
|
|
239
|
-
dev: 'no-store',
|
|
240
|
-
versioned: 'public, max-age=31536000, immutable',
|
|
241
|
-
unversioned: 'public, max-age=0, must-revalidate',
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
context: () => ({}),
|
|
246
|
-
} satisfies RouterBaseConfig;
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"scripts": {
|
|
88
|
+
"dev": "proteum dev",
|
|
89
|
+
"check": "proteum check",
|
|
90
|
+
"build": "proteum build --prod",
|
|
91
|
+
"start": "node ./bin/server.js"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
247
94
|
```
|
|
248
95
|
|
|
249
|
-
|
|
96
|
+
## 🧱 Core Concepts
|
|
250
97
|
|
|
251
|
-
|
|
98
|
+
Everything you author is an explicit, typed definition object. Here is the whole surface in four snippets.
|
|
252
99
|
|
|
253
|
-
|
|
100
|
+
### Pages — server-first SSR
|
|
254
101
|
|
|
255
102
|
```tsx
|
|
256
103
|
import { definePageRoute } from '@common/router/definitions';
|
|
257
104
|
|
|
258
105
|
export default definePageRoute({
|
|
259
106
|
path: '/',
|
|
260
|
-
options: {
|
|
261
|
-
|
|
262
|
-
layout: false,
|
|
263
|
-
},
|
|
107
|
+
options: { auth: false, layout: false },
|
|
108
|
+
// Runs on the server. Every returned key becomes page data.
|
|
264
109
|
data: ({ Plans, Stats }) => ({
|
|
265
110
|
plans: Plans.getPlans(),
|
|
266
111
|
stats: Stats.general(),
|
|
267
112
|
}),
|
|
268
|
-
render: ({ plans, stats }) => {
|
|
269
|
-
return <LandingPage plans={plans} stats={stats} />;
|
|
270
|
-
},
|
|
113
|
+
render: ({ plans, stats }) => <LandingPage plans={plans} stats={stats} />,
|
|
271
114
|
});
|
|
272
115
|
```
|
|
273
116
|
|
|
274
|
-
|
|
117
|
+
`path` and `options` (`auth`, `layout`, `static`, `redirectLogged`, …) are static and compiler-readable. Runtime references are allowed only inside `data` and `render`.
|
|
275
118
|
|
|
276
|
-
|
|
277
|
-
- route behavior such as `auth`, `layout`, `static`, or `redirectLogged` lives in the options object
|
|
278
|
-
- every key returned from `data` becomes page data
|
|
279
|
-
- runtime app/client references are allowed only inside `data` and `render`
|
|
280
|
-
|
|
281
|
-
## Example: Controller
|
|
282
|
-
|
|
283
|
-
Proteum controllers are explicit request entrypoints.
|
|
119
|
+
### Controllers — typed request entrypoints
|
|
284
120
|
|
|
285
121
|
```ts
|
|
286
122
|
import { defineAction, defineController, schema } from '@generated/server/controller';
|
|
@@ -293,53 +129,16 @@ export default defineController({
|
|
|
293
129
|
email: schema.string().email(),
|
|
294
130
|
password: schema.string().min(8),
|
|
295
131
|
}),
|
|
296
|
-
handler: ({ input, services, request }) =>
|
|
297
|
-
|
|
298
|
-
},
|
|
132
|
+
handler: ({ input, services, request }) =>
|
|
133
|
+
services.Auth.loginWithPassword(input, request),
|
|
299
134
|
}),
|
|
300
135
|
},
|
|
301
136
|
});
|
|
302
137
|
```
|
|
303
138
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
- read request-scoped values from action context
|
|
307
|
-
- declare validation once with `defineAction({ input, handler })`
|
|
308
|
-
- call business logic through `services`, `models`, or `app`
|
|
309
|
-
- return explicit values instead of relying on ambient globals
|
|
310
|
-
|
|
311
|
-
## Example: Command
|
|
312
|
-
|
|
313
|
-
Proteum commands are explicit dev-only internal entrypoints.
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
import { Commands } from '@server/app/commands';
|
|
317
|
-
|
|
318
|
-
export default class DiagnosticsCommands extends Commands {
|
|
319
|
-
public async ping() {
|
|
320
|
-
const { Stats } = this.services;
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
app: this.app.identity.identifier,
|
|
324
|
-
domains: await Stats.general(),
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
Command rules:
|
|
139
|
+
Validation lives next to the handler. Business logic is reached through `services`, `models`, or `app` — never ambient globals.
|
|
331
140
|
|
|
332
|
-
|
|
333
|
-
- each file default-exports a class extending `Commands` from `@server/app/commands`
|
|
334
|
-
- methods with bodies become generated dev commands
|
|
335
|
-
- command path comes from the file path plus the method name
|
|
336
|
-
- `export const commandPath = 'Custom/path'` can override the base path when needed
|
|
337
|
-
- `commands/tsconfig.json` and `.proteum/server/commands.d.ts` give `/commands` its own dev-only alias and app typing surface
|
|
338
|
-
- commands run only in dev contexts: `proteum command ...`, the dev profiler, or dev-only `__proteum/commands` endpoints
|
|
339
|
-
|
|
340
|
-
## Example: Service
|
|
341
|
-
|
|
342
|
-
Proteum services keep business logic out of request handlers.
|
|
141
|
+
### Services — business logic, request-free
|
|
343
142
|
|
|
344
143
|
```ts
|
|
345
144
|
import Service from '@server/app/service';
|
|
@@ -347,361 +146,176 @@ import Service from '@server/app/service';
|
|
|
347
146
|
export default class StatsService extends Service<Config, {}, MyApp, MyApp> {
|
|
348
147
|
public async general() {
|
|
349
148
|
return {
|
|
350
|
-
|
|
149
|
+
// Prisma first: typed model access on this.models
|
|
150
|
+
totalDomains: await this.models.domain.count(),
|
|
351
151
|
tlds: Object.keys(this.app.Domains.tlds).length,
|
|
152
|
+
// Need raw SQL? It's right there as an escape hatch:
|
|
153
|
+
// await this.models.SQL`SELECT COUNT(*) FROM domains`.value()
|
|
352
154
|
};
|
|
353
155
|
}
|
|
354
156
|
}
|
|
355
157
|
```
|
|
356
158
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
- services extend `Service`
|
|
360
|
-
- request context should be resolved in controllers, then passed into services as explicit values
|
|
361
|
-
- services can use `this.services`, `this.models`, and `this.app`
|
|
362
|
-
|
|
363
|
-
## Framework-Owned Generated Contracts
|
|
364
|
-
|
|
365
|
-
Proteum generates a machine-readable app description in `.proteum/`.
|
|
366
|
-
|
|
367
|
-
Typical generated artifacts:
|
|
368
|
-
|
|
369
|
-
- `.proteum/manifest.json`
|
|
370
|
-
- `.proteum/client/routes.ts`
|
|
371
|
-
- `.proteum/client/controllers.ts`
|
|
372
|
-
- `.proteum/client/layouts.ts`
|
|
373
|
-
- `.proteum/common/controllers.ts`
|
|
374
|
-
- `.proteum/server/commands.ts`
|
|
375
|
-
- `.proteum/server/routes.ts`
|
|
376
|
-
- `.proteum/server/controllers.ts`
|
|
377
|
-
|
|
378
|
-
These files are not hand-written application code. They are deterministic outputs derived from your app source and used by the runtime, the compiler, and tooling.
|
|
379
|
-
|
|
380
|
-
This is one of Proteum's most important properties: the framework can explain what it discovered instead of asking you to guess.
|
|
381
|
-
|
|
382
|
-
## CLI
|
|
383
|
-
|
|
384
|
-
Proteum ships with a compact CLI focused on the real app lifecycle:
|
|
385
|
-
|
|
386
|
-
| Command | Purpose |
|
|
387
|
-
| --- | --- |
|
|
388
|
-
| `proteum dev` | Start the compiler, SSR server, and hot reload loop |
|
|
389
|
-
| `proteum refresh` | Regenerate `.proteum` contracts and typings |
|
|
390
|
-
| `proteum typecheck` | Refresh generated typings, then run TypeScript |
|
|
391
|
-
| `proteum lint` | Run ESLint for the current app |
|
|
392
|
-
| `proteum check` | Refresh, typecheck, and lint in one command |
|
|
393
|
-
| `proteum build --prod` | Produce the production server and client bundles into `bin/`, with optional static or served bundle analysis |
|
|
394
|
-
| `proteum connect` | Inspect connected-project sources, env, cached contracts, and imported controllers |
|
|
395
|
-
| `proteum doctor` | Inspect manifest diagnostics |
|
|
396
|
-
| `proteum explain` | Explain routes, controllers, services, layouts, conventions, env, and connected projects |
|
|
397
|
-
| `proteum diagnose` | Combine owner lookup, diagnostics, trace data, and server logs for one concrete route or request target |
|
|
398
|
-
| `proteum perf` | Aggregate request-trace performance into hot paths, one-request waterfalls, regressions, and memory drift views |
|
|
399
|
-
| `proteum trace` | Inspect live dev-only request traces from the running SSR server |
|
|
400
|
-
| `proteum mcp` | Start, inspect, or attach to the machine-scope MCP router that routes live app reads by `projectId` |
|
|
401
|
-
| `proteum command` | Run a dev-only internal command locally or against a running dev server |
|
|
402
|
-
| `proteum session` | Mint a dev-only auth session token and Playwright-ready cookie payload |
|
|
403
|
-
| `proteum e2e` | Run Playwright with Proteum-managed `E2E_*` values instead of shell-leading env assignments |
|
|
404
|
-
| `proteum verify` | Validate targeted changed-file checks, focused owner/request/browser workflows, or the full framework reference-app pass |
|
|
405
|
-
| `proteum init` | Scaffold a new Proteum app with built-in deterministic templates |
|
|
406
|
-
| `proteum configure agents` | Interactively configure tracked Proteum instruction files and Claude aliases |
|
|
407
|
-
| `proteum create` | Scaffold a page, controller, command, route, or root service inside an app |
|
|
408
|
-
| `proteum worktree` | Create or initialize Codex worktrees with a machine-readable bootstrap marker |
|
|
409
|
-
|
|
410
|
-
Recommended daily workflow:
|
|
411
|
-
|
|
412
|
-
```bash
|
|
413
|
-
proteum dev
|
|
414
|
-
proteum refresh
|
|
415
|
-
proteum check
|
|
416
|
-
proteum verify changed --dry-run
|
|
417
|
-
proteum build --prod
|
|
418
|
-
proteum build --prod --analyze
|
|
419
|
-
proteum build --prod --analyze --analyze-serve --analyze-port auto
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
Only the bare `proteum build` and bare `proteum dev` commands print the welcome banner and include the active Proteum installation method. Any extra argument or option skips the banner. `proteum dev` is the only command that clears the interactive terminal before rendering its live session UI, exposes `CTRL+R` reload plus `CTRL+C` shutdown hotkeys, and prints connected app names plus successful connected `/ping` checks in the server-ready banner. Every `proteum dev` start ensures tracked Proteum instruction files contain the current managed `# Proteum Instructions` section and `CLAUDE.md` symlinks point to sibling `AGENTS.md` files before the dev loop begins.
|
|
423
|
-
|
|
424
|
-
Useful inspection commands:
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
proteum doctor
|
|
428
|
-
proteum doctor --contracts
|
|
429
|
-
proteum doctor --json
|
|
430
|
-
proteum connect
|
|
431
|
-
proteum connect --controllers
|
|
432
|
-
proteum connect --strict
|
|
433
|
-
proteum explain
|
|
434
|
-
proteum explain owner /api/Auth/CurrentUser
|
|
435
|
-
proteum explain --routes --controllers --commands
|
|
436
|
-
proteum explain --routes --controllers --commands --full
|
|
437
|
-
proteum explain --connected --controllers
|
|
438
|
-
proteum explain --all --full
|
|
439
|
-
proteum diagnose /
|
|
440
|
-
proteum diagnose /dashboard --port 3101
|
|
441
|
-
proteum perf top --since today
|
|
442
|
-
proteum perf request /dashboard --port 3101
|
|
443
|
-
proteum perf compare --baseline yesterday --target today --group-by route
|
|
444
|
-
proteum perf memory --since 1h --group-by controller
|
|
445
|
-
proteum mcp
|
|
446
|
-
proteum mcp status
|
|
447
|
-
proteum command proteum/diagnostics/ping
|
|
448
|
-
proteum command proteum/diagnostics/ping --port 3101
|
|
449
|
-
proteum session admin@example.com --role ADMIN --port 3101
|
|
450
|
-
proteum session god@example.com --role GOD --json
|
|
451
|
-
proteum e2e --port 3101 --session-email admin@example.com --session-role ADMIN tests/e2e/features/admin.spec.ts
|
|
452
|
-
proteum trace requests
|
|
453
|
-
proteum trace arm --capture deep
|
|
454
|
-
proteum trace latest
|
|
455
|
-
```
|
|
159
|
+
### Application — the explicit composition root
|
|
456
160
|
|
|
457
|
-
|
|
161
|
+
```ts
|
|
162
|
+
// server/index.ts — the canonical type root for services, router, models, and commands
|
|
163
|
+
import { defineApplication } from '@server/app';
|
|
458
164
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
proteum worktree init --source /path/to/main-app
|
|
464
|
-
proteum worktree create /path/to/.codex/worktrees/feature --source /path/to/main-app --branch feature/name
|
|
465
|
-
proteum create page marketing/faq --route /faq
|
|
466
|
-
proteum create controller Founder/projects --method list
|
|
467
|
-
proteum create service Conversion/Plans
|
|
165
|
+
export default defineApplication({
|
|
166
|
+
services: createServices, // (app) => ({ Users: new Users(...) })
|
|
167
|
+
router: createRouter, // (app) => new Router(...)
|
|
168
|
+
});
|
|
468
169
|
```
|
|
469
170
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
## Dev Sessions
|
|
494
|
-
|
|
495
|
-
Proteum includes a dev-only auth bootstrap command for browser automation, API probes, and protected-route debugging without driving the login UI.
|
|
496
|
-
|
|
497
|
-
- `proteum session <email>` mints a session for a known user
|
|
498
|
-
- `--role <role>` asserts that the resolved user has the expected role before returning the session
|
|
499
|
-
- `--port <port>` or `--url <baseUrl>` targets an existing `proteum dev` server
|
|
500
|
-
- without `--port` or `--url`, Proteum starts a temporary local dev server, creates the session, prints the payload, and exits
|
|
501
|
-
- output includes the raw token, a `Cookie:` header, and a Playwright-ready `cookies` payload
|
|
502
|
-
- prefer this command when an LLM or test runner needs an authenticated dev context
|
|
503
|
-
- do not use it when the login flow itself is what you are testing
|
|
504
|
-
|
|
505
|
-
Typical usage:
|
|
506
|
-
|
|
507
|
-
```bash
|
|
508
|
-
proteum session admin@example.com --role ADMIN --port 3101
|
|
509
|
-
proteum session god@example.com --role GOD --json
|
|
171
|
+
Proteum reads `server/index.ts` as the single source of truth for installed root services and router plugins — there is no hidden registry.
|
|
172
|
+
|
|
173
|
+
## 🧠 Built for AI Agents
|
|
174
|
+
|
|
175
|
+
This is where Proteum is different. The compiler emits a machine-readable description of your app into `.proteum/`, and **every tool reads the same snapshot** — the CLI, the dev-only HTTP endpoints, the profiler, and a Model Context Protocol server.
|
|
176
|
+
|
|
177
|
+
```mermaid
|
|
178
|
+
flowchart LR
|
|
179
|
+
subgraph src["Your source"]
|
|
180
|
+
P["pages/**"]
|
|
181
|
+
C["controllers/**"]
|
|
182
|
+
S["services/**"]
|
|
183
|
+
A["server/index.ts"]
|
|
184
|
+
end
|
|
185
|
+
src --> CMP["Proteum compiler"]
|
|
186
|
+
CMP --> GEN[".proteum/<br/>manifest + contracts"]
|
|
187
|
+
GEN --> RT["SSR runtime"]
|
|
188
|
+
GEN --> CLI["CLI<br/>explain · doctor · diagnose"]
|
|
189
|
+
GEN --> PROF["Dev profiler"]
|
|
190
|
+
GEN --> MCP["MCP server<br/>/__proteum/mcp"]
|
|
191
|
+
MCP -.->|projectId-routed reads| AGENT(("AI agent"))
|
|
192
|
+
CLI -.->|compact JSON| AGENT
|
|
510
193
|
```
|
|
511
194
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
## Request Tracing
|
|
195
|
+
An agent — or you — can ask the framework directly:
|
|
515
196
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
197
|
+
| Question | Command |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| Which controller owns this request? | `proteum explain owner /api/Auth/CurrentUser` |
|
|
200
|
+
| What did the framework detect? | `proteum doctor --json` |
|
|
201
|
+
| Why is this route failing? | `proteum diagnose /dashboard` |
|
|
202
|
+
| Where is the time going? | `proteum perf request /dashboard` |
|
|
203
|
+
| What happened in the last request? | `proteum trace latest` |
|
|
519
204
|
|
|
520
|
-
|
|
205
|
+
**Why agents work well here:**
|
|
521
206
|
|
|
522
|
-
- `proteum
|
|
523
|
-
- `proteum
|
|
524
|
-
- `proteum
|
|
525
|
-
- `proteum
|
|
526
|
-
- `proteum trace export <requestId>`: write one trace to disk
|
|
527
|
-
- `proteum trace latest --url http://127.0.0.1:3010`: target a non-standard dev base URL directly
|
|
528
|
-
- `proteum diagnose /dashboard --port 3101`: combine owner lookup, diagnostics, trace summary, and buffered logs for one concrete path
|
|
529
|
-
- `proteum perf top --since today`: rank the hottest traced paths in the selected window
|
|
530
|
-
- `proteum perf request /dashboard --port 3101`: inspect one traced request with stage timings, CPU, SQL, render, and memory deltas
|
|
531
|
-
- `proteum perf compare --baseline yesterday --target today --group-by route`: compare regression deltas between two windows
|
|
532
|
-
- `proteum perf memory --since 1h --group-by controller`: rank recent heap and RSS drift
|
|
207
|
+
- **One MCP entry point.** `proteum mcp` runs a machine-scope router; `proteum dev` exposes each app at `/__proteum/mcp`. An agent calls `workflow_start`, gets a stable `projectId`, and routes every follow-up read to the right app.
|
|
208
|
+
- **Token-efficient output.** Diagnostics default to compact `proteum-agent-v1` JSON — decision-ready summaries first, raw detail only behind `--full`, `--manifest`, or `--events`.
|
|
209
|
+
- **Generated instruction files.** `proteum configure agents` writes managed `AGENTS.md` / `CLAUDE.md` instruction routers, kept in sync on every `proteum dev` start.
|
|
210
|
+
- **Auth without UI automation.** `proteum session <email> --role ADMIN` mints a dev session (token + Playwright-ready cookie) so agents and E2E suites skip the login flow.
|
|
533
211
|
|
|
534
|
-
|
|
212
|
+
> Full agent contract: [docs/mcp.md](docs/mcp.md), [docs/diagnostics.md](docs/diagnostics.md), and [docs/agent-routing.md](docs/agent-routing.md).
|
|
535
213
|
|
|
536
|
-
|
|
214
|
+
## 📊 Diagnostics & Observability
|
|
537
215
|
|
|
538
|
-
|
|
539
|
-
- traces live in memory and are bounded by `TRACE_REQUESTS_LIMIT` and `TRACE_EVENTS_LIMIT`
|
|
540
|
-
- payloads are summarized, long strings are truncated, and sensitive fields such as cookies, passwords, and tokens are redacted
|
|
541
|
-
- `TRACE_PERSIST_ON_ERROR` can export crashing requests under `var/traces/`
|
|
542
|
-
- `proteum dev` removes auto-persisted crash traces from `var/traces/` when the dev session stops
|
|
543
|
-
- `ENABLE_PROFILER=true` reuses the same instrumentation path to populate `request.profiling` and the router `request.finished` hook with a reduced request/API/SQL snapshot in any environment, without retaining finished requests in the global trace buffer unless dev trace is also enabled
|
|
216
|
+
Proteum ships one request-instrumentation system with two shapes: a retained **dev trace** buffer and a reduced request-local **profiling** snapshot.
|
|
544
217
|
|
|
545
|
-
|
|
218
|
+
- **`proteum trace`** — live, in-memory traces for auth, routing, controller, context, SSR, API, Prisma SQL, and render, with sensitive fields redacted and payloads summarized.
|
|
219
|
+
- **`proteum perf`** — aggregates those same traces into hot paths, one-request waterfalls, regression comparisons, and memory-drift views.
|
|
220
|
+
- **Dev profiler** — the panel during `proteum dev` renders `Summary`, `Auth`, `Routing`, `Controller`, `SSR`, `API`, `SQL`, `Errors`, `Perf`, and more as visual charts over the same live contracts.
|
|
546
221
|
|
|
547
222
|
```bash
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
export TRACE_CAPTURE=resolve
|
|
552
|
-
export TRACE_PERSIST_ON_ERROR=true
|
|
553
|
-
export ENABLE_PROFILER=true
|
|
223
|
+
proteum trace arm --capture deep # force the next request into deep capture
|
|
224
|
+
proteum perf top --since today # rank the hottest traced paths
|
|
225
|
+
proteum perf compare --baseline yesterday --target today --group-by route
|
|
554
226
|
```
|
|
555
227
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
- `summary`: request lifecycle plus high-signal events
|
|
559
|
-
- `resolve`: adds auth, route resolution, and controller/context steps
|
|
560
|
-
- `deep`: adds route skip reasons and deeper payload summaries for one request investigation
|
|
561
|
-
|
|
562
|
-
In the dev profiler, the request-trace tabs are now visual as well as textual: `Summary`, `Auth`, `Routing`, `Controller`, `SSR`, `API`, `SQL`, `Errors`, `Diagnose`, `Explain`, `Doctor`, `Commands`, and `Cron` all add focused charts over the same live contracts, while `Perf` remains the aggregated hot-path, breakdown, regression, and memory surface exposed by `proteum perf`.
|
|
563
|
-
|
|
564
|
-
The trace and perf CLIs talk to the running dev server over the dev-only `__proteum/trace` and `__proteum/perf` HTTP endpoints. Use `--port` for a different local port or `--url` when the host itself is non-standard. For the full guide, see [docs/request-tracing.md](docs/request-tracing.md).
|
|
565
|
-
|
|
566
|
-
## LLM-Friendly By Design
|
|
228
|
+
> Full guide: [docs/request-tracing.md](docs/request-tracing.md).
|
|
567
229
|
|
|
568
|
-
|
|
230
|
+
## 🛠️ The CLI
|
|
569
231
|
|
|
570
|
-
|
|
571
|
-
- Which routes exist?
|
|
572
|
-
- Which controller handles a request?
|
|
573
|
-
- Which services are installed?
|
|
574
|
-
- Which layouts exist?
|
|
575
|
-
- Which diagnostics did the framework detect?
|
|
232
|
+
A compact CLI focused on the real app lifecycle.
|
|
576
233
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
- `proteum command ...` plus the profiler `Commands` tab for dev-only internal execution
|
|
594
|
-
- `proteum session ...` for explicit authenticated dev browser or API bootstrapping without login UI automation
|
|
595
|
-
- `proteum e2e ...` for Playwright runs that need `E2E_BASE_URL`, `E2E_PORT`, or `E2E_AUTH_TOKEN` without shell-leading env assignments
|
|
596
|
-
|
|
597
|
-
If you are an LLM or automation agent, start here:
|
|
598
|
-
|
|
599
|
-
1. Use `proteum mcp` as the one registered MCP server; `proteum dev` ensures the managed machine daemon is running.
|
|
600
|
-
2. Call MCP `workflow_start` with `cwd` or a known `projectId`; if it is ambiguous or returns offline app candidates, use `project_resolve { cwd }`, choose the intended app root, follow its fresh-copy readiness and port-inspected next actions when needed, then retry `workflow_start`.
|
|
601
|
-
3. If `workflow_start` returns `data.readiness.state="blocked"`, resolve the returned setup actions first. The read-only readiness preflight covers app/root `.env`, dependency install root, generated manifest state, local connected producer apps, Prisma/client readiness, redacted database URL shape, local database TCP reachability, and exact safe setup commands.
|
|
602
|
-
4. If the app root is inside `/.codex/worktrees/` and `workflow_start` or a guarded CLI command reports missing/stale bootstrap, run the returned `npx proteum worktree init --source <source-app-root>` command before runtime reads.
|
|
603
|
-
5. Use the returned live `projectId` with MCP `runtime_status`, `orient`, `instructions_resolve`, `route_candidates`, `explain_summary`, `diagnose`, `trace_show`, `perf_request`, and `logs_tail` before CLI equivalents for repeated read-only app state.
|
|
604
|
-
6. Treat returned instruction previews as the allowed scope for read-only discovery and diagnostics. Read full file contents only before edits or git writes, when `fullRead`/`fullReadPolicy` requires it, or when compact previews are insufficient.
|
|
605
|
-
7. Use compact CLI commands for fallback, `dev`, `build`, `check`, `verify`, migrations, E2E, and final reproducible terminal evidence.
|
|
606
|
-
8. Use `proteum diagnose`, `proteum perf`, and compact `proteum trace` for reproducible command evidence when MCP is unavailable or the terminal output itself is needed.
|
|
607
|
-
9. If machine MCP routing fails, run `proteum mcp status` and `proteum runtime status` from the intended app root; if no live session exists, use the exact MCP offline or runtime-status next action. If the same app already responds on the configured port without live tracking, use or repair that runtime instead of starting another server. If a live session exists but runtime/MCP is unreachable, stop the listed session file first, then start dev again and retry `workflow_start`. Do not run diagnose, trace, or perf reads while runtime health is unreachable, and do not `curl` normal page routes to identify port ownership.
|
|
608
|
-
10. Inspect `server/index.ts`, controllers, services, or pages only after the routing/diagnostic surfaces identify the relevant owner. Do not run broad owner searches after MCP already returned the route/page/controller owner.
|
|
609
|
-
11. If the task touches a protected route or controller in dev and login UX is not the feature under test, use `proteum e2e --session-email <email> --session-role <role>` for Playwright suites or `proteum session <email> --role <role>` before direct HTTP calls.
|
|
610
|
-
|
|
611
|
-
For implementation rules in a real Proteum app, treat the routed local `AGENTS.md` files plus `proteum orient`, compact CLI diagnostics, and MCP repeated-read surfaces as the task contract. This README is the framework overview, not the project-local instruction layer.
|
|
612
|
-
|
|
613
|
-
## What Proteum Avoids
|
|
614
|
-
|
|
615
|
-
Proteum intentionally avoids several patterns that make frameworks harder to inspect and harder to trust:
|
|
616
|
-
|
|
617
|
-
- hidden runtime globals
|
|
618
|
-
- implicit service registration hidden behind bootstrap helpers
|
|
619
|
-
- implicit request state inside business services
|
|
620
|
-
- controller validation defined far away from the handler
|
|
621
|
-
- route systems that cannot be explained without reading the compiler
|
|
622
|
-
- generated code that hides where it came from
|
|
623
|
-
|
|
624
|
-
## Real-World Shape
|
|
625
|
-
|
|
626
|
-
Proteum is already used on large application surfaces with:
|
|
234
|
+
| Command | Purpose |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| `proteum dev` | Compiler + SSR server + hot-reload loop, with a live profiler |
|
|
237
|
+
| `proteum build --prod` | Production server & client bundles into `bin/` (`--analyze` for bundle reports) |
|
|
238
|
+
| `proteum refresh` | Regenerate `.proteum` contracts and typings |
|
|
239
|
+
| `proteum check` | Refresh, typecheck, and lint in one command |
|
|
240
|
+
| `proteum create` | Scaffold a page, controller, command, route, or service |
|
|
241
|
+
| `proteum init` | Scaffold a new app from deterministic templates |
|
|
242
|
+
| `proteum explain` | Inspect routes, controllers, services, layouts, env, and connected projects |
|
|
243
|
+
| `proteum doctor` | Inspect manifest diagnostics |
|
|
244
|
+
| `proteum diagnose` | Owner + diagnostics + traces + logs for one route or request |
|
|
245
|
+
| `proteum perf` / `proteum trace` | Performance roll-ups and live request traces |
|
|
246
|
+
| `proteum mcp` | Machine-scope MCP router for live app reads |
|
|
247
|
+
| `proteum connect` | Inspect connected-project sources, env, and imported controllers |
|
|
248
|
+
| `proteum session` / `proteum e2e` | Dev auth bootstrap and Playwright runs without shell env juggling |
|
|
249
|
+
| `proteum verify` | Targeted change checks or the full reference-app pass |
|
|
627
250
|
|
|
628
|
-
|
|
629
|
-
- SSR landing pages and authenticated app pages
|
|
630
|
-
- generated controller accessors injected into page context
|
|
631
|
-
- build, typecheck, lint, and diagnostic workflows run from the CLI
|
|
251
|
+
Run `proteum --help` or `proteum help <command>` for the full reference.
|
|
632
252
|
|
|
633
|
-
|
|
253
|
+
## 🏗️ Project Structure
|
|
634
254
|
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
255
|
+
```text
|
|
256
|
+
my-app/
|
|
257
|
+
├─ identity.config.ts # typed app identity, locale, and SEO defaults
|
|
258
|
+
├─ proteum.config.ts # compiler + connected-project settings
|
|
259
|
+
├─ client/
|
|
260
|
+
│ ├─ pages/ # SSR page entrypoints (definePageRoute)
|
|
261
|
+
│ ├─ islands/ # interactive client islands
|
|
262
|
+
│ ├─ components/
|
|
263
|
+
│ └─ services/
|
|
264
|
+
├─ server/
|
|
265
|
+
│ ├─ index.ts # defineApplication — the app graph
|
|
266
|
+
│ ├─ controllers/ # defineController + defineAction
|
|
267
|
+
│ ├─ services/ # business logic (extends Service)
|
|
268
|
+
│ └─ config/
|
|
269
|
+
├─ common/ # shared router contracts, models, errors
|
|
270
|
+
├─ commands/ # dev-only internal commands
|
|
271
|
+
└─ .proteum/ # framework-owned generated contracts (do not edit)
|
|
646
272
|
```
|
|
647
273
|
|
|
648
|
-
##
|
|
649
|
-
|
|
650
|
-
Proteum currently targets:
|
|
651
|
-
|
|
652
|
-
- Node.js `>=20.19.0`
|
|
653
|
-
- npm `>=3.10.10`
|
|
654
|
-
|
|
655
|
-
Install in an app:
|
|
656
|
-
|
|
657
|
-
```bash
|
|
658
|
-
npm install proteum
|
|
659
|
-
```
|
|
274
|
+
## 📚 Documentation
|
|
660
275
|
|
|
661
|
-
|
|
276
|
+
| Topic | Guide |
|
|
277
|
+
| --- | --- |
|
|
278
|
+
| Diagnostics & explainability | [docs/diagnostics.md](docs/diagnostics.md) |
|
|
279
|
+
| Model Context Protocol (MCP) | [docs/mcp.md](docs/mcp.md) |
|
|
280
|
+
| Request tracing & perf | [docs/request-tracing.md](docs/request-tracing.md) |
|
|
281
|
+
| Agent routing & token efficiency | [docs/agent-routing.md](docs/agent-routing.md) |
|
|
282
|
+
| Dev commands | [docs/dev-commands.md](docs/dev-commands.md) |
|
|
283
|
+
| Dev sessions | [docs/dev-sessions.md](docs/dev-sessions.md) |
|
|
284
|
+
| Migrating to 2.5 | [docs/migration-2.5.md](docs/migration-2.5.md) |
|
|
662
285
|
|
|
663
|
-
|
|
664
|
-
npx proteum init my-app --name "My App"
|
|
665
|
-
npx proteum init my-app --name "My App" --dry-run --json
|
|
666
|
-
```
|
|
286
|
+
## 🧭 Philosophy
|
|
667
287
|
|
|
668
|
-
|
|
288
|
+
Proteum is opinionated on purpose. It intentionally **avoids** the patterns that make frameworks hard to inspect and hard to trust:
|
|
669
289
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
```
|
|
290
|
+
- ❌ hidden runtime globals
|
|
291
|
+
- ❌ implicit service registration behind bootstrap helpers
|
|
292
|
+
- ❌ request state smuggled into business services
|
|
293
|
+
- ❌ validation defined far from its handler
|
|
294
|
+
- ❌ routing you cannot explain without reading the compiler
|
|
295
|
+
- ❌ generated code that hides where it came from
|
|
677
296
|
|
|
678
|
-
|
|
297
|
+
In their place: explicit definition objects, a single canonical app graph, validation at the edge, and deterministic generation you can read, diff, and trace back to source.
|
|
679
298
|
|
|
680
|
-
|
|
299
|
+
## 🧰 Built With
|
|
681
300
|
|
|
682
|
-
|
|
301
|
+
[TypeScript](https://www.typescriptlang.org/) · [Preact](https://preactjs.com/) / [React 19](https://react.dev/) · [Express](https://expressjs.com/) · [Prisma 7](https://www.prisma.io/) · [rspack](https://rspack.dev/) · [Tailwind CSS 4](https://tailwindcss.com/) · [Zod](https://zod.dev/) · [Ink](https://github.com/vadimdemedes/ink) · [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
683
302
|
|
|
684
|
-
##
|
|
303
|
+
## 🤝 Contributing
|
|
685
304
|
|
|
686
|
-
|
|
305
|
+
Issues and pull requests are welcome. Proteum is actively hardening its explicit model, and the direction is deliberate: fewer ways to do the same thing, more contracts the framework can explain on its own.
|
|
687
306
|
|
|
688
|
-
|
|
689
|
-
- `client/`: client runtime, page registration, islands, and router behavior
|
|
690
|
-
- `server/`: controller base classes, services, runtime, and SSR server behavior
|
|
691
|
-
- `common/`: shared router contracts, models, request/response types, and utilities
|
|
692
|
-
- `docs/`: focused design notes and internal documentation
|
|
693
|
-
- `agents/`: agent-specific conventions and scaffolding used in Proteum-based projects
|
|
307
|
+
When proposing a change, start from a concrete mismatch or risk visible in a real app, show the target API with realistic client/server usage, and keep generated code deterministic and auditable.
|
|
694
308
|
|
|
695
|
-
##
|
|
309
|
+
## 💜 Sponsors
|
|
696
310
|
|
|
697
|
-
Proteum is
|
|
311
|
+
Proteum is proudly sponsored by **[Unique Domains](https://unique.domains/?utm_source=github&utm_medium=referral&utm_campaign=repo_proteum&utm_content=top_sponsor)**.
|
|
698
312
|
|
|
699
|
-
|
|
313
|
+
<p>
|
|
314
|
+
<a href="https://unique.domains/?utm_source=github&utm_medium=referral&utm_campaign=repo_proteum&utm_content=top_sponsor">
|
|
315
|
+
<img src="docs/assets/unique-domains-chip.png" alt="Unique Domains" />
|
|
316
|
+
</a>
|
|
317
|
+
</p>
|
|
700
318
|
|
|
701
|
-
|
|
702
|
-
- more generated and auditable contracts
|
|
703
|
-
- clearer controller and service boundaries
|
|
704
|
-
- better SSR, SEO, and explainability defaults
|
|
705
|
-
- better ergonomics for both humans and AI agents
|
|
319
|
+
## 📄 License
|
|
706
320
|
|
|
707
|
-
|
|
321
|
+
[MIT](./LICENSE) © [Gaetan Le Gac](https://github.com/gaetanlegac)
|
package/cli/commands/dev.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { spawn, ChildProcess } from 'child_process';
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
|
-
import type
|
|
9
|
+
import { realpathSync, watch, type FSWatcher } from 'fs';
|
|
10
10
|
import prompts from 'prompts';
|
|
11
11
|
import { UsageError } from 'clipanion';
|
|
12
12
|
|
|
@@ -677,6 +677,16 @@ function normalizeWatchPath(watchPath: string) {
|
|
|
677
677
|
return path.resolve(watchPath).replace(/\\/g, '/').replace(/\/$/, '');
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
+
const resolveWatchPathAliases = (watchPath: string) => {
|
|
681
|
+
const aliases = new Set([normalizeWatchPath(watchPath)]);
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
aliases.add(normalizeWatchPath(realpathSync(watchPath)));
|
|
685
|
+
} catch {}
|
|
686
|
+
|
|
687
|
+
return [...aliases];
|
|
688
|
+
};
|
|
689
|
+
|
|
680
690
|
type TIndexedSourceWatchEvent = 'change' | 'rename';
|
|
681
691
|
type TIndexedSourceWatchCompilerName = 'server' | 'client';
|
|
682
692
|
type TIndexedSourceWatchInvalidateTarget = 'all' | TIndexedSourceWatchCompilerName;
|
|
@@ -692,14 +702,7 @@ type TMultiWatchingLike = TDevWatching & { watchings?: TNamedWatching[] };
|
|
|
692
702
|
|
|
693
703
|
const resolveIndexedSourceWatchRules = (): TIndexedSourceWatchRule[] => {
|
|
694
704
|
const transpileWatchRoots = app.transpileModuleDirectories
|
|
695
|
-
.
|
|
696
|
-
try {
|
|
697
|
-
return fs.realpathSync(rootPath);
|
|
698
|
-
} catch {
|
|
699
|
-
return rootPath;
|
|
700
|
-
}
|
|
701
|
-
})
|
|
702
|
-
.map(normalizeWatchPath)
|
|
705
|
+
.flatMap((rootPath) => resolveWatchPathAliases(rootPath))
|
|
703
706
|
.filter((rootPath, index, list) => list.indexOf(rootPath) === index);
|
|
704
707
|
|
|
705
708
|
return [
|
|
@@ -723,7 +726,7 @@ const resolveIndexedSourceWatchRules = (): TIndexedSourceWatchRule[] => {
|
|
|
723
726
|
rootPath,
|
|
724
727
|
relativePathPattern: transpileSourceWatchPattern,
|
|
725
728
|
eventTypes: ['change', 'rename'],
|
|
726
|
-
invalidateTargets: ['
|
|
729
|
+
invalidateTargets: ['all'],
|
|
727
730
|
}),
|
|
728
731
|
),
|
|
729
732
|
];
|
|
@@ -848,7 +851,7 @@ const createIndexedSourceWatching = ({
|
|
|
848
851
|
if (!fs.existsSync(rootPath)) continue;
|
|
849
852
|
|
|
850
853
|
watchers.push(
|
|
851
|
-
|
|
854
|
+
watch(rootPath, { recursive: true }, (eventType, filename) => {
|
|
852
855
|
const relativePath = typeof filename === 'string' ? filename.replace(/\\/g, '/').replace(/^\.\//, '') : '';
|
|
853
856
|
const normalizedEventType: TIndexedSourceWatchEvent = eventType === 'change' ? 'change' : 'rename';
|
|
854
857
|
|
|
@@ -44,10 +44,12 @@ export default function createCommonConfig(
|
|
|
44
44
|
outputTarget: TCompileOutputTarget = mode === 'dev' ? 'dev' : 'bin',
|
|
45
45
|
): Configuration {
|
|
46
46
|
const dev = mode === 'dev';
|
|
47
|
-
const enableFilesystemCache = dev ? cli.args.cache !== false : cli.args.cache === true;
|
|
48
47
|
const transpileModuleDirectories = app.transpileModuleDirectories;
|
|
48
|
+
const hasTranspileModuleDirectories = transpileModuleDirectories.length > 0;
|
|
49
|
+
// Persistent cache can restore stale modules from mutable workspace packages even after manual invalidation.
|
|
50
|
+
const enableFilesystemCache = dev ? cli.args.cache !== false && !hasTranspileModuleDirectories : cli.args.cache === true;
|
|
49
51
|
const transpileModuleSnapshot =
|
|
50
|
-
dev &&
|
|
52
|
+
dev && hasTranspileModuleDirectories
|
|
51
53
|
? {
|
|
52
54
|
// Transpiled local packages can resolve through node_modules symlinks,
|
|
53
55
|
// but they still need live invalidation like mutable app sources in dev.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type TResolveRequestOptions = { preferApp: boolean };
|
|
2
|
+
|
|
3
|
+
type TResolveServerExternalRequestOptions = {
|
|
4
|
+
context?: string;
|
|
5
|
+
frameworkRoots: string[];
|
|
6
|
+
request: string;
|
|
7
|
+
resolveRequest: (request: string, options: TResolveRequestOptions) => string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const normalizeModulePath = (value?: string) => (value || '').replace(/\\/g, '/');
|
|
11
|
+
|
|
12
|
+
export const isFrameworkSourceContext = (context: string | undefined, frameworkRoots: string[]) => {
|
|
13
|
+
const normalizedContext = normalizeModulePath(context);
|
|
14
|
+
|
|
15
|
+
return frameworkRoots.some((rootPath) => {
|
|
16
|
+
const normalizedRootPath = normalizeModulePath(rootPath);
|
|
17
|
+
|
|
18
|
+
return normalizedContext === normalizedRootPath || normalizedContext.startsWith(normalizedRootPath + '/');
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const resolveServerExternalRequest = ({
|
|
23
|
+
context,
|
|
24
|
+
frameworkRoots,
|
|
25
|
+
request,
|
|
26
|
+
resolveRequest,
|
|
27
|
+
}: TResolveServerExternalRequestOptions) => {
|
|
28
|
+
try {
|
|
29
|
+
return resolveRequest(request, {
|
|
30
|
+
preferApp: !isFrameworkSourceContext(context, frameworkRoots),
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return request;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -10,6 +10,7 @@ import { type Configuration } from '@rspack/core';
|
|
|
10
10
|
import cli from '@cli';
|
|
11
11
|
import createCommonConfig, { TCompileMode, TCompileOutputTarget, regex } from '../common';
|
|
12
12
|
import { toRspackAliases } from '../common/rspackAliases';
|
|
13
|
+
import { resolveServerExternalRequest } from './externals';
|
|
13
14
|
|
|
14
15
|
// Type
|
|
15
16
|
import type { App } from '../../app';
|
|
@@ -158,7 +159,7 @@ export default function createCompiler(
|
|
|
158
159
|
'./client-manifest.json',
|
|
159
160
|
|
|
160
161
|
// node_modules
|
|
161
|
-
function ({ request }, callback) {
|
|
162
|
+
function ({ context, request }, callback) {
|
|
162
163
|
const shouldCompile =
|
|
163
164
|
request !== undefined &&
|
|
164
165
|
// Local files
|
|
@@ -177,8 +178,16 @@ export default function createCompiler(
|
|
|
177
178
|
//console.log('isNodeModule', request, isNodeModule);
|
|
178
179
|
|
|
179
180
|
if (!shouldCompile) {
|
|
180
|
-
//
|
|
181
|
-
|
|
181
|
+
// Resolve server externals from their source owner. Bare runtime requires from
|
|
182
|
+
// the dev output can otherwise hit the framework node_modules symlink first.
|
|
183
|
+
const resolvedRequest = resolveServerExternalRequest({
|
|
184
|
+
context,
|
|
185
|
+
frameworkRoots,
|
|
186
|
+
request,
|
|
187
|
+
resolveRequest: (externalRequest, options) => cli.paths.resolveRequest(externalRequest, options),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return callback(undefined, 'commonjs ' + resolvedRequest);
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
// Continue without externalizing the import
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.9",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -103,6 +103,21 @@ const waitForAssetContaining = async (appRoot, extension, marker, timeoutMs = 60
|
|
|
103
103
|
throw new Error(`Timed out waiting for ${extension} asset containing ${marker}.`);
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
const waitForBodyContaining = async (port, urlPath, marker, timeoutMs = 60000) => {
|
|
107
|
+
const deadline = Date.now() + timeoutMs;
|
|
108
|
+
|
|
109
|
+
while (Date.now() < deadline) {
|
|
110
|
+
try {
|
|
111
|
+
const { body } = await request(port, urlPath, { Accept: 'text/html' });
|
|
112
|
+
if (body.includes(marker)) return body;
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
await sleep(250);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new Error(`Timed out waiting for ${urlPath} body containing ${marker}.`);
|
|
119
|
+
};
|
|
120
|
+
|
|
106
121
|
const waitForSessionReady = async (sessionFile, child, getOutput, timeoutMs = 90000) => {
|
|
107
122
|
const deadline = Date.now() + timeoutMs;
|
|
108
123
|
|
|
@@ -199,8 +214,13 @@ const createSharedStyleSource = (marker) => `.shared-style-marker {
|
|
|
199
214
|
`;
|
|
200
215
|
|
|
201
216
|
const createFixture = (root, port, options = {}) => {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
217
|
+
const monorepoRootInstall = options.monorepoRootInstall === true;
|
|
218
|
+
const appRoot = monorepoRootInstall ? path.join(root, 'apps', 'app') : path.join(root, 'app');
|
|
219
|
+
const sharedRoot = monorepoRootInstall ? path.join(root, 'packages', 'shared') : path.join(root, 'shared');
|
|
220
|
+
const sharedDependency = monorepoRootInstall ? 'file:../../packages/shared' : 'file:../shared';
|
|
221
|
+
const sharedInstallRoot = monorepoRootInstall
|
|
222
|
+
? path.join(root, 'node_modules', '@test', 'shared')
|
|
223
|
+
: path.join(appRoot, 'node_modules', '@test', 'shared');
|
|
204
224
|
const cacheConfigSource = options.routerCache ? ` cache: ${options.routerCache},\n` : '';
|
|
205
225
|
|
|
206
226
|
fs.mkdirSync(path.join(appRoot, 'public'), { recursive: true });
|
|
@@ -217,7 +237,7 @@ const createFixture = (root, port, options = {}) => {
|
|
|
217
237
|
private: true,
|
|
218
238
|
version: '0.0.0',
|
|
219
239
|
dependencies: {
|
|
220
|
-
'@test/shared':
|
|
240
|
+
'@test/shared': sharedDependency,
|
|
221
241
|
proteum: `file:${coreRoot}`,
|
|
222
242
|
},
|
|
223
243
|
},
|
|
@@ -474,7 +494,7 @@ export default definePageRoute({
|
|
|
474
494
|
writeFile(path.join(sharedRoot, 'styles.css'), createSharedStyleSource('STYLE_MARKER_INITIAL'));
|
|
475
495
|
|
|
476
496
|
createSymlink(coreRoot, path.join(appRoot, 'node_modules', 'proteum'));
|
|
477
|
-
createSymlink(sharedRoot,
|
|
497
|
+
createSymlink(sharedRoot, sharedInstallRoot);
|
|
478
498
|
|
|
479
499
|
return {
|
|
480
500
|
appRoot,
|
|
@@ -500,11 +520,14 @@ const stopDevServer = async (child) => {
|
|
|
500
520
|
});
|
|
501
521
|
};
|
|
502
522
|
|
|
503
|
-
const startDevServer = (appRoot, port, sessionFile) => {
|
|
523
|
+
const startDevServer = (appRoot, port, sessionFile, options = {}) => {
|
|
504
524
|
let output = '';
|
|
525
|
+
const args = [cliBin, 'dev', '--cwd', appRoot, '--port', String(port), '--session-file', sessionFile];
|
|
526
|
+
if (options.noCache !== false) args.push('--no-cache');
|
|
527
|
+
args.push('--verbose');
|
|
505
528
|
const child = spawn(
|
|
506
529
|
process.execPath,
|
|
507
|
-
|
|
530
|
+
args,
|
|
508
531
|
{
|
|
509
532
|
cwd: appRoot,
|
|
510
533
|
env: {
|
|
@@ -577,6 +600,41 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
577
600
|
}
|
|
578
601
|
});
|
|
579
602
|
|
|
603
|
+
test(
|
|
604
|
+
'proteum dev invalidates SSR and client assets for monorepo-root transpiled package installs',
|
|
605
|
+
{ timeout: 180000 },
|
|
606
|
+
async () => {
|
|
607
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-monorepo-transpile-watch-'));
|
|
608
|
+
const port = await resolvePortPair();
|
|
609
|
+
const { appRoot, sharedRoot } = createFixture(root, port, { monorepoRootInstall: true });
|
|
610
|
+
const sessionFile = path.join(appRoot, 'var', 'run', 'proteum', 'dev', 'monorepo-transpile-watch-test.json');
|
|
611
|
+
const { child, getOutput } = startDevServer(appRoot, port, sessionFile, { noCache: false });
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
await waitForSessionReady(sessionFile, child, getOutput);
|
|
615
|
+
await waitForBodyContaining(port, '/', 'SCRIPT_MARKER_INITIAL').catch((error) => {
|
|
616
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
617
|
+
});
|
|
618
|
+
await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch((error) => {
|
|
619
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const reloadStream = await connectToReloadStream(port + 1);
|
|
623
|
+
writeFile(path.join(sharedRoot, 'index.tsx'), createSharedIndexSource('SCRIPT_MARKER_MONOREPO_UPDATED'));
|
|
624
|
+
|
|
625
|
+
await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_MONOREPO_UPDATED');
|
|
626
|
+
await waitForBodyContaining(port, '/', 'SCRIPT_MARKER_MONOREPO_UPDATED');
|
|
627
|
+
const reloadEvent = await reloadStream.waitForReload();
|
|
628
|
+
reloadStream.close();
|
|
629
|
+
|
|
630
|
+
assert.equal(reloadEvent.type, 'reload');
|
|
631
|
+
} finally {
|
|
632
|
+
await stopDevServer(child);
|
|
633
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
|
|
580
638
|
test('proteum dev applies router HTTP cache config to HTML and public assets', { timeout: 180000 }, async () => {
|
|
581
639
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-router-cache-'));
|
|
582
640
|
const port = await resolvePortPair();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
5
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
6
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
7
|
+
|
|
8
|
+
require('ts-node/register/transpile-only');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
isFrameworkSourceContext,
|
|
12
|
+
resolveServerExternalRequest,
|
|
13
|
+
} = require('../cli/compiler/server/externals.ts');
|
|
14
|
+
|
|
15
|
+
test('server external resolution prefers app dependencies outside framework source', () => {
|
|
16
|
+
const optionsSeen = [];
|
|
17
|
+
const resolved = resolveServerExternalRequest({
|
|
18
|
+
context: '/repo/node_modules/@klair/usecases/src/mcp/groups/workers',
|
|
19
|
+
frameworkRoots: ['/framework/core'],
|
|
20
|
+
request: '@prisma/client',
|
|
21
|
+
resolveRequest: (request, options) => {
|
|
22
|
+
optionsSeen.push(options);
|
|
23
|
+
assert.equal(request, '@prisma/client');
|
|
24
|
+
return options.preferApp
|
|
25
|
+
? '/repo/node_modules/@prisma/client/default.js'
|
|
26
|
+
: '/framework/core/node_modules/@prisma/client/default.js';
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.equal(resolved, '/repo/node_modules/@prisma/client/default.js');
|
|
31
|
+
assert.deepEqual(optionsSeen, [{ preferApp: true }]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('server external resolution prefers framework dependencies for framework source', () => {
|
|
35
|
+
const resolved = resolveServerExternalRequest({
|
|
36
|
+
context: '/framework/core/server',
|
|
37
|
+
frameworkRoots: ['/framework/core'],
|
|
38
|
+
request: 'express',
|
|
39
|
+
resolveRequest: (request, options) => {
|
|
40
|
+
assert.equal(request, 'express');
|
|
41
|
+
return options.preferApp
|
|
42
|
+
? '/repo/node_modules/express/index.js'
|
|
43
|
+
: '/framework/core/node_modules/express/index.js';
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(resolved, '/framework/core/node_modules/express/index.js');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('server external resolution falls back to the bare request when resolution fails', () => {
|
|
51
|
+
const resolved = resolveServerExternalRequest({
|
|
52
|
+
context: '/repo/server',
|
|
53
|
+
frameworkRoots: ['/framework/core'],
|
|
54
|
+
request: 'optional-peer',
|
|
55
|
+
resolveRequest: () => {
|
|
56
|
+
throw new Error('missing');
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.equal(resolved, 'optional-peer');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('framework source context matching handles exact and nested roots', () => {
|
|
64
|
+
assert.equal(isFrameworkSourceContext('/framework/core', ['/framework/core']), true);
|
|
65
|
+
assert.equal(isFrameworkSourceContext('/framework/core/server', ['/framework/core']), true);
|
|
66
|
+
assert.equal(isFrameworkSourceContext('/repo/node_modules/@klair/usecases', ['/framework/core']), false);
|
|
67
|
+
});
|