proteum 2.5.8 → 2.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/LICENSE +21 -0
- package/README.md +208 -594
- package/agents/project/AGENTS.md +131 -91
- package/agents/project/CODING_STYLE.md +42 -40
- package/agents/project/client/AGENTS.md +0 -1
- package/agents/project/diagnostics.md +6 -5
- package/agents/project/optimizations.md +1 -7
- package/agents/project/server/services/AGENTS.md +1 -3
- package/agents/project/tests/AGENTS.md +3 -3
- package/cli/commands/session.ts +36 -5
- package/cli/commands/verify.ts +6 -1
- package/cli/compiler/client/index.ts +11 -4
- package/cli/compiler/common/uiSingletons.ts +76 -0
- package/cli/compiler/server/externals.ts +35 -0
- package/cli/compiler/server/index.ts +46 -15
- package/cli/presentation/commands.ts +6 -1
- package/cli/runtime/commands.ts +2 -0
- package/cli/scaffold/index.ts +3 -0
- package/cli/scaffold/templates.ts +62 -6
- package/cli/utils/agents.ts +2 -2
- package/client/dev/profiler/index.tsx +761 -455
- package/common/dev/session.ts +32 -0
- package/common/errors/index.tsx +0 -1
- package/docs/agent-routing.md +2 -2
- package/package.json +1 -1
- package/server/app/container/console/index.ts +0 -17
- package/server/services/router/http/index.ts +130 -33
- package/tests/agents-utils.test.cjs +0 -4
- package/tests/dev-session-login-url.test.cjs +33 -0
- package/tests/server-externals.test.cjs +67 -0
- package/tests/ui-singletons.test.cjs +104 -0
- package/agents/project/app-root/AGENTS.md +0 -14
- package/agents/project/root/AGENTS.md +0 -399
package/common/dev/session.ts
CHANGED
|
@@ -22,3 +22,35 @@ export type TDevSessionStartResponse = {
|
|
|
22
22
|
export type TDevSessionErrorResponse = {
|
|
23
23
|
error: string;
|
|
24
24
|
};
|
|
25
|
+
|
|
26
|
+
export const devSessionLoginPath = '/__proteum/session/login';
|
|
27
|
+
export const devSessionStartPath = '/__proteum/session/start';
|
|
28
|
+
|
|
29
|
+
export const normalizeDevSessionRedirectPath = (value: string): string => {
|
|
30
|
+
const redirect = value.trim() || '/';
|
|
31
|
+
if (!redirect.startsWith('/') || redirect.startsWith('//') || redirect.startsWith('/\\') || /[\r\n]/.test(redirect)) {
|
|
32
|
+
throw new Error('Redirect must be a local absolute path such as /dashboard.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return redirect;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const buildDevSessionLoginUrl = ({
|
|
39
|
+
baseUrl,
|
|
40
|
+
email,
|
|
41
|
+
redirect,
|
|
42
|
+
role,
|
|
43
|
+
}: {
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
email: string;
|
|
46
|
+
redirect: string;
|
|
47
|
+
role?: string;
|
|
48
|
+
}): string => {
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
email,
|
|
51
|
+
redirect: normalizeDevSessionRedirectPath(redirect),
|
|
52
|
+
});
|
|
53
|
+
if (role?.trim()) params.set('role', role.trim());
|
|
54
|
+
|
|
55
|
+
return `${baseUrl.replace(/\/+$/, '')}${devSessionLoginPath}?${params.toString()}`;
|
|
56
|
+
};
|
package/common/errors/index.tsx
CHANGED
package/docs/agent-routing.md
CHANGED
|
@@ -96,11 +96,11 @@ Standard triggered reads:
|
|
|
96
96
|
- Worktree Preflight (`cwd` inside `/.codex/worktrees/`, newly created Proteum worktree, or before editing in a Codex worktree): root contract fallback, then `npx proteum worktree init --source <source-app-root>` or the returned `--refresh` command, `npx proteum runtime status`, and tracked `npx proteum dev` for runtime-visible work.
|
|
97
97
|
- Git lifecycle (`commit`, `and commit`, `stage`, `push`, `PR`, pull request): root contract fallback.
|
|
98
98
|
- Before git writes after a bug fix, behavior change, decision change, or docs-relevant production change: `DOCUMENTATION.md`.
|
|
99
|
-
- Before finishing production code changes: root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, and touched area `AGENTS.md
|
|
99
|
+
- Before finishing production code changes: root contract fallback, `DOCUMENTATION.md`, `CODING_STYLE.md`, and touched area `AGENTS.md`, then the `CODING_STYLE.md` self-check on the final diff; a non-obvious decision without a why-comment is a defect.
|
|
100
100
|
- Runtime-visible, request-time, router, SSR, browser, or controller behavior: root contract fallback plus `diagnostics.md`.
|
|
101
101
|
- Bug fixes, regressions, incidents, broken public routes, auth/OAuth failures, integration failures, or production behavior fixes: `DOCUMENTATION.md`.
|
|
102
102
|
- Non-trivial feature, product, business-rule, UX, copy, or docs changes: `DOCUMENTATION.md`.
|
|
103
|
-
- Implementation edits: `CODING_STYLE.md` plus the matching area file from the routing table.
|
|
103
|
+
- Implementation edits: `CODING_STYLE.md` plus the matching area file from the routing table; record non-obvious decisions, workarounds, and constraints as why-comments while editing.
|
|
104
104
|
|
|
105
105
|
`workflow_start`, `orient`, `route_candidates`, and MCP `instructions_resolve` should promote obvious triggered files into selected instruction previews; ambiguous conditional reads can remain in `readWhen`.
|
|
106
106
|
|
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.10",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { serialize } from 'v8';
|
|
7
7
|
import { formatWithOptions } from 'util';
|
|
8
8
|
import md5 from 'md5';
|
|
9
|
-
import dayjs from 'dayjs';
|
|
10
9
|
import stringify from 'fast-safe-stringify';
|
|
11
10
|
|
|
12
11
|
// Npm
|
|
@@ -178,7 +177,6 @@ export default class Console {
|
|
|
178
177
|
public logger!: Logger<ILogObj>;
|
|
179
178
|
// Buffers
|
|
180
179
|
public logs: TJsonLog[] = [];
|
|
181
|
-
private reported: { [hash: string]: { times: number; last: Date } } = {};
|
|
182
180
|
|
|
183
181
|
/*----------------------------------
|
|
184
182
|
- LIFECYCLE
|
|
@@ -371,24 +369,9 @@ export default class Console {
|
|
|
371
369
|
// Genertae unique error hash
|
|
372
370
|
const hash = md5(inspection.stacktraces[0]);
|
|
373
371
|
|
|
374
|
-
// Don't send the same error twice in a row (avoid email spamming)
|
|
375
|
-
const lastReport = this.reported[hash];
|
|
376
|
-
let isDuplicate = false;
|
|
377
|
-
if (lastReport === undefined) {
|
|
378
|
-
this.reported[hash] = { times: 0, last: new Date() };
|
|
379
|
-
|
|
380
|
-
// If error older than 1 day
|
|
381
|
-
} else if (dayjs(now).diff(dayjs(lastReport.last), 'day') > 1) {
|
|
382
|
-
lastReport.times++;
|
|
383
|
-
lastReport.last = now;
|
|
384
|
-
} else {
|
|
385
|
-
isDuplicate = true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
372
|
const bugReport: ServerBug = {
|
|
389
373
|
// Context
|
|
390
374
|
hash: hash,
|
|
391
|
-
isDuplicate,
|
|
392
375
|
date: now,
|
|
393
376
|
channelType,
|
|
394
377
|
channelId,
|
|
@@ -30,7 +30,13 @@ import type { TServerRouter } from '..';
|
|
|
30
30
|
import type { TDevConsoleLogLevel } from '@common/dev/console';
|
|
31
31
|
import type { TPerfGroupBy } from '@common/dev/performance';
|
|
32
32
|
import type { TServerReadyConnectedProject } from '@common/dev/serverHotReload';
|
|
33
|
-
import
|
|
33
|
+
import {
|
|
34
|
+
devSessionLoginPath,
|
|
35
|
+
devSessionStartPath,
|
|
36
|
+
normalizeDevSessionRedirectPath,
|
|
37
|
+
type TDevSessionStartResponse,
|
|
38
|
+
type TDevSessionUserSummary,
|
|
39
|
+
} from '@common/dev/session';
|
|
34
40
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
35
41
|
import { explainSectionNames } from '@common/dev/diagnostics';
|
|
36
42
|
import {
|
|
@@ -88,6 +94,16 @@ type TDevSessionAuthService = {
|
|
|
88
94
|
createSession: (session: { email: string }, request: { id: string; res: express.Response }) => string;
|
|
89
95
|
decodeSession: (session: { email: string }, req: express.Request) => Promise<TBasicUser | null>;
|
|
90
96
|
};
|
|
97
|
+
type TDevSessionResult =
|
|
98
|
+
| {
|
|
99
|
+
ok: true;
|
|
100
|
+
response: TDevSessionStartResponse;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
ok: false;
|
|
104
|
+
error: string;
|
|
105
|
+
statusCode: number;
|
|
106
|
+
};
|
|
91
107
|
|
|
92
108
|
const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
|
|
93
109
|
const directives: TContentSecurityPolicyDirectives = {
|
|
@@ -177,6 +193,77 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
177
193
|
};
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
private getSingleQueryValue(value: express.Request['query'][string]): string {
|
|
197
|
+
if (typeof value === 'string') return value.trim();
|
|
198
|
+
if (Array.isArray(value) && value.length === 1 && typeof value[0] === 'string') return value[0].trim();
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private normalizeDevSessionRedirect(value: express.Request['query'][string]): string {
|
|
203
|
+
try {
|
|
204
|
+
return normalizeDevSessionRedirectPath(this.getSingleQueryValue(value));
|
|
205
|
+
} catch {
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private isLocalDevSessionRequest(req: express.Request): boolean {
|
|
211
|
+
const hostname = req.hostname.toLowerCase();
|
|
212
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async createDevSessionResponse({
|
|
216
|
+
email,
|
|
217
|
+
req,
|
|
218
|
+
requiredRole,
|
|
219
|
+
res,
|
|
220
|
+
source,
|
|
221
|
+
}: {
|
|
222
|
+
email: string;
|
|
223
|
+
req: express.Request;
|
|
224
|
+
requiredRole: string;
|
|
225
|
+
res: express.Response;
|
|
226
|
+
source: 'login' | 'start';
|
|
227
|
+
}): Promise<TDevSessionResult> {
|
|
228
|
+
if (!email) return { ok: false, statusCode: 400, error: 'Email is required.' };
|
|
229
|
+
|
|
230
|
+
const auth = this.resolveDevSessionAuthService();
|
|
231
|
+
const user = await auth.decodeSession({ email }, req);
|
|
232
|
+
|
|
233
|
+
if (!user) return { ok: false, statusCode: 404, error: `No user could be resolved for "${email}".` };
|
|
234
|
+
if (requiredRole && !user.roles.includes(requiredRole)) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
statusCode: 403,
|
|
238
|
+
error: `User "${email}" does not have required role "${requiredRole}".`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const token = auth.createSession(
|
|
243
|
+
{ email },
|
|
244
|
+
{
|
|
245
|
+
id: `proteum-session-${source}:${Date.now()}`,
|
|
246
|
+
res,
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
const issuedAt = new Date().toISOString();
|
|
250
|
+
const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
response: {
|
|
255
|
+
user: this.summarizeDevSessionUser(user),
|
|
256
|
+
session: {
|
|
257
|
+
token,
|
|
258
|
+
cookieName: 'authorization',
|
|
259
|
+
expiresInMs: auth.config.jwt.expiration,
|
|
260
|
+
issuedAt,
|
|
261
|
+
expiresAt,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
180
267
|
private async verifyConnectedProjectsBeforeStart(): Promise<TServerReadyConnectedProject[]> {
|
|
181
268
|
const verifiedConnectedProjects: TServerReadyConnectedProject[] = [];
|
|
182
269
|
|
|
@@ -766,50 +853,60 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
766
853
|
}
|
|
767
854
|
});
|
|
768
855
|
|
|
769
|
-
routes.
|
|
770
|
-
|
|
771
|
-
|
|
856
|
+
routes.get(devSessionLoginPath, async (req, res) => {
|
|
857
|
+
if (!this.isLocalDevSessionRequest(req)) {
|
|
858
|
+
res.status(403).send('Proteum session login is only available from localhost.');
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
772
861
|
|
|
773
|
-
|
|
774
|
-
|
|
862
|
+
const email = this.getSingleQueryValue(req.query.email);
|
|
863
|
+
const requiredRole = this.getSingleQueryValue(req.query.role);
|
|
864
|
+
const redirect = this.normalizeDevSessionRedirect(req.query.redirect);
|
|
865
|
+
|
|
866
|
+
if (!redirect) {
|
|
867
|
+
res.status(400).send('Redirect must be a local absolute path.');
|
|
775
868
|
return;
|
|
776
869
|
}
|
|
777
870
|
|
|
778
871
|
try {
|
|
779
|
-
const
|
|
780
|
-
|
|
872
|
+
const result = await this.createDevSessionResponse({
|
|
873
|
+
email,
|
|
874
|
+
req,
|
|
875
|
+
requiredRole,
|
|
876
|
+
res,
|
|
877
|
+
source: 'login',
|
|
878
|
+
});
|
|
781
879
|
|
|
782
|
-
if (!
|
|
783
|
-
res.status(
|
|
880
|
+
if (!result.ok) {
|
|
881
|
+
res.status(result.statusCode).send(result.error);
|
|
784
882
|
return;
|
|
785
883
|
}
|
|
786
884
|
|
|
787
|
-
|
|
788
|
-
|
|
885
|
+
res.redirect(302, redirect);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
res.status(500).send(error instanceof Error ? error.message : String(error));
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
routes.post(devSessionStartPath, async (req, res) => {
|
|
892
|
+
const email = typeof req.body?.email === 'string' ? req.body.email.trim() : '';
|
|
893
|
+
const requiredRole = typeof req.body?.role === 'string' ? req.body.role.trim() : '';
|
|
894
|
+
|
|
895
|
+
try {
|
|
896
|
+
const result = await this.createDevSessionResponse({
|
|
897
|
+
email,
|
|
898
|
+
req,
|
|
899
|
+
requiredRole,
|
|
900
|
+
res,
|
|
901
|
+
source: 'start',
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
if (!result.ok) {
|
|
905
|
+
res.status(result.statusCode).json({ error: result.error });
|
|
789
906
|
return;
|
|
790
907
|
}
|
|
791
908
|
|
|
792
|
-
|
|
793
|
-
{ email },
|
|
794
|
-
{
|
|
795
|
-
id: `proteum-session:${Date.now()}`,
|
|
796
|
-
res,
|
|
797
|
-
},
|
|
798
|
-
);
|
|
799
|
-
const issuedAt = new Date().toISOString();
|
|
800
|
-
const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
|
|
801
|
-
const response: TDevSessionStartResponse = {
|
|
802
|
-
user: this.summarizeDevSessionUser(user),
|
|
803
|
-
session: {
|
|
804
|
-
token,
|
|
805
|
-
cookieName: 'authorization',
|
|
806
|
-
expiresInMs: auth.config.jwt.expiration,
|
|
807
|
-
issuedAt,
|
|
808
|
-
expiresAt,
|
|
809
|
-
},
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
res.json(response);
|
|
909
|
+
res.json(result.response);
|
|
813
910
|
} catch (error) {
|
|
814
911
|
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
815
912
|
}
|
|
@@ -108,10 +108,6 @@ test('project instruction sources require unit tests for applicable production c
|
|
|
108
108
|
fs.readFileSync(path.join(projectAgentsRoot, 'AGENTS.md'), 'utf8'),
|
|
109
109
|
/production changes must always add or update focused unit tests/,
|
|
110
110
|
);
|
|
111
|
-
assert.match(
|
|
112
|
-
fs.readFileSync(path.join(projectAgentsRoot, 'root', 'AGENTS.md'), 'utf8'),
|
|
113
|
-
/always add or update focused unit tests/,
|
|
114
|
-
);
|
|
115
111
|
assert.match(
|
|
116
112
|
fs.readFileSync(path.join(projectAgentsRoot, 'tests', 'AGENTS.md'), 'utf8'),
|
|
117
113
|
/For every production change, add or update focused unit tests/,
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
require('ts-node/register/transpile-only');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
buildDevSessionLoginUrl,
|
|
11
|
+
normalizeDevSessionRedirectPath,
|
|
12
|
+
} = require('../common/dev/session.ts');
|
|
13
|
+
|
|
14
|
+
test('buildDevSessionLoginUrl creates a local browser handoff URL', () => {
|
|
15
|
+
const url = buildDevSessionLoginUrl({
|
|
16
|
+
baseUrl: 'http://127.0.0.1:3020/',
|
|
17
|
+
email: 'contact@gaetan-legac.fr',
|
|
18
|
+
redirect: '/projects/project-123?tab=radar',
|
|
19
|
+
role: 'GOD',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
assert.equal(
|
|
23
|
+
url,
|
|
24
|
+
'http://127.0.0.1:3020/__proteum/session/login?email=contact%40gaetan-legac.fr&redirect=%2Fprojects%2Fproject-123%3Ftab%3Dradar&role=GOD',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('normalizeDevSessionRedirectPath rejects external redirects', () => {
|
|
29
|
+
assert.throws(() => normalizeDevSessionRedirectPath('https://example.com'), /local absolute path/);
|
|
30
|
+
assert.throws(() => normalizeDevSessionRedirectPath('//example.com'), /local absolute path/);
|
|
31
|
+
assert.throws(() => normalizeDevSessionRedirectPath('/\nadmin'), /local absolute path/);
|
|
32
|
+
assert.equal(normalizeDevSessionRedirectPath(''), '/');
|
|
33
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
isUiSingletonRequest,
|
|
12
|
+
resolveUiSingletonAliases,
|
|
13
|
+
resolveUiSingletonServerExternalRequest,
|
|
14
|
+
} = require('../cli/compiler/common/uiSingletons.ts');
|
|
15
|
+
|
|
16
|
+
test('UI singleton matcher includes React and Preact runtime packages', () => {
|
|
17
|
+
assert.equal(isUiSingletonRequest('preact'), true);
|
|
18
|
+
assert.equal(isUiSingletonRequest('preact/hooks'), true);
|
|
19
|
+
assert.equal(isUiSingletonRequest('preact-render-to-string'), true);
|
|
20
|
+
assert.equal(isUiSingletonRequest('react'), true);
|
|
21
|
+
assert.equal(isUiSingletonRequest('react-dom/client'), true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('UI singleton matcher does not match unrelated React-prefixed packages', () => {
|
|
25
|
+
assert.equal(isUiSingletonRequest('react-number-format'), false);
|
|
26
|
+
assert.equal(isUiSingletonRequest('reactive-stream'), false);
|
|
27
|
+
assert.equal(isUiSingletonRequest('@radix-ui/react-popover'), false);
|
|
28
|
+
assert.equal(isUiSingletonRequest(undefined), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('UI singleton aliases resolve React imports to Preact runtime requests', () => {
|
|
32
|
+
const seenRequests = [];
|
|
33
|
+
const seenPackageRoots = [];
|
|
34
|
+
const aliases = resolveUiSingletonAliases({
|
|
35
|
+
resolvePackageRoot: (packageName) => {
|
|
36
|
+
seenPackageRoots.push(packageName);
|
|
37
|
+
return `/app/node_modules/${packageName}`;
|
|
38
|
+
},
|
|
39
|
+
resolveRequest: (request) => {
|
|
40
|
+
seenRequests.push(request);
|
|
41
|
+
return `/app/node_modules/${request}`;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.equal(aliases['preact'], '/app/node_modules/preact');
|
|
46
|
+
assert.equal(aliases['preact$'], '/app/node_modules/preact');
|
|
47
|
+
assert.equal(aliases['preact/hooks$'], '/app/node_modules/preact/hooks');
|
|
48
|
+
assert.equal(aliases['react$'], '/app/node_modules/preact/compat');
|
|
49
|
+
assert.equal(aliases['react-dom$'], '/app/node_modules/preact/compat');
|
|
50
|
+
assert.equal(aliases['react-dom/client$'], '/app/node_modules/preact/compat/client');
|
|
51
|
+
assert.equal(aliases['react/jsx-runtime$'], '/app/node_modules/preact/jsx-runtime');
|
|
52
|
+
assert.equal(aliases['preact-render-to-string'], '/app/node_modules/preact-render-to-string');
|
|
53
|
+
assert.equal(aliases['preact-render-to-string$'], '/app/node_modules/preact-render-to-string');
|
|
54
|
+
assert.deepEqual(seenPackageRoots, ['preact', 'preact-render-to-string']);
|
|
55
|
+
assert.equal(
|
|
56
|
+
Object.keys(aliases).indexOf('preact/jsx-dev-runtime$') < Object.keys(aliases).indexOf('preact'),
|
|
57
|
+
true,
|
|
58
|
+
);
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
seenRequests.filter((request) => request === 'preact-render-to-string'),
|
|
61
|
+
['preact-render-to-string'],
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('UI singleton server externals resolve Preact and React compat requests from the app', () => {
|
|
66
|
+
const seenRequests = [];
|
|
67
|
+
const resolveRequest = (request) => {
|
|
68
|
+
seenRequests.push(request);
|
|
69
|
+
return `/app/node_modules/${request}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
assert.equal(resolveUiSingletonServerExternalRequest('preact', resolveRequest), '/app/node_modules/preact');
|
|
73
|
+
assert.equal(
|
|
74
|
+
resolveUiSingletonServerExternalRequest('preact/hooks', resolveRequest),
|
|
75
|
+
'/app/node_modules/preact/hooks',
|
|
76
|
+
);
|
|
77
|
+
assert.equal(
|
|
78
|
+
resolveUiSingletonServerExternalRequest('react', resolveRequest),
|
|
79
|
+
'/app/node_modules/preact/compat',
|
|
80
|
+
);
|
|
81
|
+
assert.equal(
|
|
82
|
+
resolveUiSingletonServerExternalRequest('react/jsx-runtime', resolveRequest),
|
|
83
|
+
'/app/node_modules/preact/jsx-runtime',
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
resolveUiSingletonServerExternalRequest('react-dom/client', resolveRequest),
|
|
87
|
+
'/app/node_modules/preact/compat/client',
|
|
88
|
+
);
|
|
89
|
+
assert.deepEqual(seenRequests, [
|
|
90
|
+
'preact',
|
|
91
|
+
'preact/hooks',
|
|
92
|
+
'preact/compat',
|
|
93
|
+
'preact/jsx-runtime',
|
|
94
|
+
'preact/compat/client',
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('UI singleton server externals keep the SSR renderer compiled', () => {
|
|
99
|
+
const resolveRequest = () => {
|
|
100
|
+
throw new Error('should not resolve');
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
assert.equal(resolveUiSingletonServerExternalRequest('preact-render-to-string', resolveRequest), undefined);
|
|
104
|
+
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Proteum App-Root Addendum
|
|
2
|
-
|
|
3
|
-
This file is the app-root-only addendum for a Proteum app that lives inside a larger monorepo.
|
|
4
|
-
Keep the reusable Proteum contract in the nearest ancestor root `AGENTS.md`, and keep this file only for instructions that depend on the current directory being the Proteum app root.
|
|
5
|
-
Role: keep only app-root workflow and local project-semantic rules here.
|
|
6
|
-
Do not put here: reusable Proteum architecture contracts, shared verification rules, documentation-driven coding workflow, diagnostics workflow, optimization checklists, coding-style details, or area-specific rules already covered by broader `AGENTS.md` files or the root-level `DOCUMENTATION.md`, `diagnostics.md`, `optimizations.md`, and `CODING_STYLE.md`.
|
|
7
|
-
|
|
8
|
-
## App-Root Triggers
|
|
9
|
-
|
|
10
|
-
- If you are working in a newly created Proteum worktree, before following the rest of these instructions:
|
|
11
|
-
- Run `npx proteum worktree init --source <source-app-root>`.
|
|
12
|
-
- Read and acknowledge the applicable `AGENTS.md` files.
|
|
13
|
-
- Run the dev server with the task-safe elevated-permissions launch workflow from the reusable root `AGENTS.md`, keep it running so user can see the results by himself, and print the live server URL as a clickable Markdown link. If `proteum dev` reports blocked instruction paths, resolve them before continuing.
|
|
14
|
-
- If the task changes UX, copy, onboarding, pricing, product semantics, or commercial positioning, use root-level `DOCUMENTATION.md` to choose the smallest relevant `./docs/` pack before editing. If a dev server is already running, print the live dev server URL as a clickable Markdown link.
|