@c9up/ream 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/HttpKernel.d.ts +5 -1
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +6 -6
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +3 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +94 -14
- package/dist/Ignitor.js.map +1 -1
- package/dist/console/prompts.d.ts.map +1 -1
- package/dist/console/prompts.js +34 -46
- package/dist/console/prompts.js.map +1 -1
- package/dist/console/selection.d.ts +54 -0
- package/dist/console/selection.d.ts.map +1 -0
- package/dist/console/selection.js +235 -0
- package/dist/console/selection.js.map +1 -0
- package/dist/dev/fullReload.d.ts +35 -10
- package/dist/dev/fullReload.d.ts.map +1 -1
- package/dist/dev/fullReload.js +55 -14
- package/dist/dev/fullReload.js.map +1 -1
- package/dist/dev/hotWatcher.d.ts +19 -3
- package/dist/dev/hotWatcher.d.ts.map +1 -1
- package/dist/dev/hotWatcher.js +168 -18
- package/dist/dev/hotWatcher.js.map +1 -1
- package/dist/dev/readyBanner.d.ts +45 -0
- package/dist/dev/readyBanner.d.ts.map +1 -0
- package/dist/dev/readyBanner.js +72 -0
- package/dist/dev/readyBanner.js.map +1 -0
- package/dist/dev/shortcuts.d.ts +41 -0
- package/dist/dev/shortcuts.d.ts.map +1 -0
- package/dist/dev/shortcuts.js +142 -0
- package/dist/dev/shortcuts.js.map +1 -0
- package/dist/errors/prettyPrintError.d.ts.map +1 -1
- package/dist/errors/prettyPrintError.js +5 -8
- package/dist/errors/prettyPrintError.js.map +1 -1
- package/dist/errors/renderError.d.ts +46 -0
- package/dist/errors/renderError.d.ts.map +1 -0
- package/dist/errors/renderError.js +197 -0
- package/dist/errors/renderError.js.map +1 -0
- package/dist/hot.js +12 -2
- package/dist/hot.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/native/generated.d.ts +34 -0
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/profiler/MemoryProfileStore.d.ts +23 -0
- package/dist/profiler/MemoryProfileStore.d.ts.map +1 -0
- package/dist/profiler/MemoryProfileStore.js +70 -0
- package/dist/profiler/MemoryProfileStore.js.map +1 -0
- package/dist/profiler/Profiler.d.ts +56 -0
- package/dist/profiler/Profiler.d.ts.map +1 -0
- package/dist/profiler/Profiler.js +162 -0
- package/dist/profiler/Profiler.js.map +1 -0
- package/dist/profiler/collectors.d.ts +105 -0
- package/dist/profiler/collectors.d.ts.map +1 -0
- package/dist/profiler/collectors.js +164 -0
- package/dist/profiler/collectors.js.map +1 -0
- package/dist/profiler/endpoint.d.ts +27 -0
- package/dist/profiler/endpoint.d.ts.map +1 -0
- package/dist/profiler/endpoint.js +79 -0
- package/dist/profiler/endpoint.js.map +1 -0
- package/dist/profiler/index.d.ts +26 -0
- package/dist/profiler/index.d.ts.map +1 -0
- package/dist/profiler/index.js +41 -0
- package/dist/profiler/index.js.map +1 -0
- package/dist/profiler/middleware.d.ts +17 -0
- package/dist/profiler/middleware.d.ts.map +1 -0
- package/dist/profiler/middleware.js +79 -0
- package/dist/profiler/middleware.js.map +1 -0
- package/dist/profiler/render.d.ts +23 -0
- package/dist/profiler/render.d.ts.map +1 -0
- package/dist/profiler/render.js +141 -0
- package/dist/profiler/render.js.map +1 -0
- package/dist/profiler/toolbar.d.ts +34 -0
- package/dist/profiler/toolbar.d.ts.map +1 -0
- package/dist/profiler/toolbar.js +104 -0
- package/dist/profiler/toolbar.js.map +1 -0
- package/dist/profiler/types.d.ts +70 -0
- package/dist/profiler/types.d.ts.map +1 -0
- package/dist/profiler/types.js +11 -0
- package/dist/profiler/types.js.map +1 -0
- package/dist/router/Router.d.ts +11 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +16 -0
- package/dist/router/Router.js.map +1 -1
- package/dist/server/Server.d.ts +37 -0
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +30 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/session/Session.d.ts +33 -0
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +31 -2
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +24 -4
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/CookieDriver.d.ts +12 -3
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +20 -6
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +2 -0
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +6 -0
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +8 -0
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +85 -11
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +2 -0
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +9 -0
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +2 -0
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +6 -0
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/session/errors.d.ts +10 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +12 -0
- package/dist/session/errors.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/HttpKernel.ts +12 -8
- package/src/Ignitor.ts +109 -15
- package/src/console/prompts.ts +45 -64
- package/src/console/selection.ts +274 -0
- package/src/dev/fullReload.ts +57 -15
- package/src/dev/hotWatcher.ts +165 -21
- package/src/dev/readyBanner.ts +84 -0
- package/src/dev/shortcuts.ts +158 -0
- package/src/errors/prettyPrintError.ts +5 -5
- package/src/errors/renderError.ts +230 -0
- package/src/hot.ts +12 -2
- package/src/index.ts +11 -1
- package/src/native/generated.ts +38 -0
- package/src/profiler/MemoryProfileStore.ts +75 -0
- package/src/profiler/Profiler.ts +182 -0
- package/src/profiler/collectors.ts +230 -0
- package/src/profiler/endpoint.ts +98 -0
- package/src/profiler/index.ts +56 -0
- package/src/profiler/middleware.ts +83 -0
- package/src/profiler/render.ts +173 -0
- package/src/profiler/toolbar.ts +106 -0
- package/src/profiler/types.ts +75 -0
- package/src/router/Router.ts +16 -0
- package/src/server/Server.ts +49 -0
- package/src/session/Session.ts +56 -2
- package/src/session/SessionMiddleware.ts +23 -4
- package/src/session/drivers/CookieDriver.ts +21 -6
- package/src/session/drivers/DatabaseDriver.ts +10 -0
- package/src/session/drivers/FileDriver.ts +91 -11
- package/src/session/drivers/MemoryDriver.ts +9 -0
- package/src/session/drivers/RedisDriver.ts +7 -0
- package/src/session/errors.ts +17 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profiler, assembled.
|
|
3
|
+
*
|
|
4
|
+
* One place that knows which collectors ship, so the Ignitor wires a profiler
|
|
5
|
+
* rather than a list — and an application that wants its own panel adds a
|
|
6
|
+
* collector here instead of rebuilding the set.
|
|
7
|
+
*/
|
|
8
|
+
import { type QueryEventSource } from './collectors.js';
|
|
9
|
+
import { Profiler } from './Profiler.js';
|
|
10
|
+
import type { ProfilerCollector } from './types.js';
|
|
11
|
+
export * from './collectors.js';
|
|
12
|
+
export { profilerEndpoint } from './endpoint.js';
|
|
13
|
+
export { MemoryProfileStore } from './MemoryProfileStore.js';
|
|
14
|
+
export { profileRequest } from './middleware.js';
|
|
15
|
+
export { PROFILER_PATH, PROFILER_TOKEN_HEADER, Profiler } from './Profiler.js';
|
|
16
|
+
export { toolbarScript } from './toolbar.js';
|
|
17
|
+
export * from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* The profiler a dev server runs with.
|
|
20
|
+
*
|
|
21
|
+
* `events` is the application's emitter. The database panel comes from
|
|
22
|
+
* `db:query` on it, which is how a data layer reports a statement without the
|
|
23
|
+
* profiler importing one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createProfiler(events: QueryEventSource, extra?: readonly ProfilerCollector[]): Profiler;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/profiler/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAOL,KAAK,gBAAgB,EAEtB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD,cAAc,iBAAiB,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,cAAc,YAAY,CAAA;AAE1B;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,KAAK,GAAE,SAAS,iBAAiB,EAAO,GACvC,QAAQ,CAeV"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profiler, assembled.
|
|
3
|
+
*
|
|
4
|
+
* One place that knows which collectors ship, so the Ignitor wires a profiler
|
|
5
|
+
* rather than a list — and an application that wants its own panel adds a
|
|
6
|
+
* collector here instead of rebuilding the set.
|
|
7
|
+
*/
|
|
8
|
+
import { databaseCollector, exceptionCollector, logCollector, memoryCollector, requestCollector, } from './collectors.js';
|
|
9
|
+
import { MemoryProfileStore } from './MemoryProfileStore.js';
|
|
10
|
+
import { Profiler } from './Profiler.js';
|
|
11
|
+
export * from './collectors.js';
|
|
12
|
+
export { profilerEndpoint } from './endpoint.js';
|
|
13
|
+
export { MemoryProfileStore } from './MemoryProfileStore.js';
|
|
14
|
+
export { profileRequest } from './middleware.js';
|
|
15
|
+
export { PROFILER_PATH, PROFILER_TOKEN_HEADER, Profiler } from './Profiler.js';
|
|
16
|
+
export { toolbarScript } from './toolbar.js';
|
|
17
|
+
export * from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* The profiler a dev server runs with.
|
|
20
|
+
*
|
|
21
|
+
* `events` is the application's emitter. The database panel comes from
|
|
22
|
+
* `db:query` on it, which is how a data layer reports a statement without the
|
|
23
|
+
* profiler importing one.
|
|
24
|
+
*/
|
|
25
|
+
export function createProfiler(events, extra = []) {
|
|
26
|
+
// Built before the profiler so the database collector can read the bag of
|
|
27
|
+
// whichever request is running — the reference is filled in a line later,
|
|
28
|
+
// and nothing reads it until a request arrives.
|
|
29
|
+
let profiler;
|
|
30
|
+
const collectors = [
|
|
31
|
+
requestCollector(),
|
|
32
|
+
databaseCollector(events, () => profiler.current('database')),
|
|
33
|
+
exceptionCollector(),
|
|
34
|
+
logCollector(events, () => profiler.current('logs')),
|
|
35
|
+
memoryCollector(),
|
|
36
|
+
...extra,
|
|
37
|
+
];
|
|
38
|
+
profiler = new Profiler(collectors, new MemoryProfileStore(collectors));
|
|
39
|
+
return profiler;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/profiler/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAElB,YAAY,EACZ,eAAe,EAGf,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGxC,cAAc,iBAAiB,CAAA;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,cAAc,YAAY,CAAA;AAE1B;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAwB,EACxB,QAAsC,EAAE;IAExC,0EAA0E;IAC1E,0EAA0E;IAC1E,gDAAgD;IAChD,IAAI,QAAkB,CAAA;IACtB,MAAM,UAAU,GAAwB;QACtC,gBAAgB,EAAE;QAClB,iBAAiB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAW,UAAU,CAAC,CAAC;QACvE,kBAAkB,EAAE;QACpB,YAAY,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAS,MAAM,CAAC,CAAC;QAC5D,eAAe,EAAE;QACjB,GAAG,KAAK;KACT,CAAA;IACD,QAAQ,GAAG,IAAI,QAAQ,CAAC,UAAU,EAAE,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAA;IACvE,OAAO,QAAQ,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The middleware that records a request.
|
|
3
|
+
*
|
|
4
|
+
* It wraps everything after it, so what it measures is the whole answer — the
|
|
5
|
+
* application's own middleware included — and it stamps the token on the
|
|
6
|
+
* response first thing. That header is how a client finds the profile again;
|
|
7
|
+
* the injected bar carries the same token inline, because a script in a page
|
|
8
|
+
* cannot read the headers of the navigation that delivered it.
|
|
9
|
+
*
|
|
10
|
+
* Registered AFTER the profiler's own endpoint and the hot-reload stream, so
|
|
11
|
+
* neither ends up in its own history — and so a reload stream, which is open
|
|
12
|
+
* for as long as the tab is, never sits in the middle of an unfinished profile.
|
|
13
|
+
*/
|
|
14
|
+
import type { MiddlewareFunction } from '../middleware/Pipeline.js';
|
|
15
|
+
import { type Profiler } from './Profiler.js';
|
|
16
|
+
export declare function profileRequest(profiler: Profiler): MiddlewareFunction;
|
|
17
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/profiler/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAEnE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,kBAAkB,CAqBrE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The middleware that records a request.
|
|
3
|
+
*
|
|
4
|
+
* It wraps everything after it, so what it measures is the whole answer — the
|
|
5
|
+
* application's own middleware included — and it stamps the token on the
|
|
6
|
+
* response first thing. That header is how a client finds the profile again;
|
|
7
|
+
* the injected bar carries the same token inline, because a script in a page
|
|
8
|
+
* cannot read the headers of the navigation that delivered it.
|
|
9
|
+
*
|
|
10
|
+
* Registered AFTER the profiler's own endpoint and the hot-reload stream, so
|
|
11
|
+
* neither ends up in its own history — and so a reload stream, which is open
|
|
12
|
+
* for as long as the tab is, never sits in the middle of an unfinished profile.
|
|
13
|
+
*/
|
|
14
|
+
import { PROFILER_TOKEN_HEADER } from './Profiler.js';
|
|
15
|
+
export function profileRequest(profiler) {
|
|
16
|
+
return async (ctx, next) => {
|
|
17
|
+
await profiler.profile({ method: ctx.request.method(), url: ctx.request.url() }, async (scope) => {
|
|
18
|
+
// Before the work, not after: a handler that throws unwinds past this
|
|
19
|
+
// line, and the request that broke is the one whose profile you most
|
|
20
|
+
// want to open.
|
|
21
|
+
ctx.response.header(PROFILER_TOKEN_HEADER, scope.token);
|
|
22
|
+
try {
|
|
23
|
+
await next();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
// After routing, because this runs ahead of it: the pattern and the
|
|
27
|
+
// controller are only on the context once a route has matched. In a
|
|
28
|
+
// `finally`, because a request that threw still matched one.
|
|
29
|
+
describeRoute(profiler.current('request'), ctx);
|
|
30
|
+
}
|
|
31
|
+
}, (error) => ({ status: statusOf(ctx, error) }));
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The status the response will carry.
|
|
36
|
+
*
|
|
37
|
+
* When the work threw, the exception handler has not run yet — the error is
|
|
38
|
+
* still travelling outwards — so the response still holds whatever it held
|
|
39
|
+
* before. Read the error the way the handler will instead, and fall back to
|
|
40
|
+
* 500, which is what an error carrying no status becomes.
|
|
41
|
+
*/
|
|
42
|
+
function statusOf(ctx, error) {
|
|
43
|
+
if (error === undefined)
|
|
44
|
+
return ctx.response.getStatus();
|
|
45
|
+
if (typeof error === 'object' && error !== null) {
|
|
46
|
+
const status = Reflect.get(error, 'status') ?? Reflect.get(error, 'statusCode');
|
|
47
|
+
if (typeof status === 'number' && Number.isInteger(status))
|
|
48
|
+
return status;
|
|
49
|
+
}
|
|
50
|
+
return 500;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What matched, as the request panel shows it.
|
|
54
|
+
*
|
|
55
|
+
* Read off the context rather than passed in, and every field optional: a 404
|
|
56
|
+
* matched nothing, and an inline handler has no controller to name.
|
|
57
|
+
*/
|
|
58
|
+
function describeRoute(bag, ctx) {
|
|
59
|
+
if (bag === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const route = ctx.route;
|
|
62
|
+
if (route === undefined)
|
|
63
|
+
return;
|
|
64
|
+
bag.route = route.name ?? route.pattern;
|
|
65
|
+
const controller = route.controller;
|
|
66
|
+
if (controller !== undefined && route.action !== undefined) {
|
|
67
|
+
// `route.controller` is the PROTOTYPE, not the class — that is what the
|
|
68
|
+
// decorator-reading middleware needs — so the name is on its constructor.
|
|
69
|
+
const name = Reflect.get(Reflect.get(controller, 'constructor') ?? {}, 'name');
|
|
70
|
+
bag.controller = `${typeof name === 'string' ? name : 'Controller'}.${route.action}`;
|
|
71
|
+
}
|
|
72
|
+
const user = ctx.auth?.user;
|
|
73
|
+
if (typeof user === 'object' && user !== null) {
|
|
74
|
+
const id = Reflect.get(user, 'id');
|
|
75
|
+
if (typeof id === 'string' || typeof id === 'number')
|
|
76
|
+
bag.userId = String(id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/profiler/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profiler's own pages, and the bar injected into yours.
|
|
3
|
+
*
|
|
4
|
+
* Plain HTML and one inline stylesheet. A dev tool that needed a build step,
|
|
5
|
+
* or pulled a framework into the page it is inspecting, would be a worse
|
|
6
|
+
* neighbour than the problem it reports — and the bar lives in a shadow root
|
|
7
|
+
* for the same reason: your stylesheet must not reach it, and it must not
|
|
8
|
+
* reach yours.
|
|
9
|
+
*
|
|
10
|
+
* EVERY value that came from a request is escaped. A profiler renders the
|
|
11
|
+
* URL someone asked for, the SQL a query ran and the message an error carried
|
|
12
|
+
* — all attacker-reachable in exactly the case you most want to look at, and a
|
|
13
|
+
* tool that turned a crafted URL into script in your own browser would be a
|
|
14
|
+
* hole with a friendly face.
|
|
15
|
+
*/
|
|
16
|
+
import type { Profile, ProfileSummary } from './types.js';
|
|
17
|
+
/** `<`, `&` and the quotes. Everything below goes through it. */
|
|
18
|
+
export declare function escapeHtml(value: unknown): string;
|
|
19
|
+
/** The history: what a developer lands on with no token in hand. */
|
|
20
|
+
export declare function renderProfileList(profiles: readonly ProfileSummary[], missing?: string): string;
|
|
21
|
+
/** One request, every panel. */
|
|
22
|
+
export declare function renderProfilePage(profile: Profile, badges: Record<string, string>): string;
|
|
23
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/profiler/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEzD,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjD;AAkCD,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B/F;AAED,gCAAgC;AAChC,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAkC1F"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profiler's own pages, and the bar injected into yours.
|
|
3
|
+
*
|
|
4
|
+
* Plain HTML and one inline stylesheet. A dev tool that needed a build step,
|
|
5
|
+
* or pulled a framework into the page it is inspecting, would be a worse
|
|
6
|
+
* neighbour than the problem it reports — and the bar lives in a shadow root
|
|
7
|
+
* for the same reason: your stylesheet must not reach it, and it must not
|
|
8
|
+
* reach yours.
|
|
9
|
+
*
|
|
10
|
+
* EVERY value that came from a request is escaped. A profiler renders the
|
|
11
|
+
* URL someone asked for, the SQL a query ran and the message an error carried
|
|
12
|
+
* — all attacker-reachable in exactly the case you most want to look at, and a
|
|
13
|
+
* tool that turned a crafted URL into script in your own browser would be a
|
|
14
|
+
* hole with a friendly face.
|
|
15
|
+
*/
|
|
16
|
+
/** `<`, `&` and the quotes. Everything below goes through it. */
|
|
17
|
+
export function escapeHtml(value) {
|
|
18
|
+
return String(value)
|
|
19
|
+
.replaceAll('&', '&')
|
|
20
|
+
.replaceAll('<', '<')
|
|
21
|
+
.replaceAll('>', '>')
|
|
22
|
+
.replaceAll('"', '"')
|
|
23
|
+
.replaceAll("'", ''');
|
|
24
|
+
}
|
|
25
|
+
const STYLE = `
|
|
26
|
+
:root { color-scheme: dark }
|
|
27
|
+
body { margin: 0; background: #14161a; color: #e6e6e6;
|
|
28
|
+
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace }
|
|
29
|
+
a { color: #6cb6ff; text-decoration: none }
|
|
30
|
+
a:hover { text-decoration: underline }
|
|
31
|
+
header { padding: 16px 20px; border-bottom: 1px solid #262a31 }
|
|
32
|
+
h1 { margin: 0; font-size: 15px; font-weight: 600 }
|
|
33
|
+
.meta { color: #8b949e; margin-top: 4px }
|
|
34
|
+
main { padding: 20px }
|
|
35
|
+
section { margin-bottom: 28px }
|
|
36
|
+
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
|
|
37
|
+
color: #8b949e; margin: 0 0 10px }
|
|
38
|
+
table { width: 100%; border-collapse: collapse }
|
|
39
|
+
td, th { text-align: left; padding: 6px 10px; border-bottom: 1px solid #21252c;
|
|
40
|
+
vertical-align: top }
|
|
41
|
+
th { color: #8b949e; font-weight: 500 }
|
|
42
|
+
.num { text-align: right; white-space: nowrap; color: #8b949e }
|
|
43
|
+
.sql { white-space: pre-wrap; word-break: break-word }
|
|
44
|
+
.slow { color: #f0883e }
|
|
45
|
+
.bad { color: #ff7b72 }
|
|
46
|
+
pre { white-space: pre-wrap; word-break: break-word; margin: 0; color: #c9d1d9 }
|
|
47
|
+
.pill { display: inline-block; padding: 1px 8px; border-radius: 999px;
|
|
48
|
+
background: #21262d; color: #8b949e; margin-right: 6px }
|
|
49
|
+
`;
|
|
50
|
+
/** A whole page, so the profiler needs nothing served alongside it. */
|
|
51
|
+
function page(title, body) {
|
|
52
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
53
|
+
<title>${escapeHtml(title)}</title><style>${STYLE}</style></head><body>${body}</body></html>`;
|
|
54
|
+
}
|
|
55
|
+
/** The history: what a developer lands on with no token in hand. */
|
|
56
|
+
export function renderProfileList(profiles, missing) {
|
|
57
|
+
const warning = missing === undefined
|
|
58
|
+
? ''
|
|
59
|
+
: `<p class="bad">No profile for <code>${escapeHtml(missing)}</code> — it may have aged out.</p>`;
|
|
60
|
+
const rows = profiles
|
|
61
|
+
.map((profile) => `<tr>
|
|
62
|
+
<td><a href="/__ream/profiler/${escapeHtml(profile.token)}">${escapeHtml(profile.token)}</a></td>
|
|
63
|
+
<td>${escapeHtml(profile.method)}</td>
|
|
64
|
+
<td class="sql">${escapeHtml(profile.url)}</td>
|
|
65
|
+
<td class="${profile.status >= 500 ? 'bad' : ''}">${escapeHtml(profile.status)}</td>
|
|
66
|
+
<td class="num">${escapeHtml(profile.duration.toFixed(1))} ms</td>
|
|
67
|
+
<td>${Object.values(profile.badges)
|
|
68
|
+
.map((badge) => `<span class="pill">${escapeHtml(badge)}</span>`)
|
|
69
|
+
.join('')}</td>
|
|
70
|
+
</tr>`)
|
|
71
|
+
.join('');
|
|
72
|
+
return page('Profiler', `<header><h1>Profiler</h1><div class="meta">${profiles.length} request${profiles.length === 1 ? '' : 's'} kept</div></header><main>${warning}
|
|
73
|
+
<table><tr><th>Token</th><th>Method</th><th>URL</th><th>Status</th>
|
|
74
|
+
<th class="num">Time</th><th>Collected</th></tr>${rows}</table></main>`);
|
|
75
|
+
}
|
|
76
|
+
/** One request, every panel. */
|
|
77
|
+
export function renderProfilePage(profile, badges) {
|
|
78
|
+
const sections = [];
|
|
79
|
+
const exception = profile.data.exception;
|
|
80
|
+
if (exception !== undefined && typeof exception.message === 'string') {
|
|
81
|
+
sections.push(`<section><h2 class="bad">Exception</h2>
|
|
82
|
+
<p class="bad">${escapeHtml(exception.name ?? 'Error')}: ${escapeHtml(exception.message)}</p>
|
|
83
|
+
${typeof exception.stack === 'string' ? `<pre>${escapeHtml(exception.stack)}</pre>` : ''}
|
|
84
|
+
</section>`);
|
|
85
|
+
}
|
|
86
|
+
sections.push(renderQueries(profile));
|
|
87
|
+
for (const [name, data] of Object.entries(profile.data)) {
|
|
88
|
+
if (name === 'database' || name === 'exception')
|
|
89
|
+
continue;
|
|
90
|
+
if (Object.keys(data).length === 0)
|
|
91
|
+
continue;
|
|
92
|
+
sections.push(`<section><h2>${escapeHtml(name)}</h2>
|
|
93
|
+
<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre></section>`);
|
|
94
|
+
}
|
|
95
|
+
return page(`${profile.method} ${profile.url}`, `<header>
|
|
96
|
+
<h1>${escapeHtml(profile.method)} ${escapeHtml(profile.url)}</h1>
|
|
97
|
+
<div class="meta">
|
|
98
|
+
${escapeHtml(profile.status)} · ${escapeHtml(profile.duration.toFixed(1))} ms ·
|
|
99
|
+
${escapeHtml(new Date(profile.time).toISOString())} ·
|
|
100
|
+
<a href="/__ream/profiler">all requests</a>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="meta">${Object.values(badges)
|
|
103
|
+
.map((badge) => `<span class="pill">${escapeHtml(badge)}</span>`)
|
|
104
|
+
.join('')}</div>
|
|
105
|
+
</header><main>${sections.join('')}</main>`);
|
|
106
|
+
}
|
|
107
|
+
/** The panel the whole thing exists for. */
|
|
108
|
+
function renderQueries(profile) {
|
|
109
|
+
const database = profile.data.database;
|
|
110
|
+
if (database === undefined)
|
|
111
|
+
return '';
|
|
112
|
+
const queries = Array.isArray(database.queries) ? database.queries : [];
|
|
113
|
+
const total = typeof database.total === 'number' ? database.total : 0;
|
|
114
|
+
if (total === 0)
|
|
115
|
+
return '<section><h2>Database</h2><p class="meta">No query.</p></section>';
|
|
116
|
+
const rows = queries
|
|
117
|
+
.map((entry) => {
|
|
118
|
+
const query = asRecord(entry);
|
|
119
|
+
const duration = typeof query.duration === 'number' ? query.duration : 0;
|
|
120
|
+
const bindings = Array.isArray(query.bindings) ? query.bindings : [];
|
|
121
|
+
return `<tr>
|
|
122
|
+
<td class="num ${duration >= 50 ? 'slow' : ''}">${escapeHtml(duration.toFixed(2))} ms</td>
|
|
123
|
+
<td class="sql">${escapeHtml(query.sql)}${bindings.length === 0
|
|
124
|
+
? ''
|
|
125
|
+
: `<div class="meta">${escapeHtml(JSON.stringify(bindings))}</div>`}${typeof query.error === 'string' ? `<div class="bad">${escapeHtml(query.error)}</div>` : ''}</td>
|
|
126
|
+
<td class="meta">${escapeHtml(query.model ?? '')}${query.inTransaction === true ? ' <span class="pill">tx</span>' : ''}</td>
|
|
127
|
+
</tr>`;
|
|
128
|
+
})
|
|
129
|
+
.join('');
|
|
130
|
+
const capped = database.capped === true
|
|
131
|
+
? `<p class="meta">Listing stopped at ${queries.length}; ${escapeHtml(total)} ran.</p>`
|
|
132
|
+
: '';
|
|
133
|
+
return `<section><h2>Database</h2>${capped}
|
|
134
|
+
<table><tr><th class="num">Time</th><th>Query</th><th>Model</th></tr>${rows}</table></section>`;
|
|
135
|
+
}
|
|
136
|
+
function asRecord(value) {
|
|
137
|
+
return typeof value === 'object' && value !== null
|
|
138
|
+
? Object.fromEntries(Object.entries(value))
|
|
139
|
+
: {};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/profiler/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bar at the bottom of the page.
|
|
3
|
+
*
|
|
4
|
+
* It carries its own token inline rather than reading `X-Debug-Token` off the
|
|
5
|
+
* response: a script in a page cannot see the headers of the navigation that
|
|
6
|
+
* delivered it. Symfony injects the token for the same reason.
|
|
7
|
+
*
|
|
8
|
+
* Everything lives in a shadow root — your stylesheet must not reach the bar,
|
|
9
|
+
* and the bar must not reach your page. It fetches its numbers once, on load,
|
|
10
|
+
* and then does nothing: a dev tool that polls is a dev tool that shows up in
|
|
11
|
+
* its own history.
|
|
12
|
+
*
|
|
13
|
+
* A string rather than a stringified function, as the hot-reload script is:
|
|
14
|
+
* this package compiles without the DOM library, deliberately — it is a server
|
|
15
|
+
* — so browser code here is text, and its only inputs are escaped below.
|
|
16
|
+
*
|
|
17
|
+
* It styles its nodes through the CSSOM — `node.style.cssText` — rather than
|
|
18
|
+
* with a `<style>` element. Under a CSP with `style-src 'self'` (or only a
|
|
19
|
+
* `default-src`, which is the common case) a stylesheet inserted at runtime is
|
|
20
|
+
* inline style and is dropped, with no error anywhere: the bar renders as a
|
|
21
|
+
* stack of grey text and nothing says why. Setting properties on an element is
|
|
22
|
+
* not inline style and is never blocked, which is also how the host below has
|
|
23
|
+
* always been positioned.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The `<script>` to append to an HTML response.
|
|
27
|
+
*
|
|
28
|
+
* `token` is generated by the profiler (hex) and `nonce` by the response, so
|
|
29
|
+
* neither is user input; both are still put through `JSON.stringify` rather
|
|
30
|
+
* than interpolated raw, because "cannot contain a quote" is an invariant that
|
|
31
|
+
* holds until someone changes how one of them is made.
|
|
32
|
+
*/
|
|
33
|
+
export declare function toolbarScript(token: string, nonce?: string): string;
|
|
34
|
+
//# sourceMappingURL=toolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolbar.d.ts","sourceRoot":"","sources":["../../src/profiler/toolbar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAInE"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bar at the bottom of the page.
|
|
3
|
+
*
|
|
4
|
+
* It carries its own token inline rather than reading `X-Debug-Token` off the
|
|
5
|
+
* response: a script in a page cannot see the headers of the navigation that
|
|
6
|
+
* delivered it. Symfony injects the token for the same reason.
|
|
7
|
+
*
|
|
8
|
+
* Everything lives in a shadow root — your stylesheet must not reach the bar,
|
|
9
|
+
* and the bar must not reach your page. It fetches its numbers once, on load,
|
|
10
|
+
* and then does nothing: a dev tool that polls is a dev tool that shows up in
|
|
11
|
+
* its own history.
|
|
12
|
+
*
|
|
13
|
+
* A string rather than a stringified function, as the hot-reload script is:
|
|
14
|
+
* this package compiles without the DOM library, deliberately — it is a server
|
|
15
|
+
* — so browser code here is text, and its only inputs are escaped below.
|
|
16
|
+
*
|
|
17
|
+
* It styles its nodes through the CSSOM — `node.style.cssText` — rather than
|
|
18
|
+
* with a `<style>` element. Under a CSP with `style-src 'self'` (or only a
|
|
19
|
+
* `default-src`, which is the common case) a stylesheet inserted at runtime is
|
|
20
|
+
* inline style and is dropped, with no error anywhere: the bar renders as a
|
|
21
|
+
* stack of grey text and nothing says why. Setting properties on an element is
|
|
22
|
+
* not inline style and is never blocked, which is also how the host below has
|
|
23
|
+
* always been positioned.
|
|
24
|
+
*/
|
|
25
|
+
import { PROFILER_PATH } from './Profiler.js';
|
|
26
|
+
/**
|
|
27
|
+
* The `<script>` to append to an HTML response.
|
|
28
|
+
*
|
|
29
|
+
* `token` is generated by the profiler (hex) and `nonce` by the response, so
|
|
30
|
+
* neither is user input; both are still put through `JSON.stringify` rather
|
|
31
|
+
* than interpolated raw, because "cannot contain a quote" is an invariant that
|
|
32
|
+
* holds until someone changes how one of them is made.
|
|
33
|
+
*/
|
|
34
|
+
export function toolbarScript(token, nonce) {
|
|
35
|
+
const attribute = nonce === undefined ? '' : ` nonce="${nonce.replace(/"/g, '"')}"`;
|
|
36
|
+
const config = JSON.stringify({ token, path: PROFILER_PATH });
|
|
37
|
+
return `<script${attribute}>(${BAR})(${config})</script>`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The bar, as it runs in the browser.
|
|
41
|
+
*
|
|
42
|
+
* Kept on one indented block so what ships is readable in a page's source —
|
|
43
|
+
* someone will open the inspector and find this, and a minified blob would
|
|
44
|
+
* tell them nothing about where it came from.
|
|
45
|
+
*/
|
|
46
|
+
const BAR = `function(c){
|
|
47
|
+
var host=document.createElement('div');
|
|
48
|
+
host.style.cssText='position:fixed;left:0;right:0;bottom:0;z-index:2147483000';
|
|
49
|
+
var root=host.attachShadow({mode:'closed'});
|
|
50
|
+
document.body.appendChild(host);
|
|
51
|
+
var bar=document.createElement('div');
|
|
52
|
+
bar.style.cssText='display:flex;align-items:stretch;background:#14161a;color:#e6e6e6;'
|
|
53
|
+
+'font:12px/1 ui-monospace,Menlo,monospace;border-top:1px solid #262a31;'
|
|
54
|
+
+'box-shadow:0 -2px 8px rgba(0,0,0,.4)';
|
|
55
|
+
root.appendChild(bar);
|
|
56
|
+
var page=c.path+'/'+c.token;
|
|
57
|
+
var COLOR={bad:'#ff7b72',warn:'#f0883e',ok:'#7ee787'};
|
|
58
|
+
function cell(label,value,tone,plain){
|
|
59
|
+
var node=document.createElement('div');
|
|
60
|
+
node.style.cssText='padding:7px 11px;white-space:nowrap;'
|
|
61
|
+
+(plain?'flex:1;overflow:hidden;text-overflow:ellipsis;cursor:default'
|
|
62
|
+
:'border-right:1px solid #21252c;cursor:pointer');
|
|
63
|
+
if(label){
|
|
64
|
+
var tag=document.createElement('span');
|
|
65
|
+
tag.style.cssText='color:#8b949e;margin-right:6px';
|
|
66
|
+
tag.textContent=label;
|
|
67
|
+
node.appendChild(tag);
|
|
68
|
+
}
|
|
69
|
+
var text=document.createElement('span');
|
|
70
|
+
if(tone){text.style.color=COLOR[tone];}
|
|
71
|
+
text.textContent=value;
|
|
72
|
+
node.appendChild(text);
|
|
73
|
+
if(!plain){
|
|
74
|
+
node.addEventListener('mouseenter',function(){node.style.background='#1b1f25'});
|
|
75
|
+
node.addEventListener('mouseleave',function(){node.style.background=''});
|
|
76
|
+
node.addEventListener('click',function(){window.open(page,'_blank')});
|
|
77
|
+
}
|
|
78
|
+
bar.appendChild(node);
|
|
79
|
+
}
|
|
80
|
+
function panel(data,name){
|
|
81
|
+
var value=data&&data[name];
|
|
82
|
+
return (value&&typeof value==='object')?value:null;
|
|
83
|
+
}
|
|
84
|
+
fetch(page+'.json',{headers:{accept:'application/json'}})
|
|
85
|
+
.then(function(r){return r.ok?r.json():null})
|
|
86
|
+
.then(function(p){
|
|
87
|
+
if(!p){host.remove();return}
|
|
88
|
+
var status=Number(p.status||0);
|
|
89
|
+
cell('',String(status),status>=500?'bad':status>=400?'warn':'ok');
|
|
90
|
+
cell('time',Number(p.duration||0).toFixed(1)+' ms');
|
|
91
|
+
var db=panel(p.data,'database');
|
|
92
|
+
if(db){
|
|
93
|
+
var total=Number(db.total||0);
|
|
94
|
+
cell('db',total+' \u00b7 '+Number(db.duration||0).toFixed(1)+' ms',total>=50?'warn':'');
|
|
95
|
+
}
|
|
96
|
+
var mem=panel(p.data,'memory');
|
|
97
|
+
if(mem){cell('mem',Number(mem.usedMb||0).toFixed(1)+' MB');}
|
|
98
|
+
var err=panel(p.data,'exception');
|
|
99
|
+
if(err&&typeof err.message==='string'){cell('',err.message,'bad');}
|
|
100
|
+
cell('',String(p.url||''),'',true);
|
|
101
|
+
})
|
|
102
|
+
.catch(function(){host.remove()});
|
|
103
|
+
}`;
|
|
104
|
+
//# sourceMappingURL=toolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../src/profiler/toolbar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE7C;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,KAAc;IACzD,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAA;IACxF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAA;IAC7D,OAAO,UAAU,SAAS,KAAK,GAAG,KAAK,MAAM,YAAY,CAAA;AAC3D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyDV,CAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the profiler keeps about one request.
|
|
3
|
+
*
|
|
4
|
+
* Shaped after Symfony's profiler rather than Laravel's debugbar, which is a
|
|
5
|
+
* decision about where the data lives: a token per request, an endpoint that
|
|
6
|
+
* serves it, and a history. Inlining it in the page is simpler until the
|
|
7
|
+
* request you need to look at was a POST that redirected — and then the data
|
|
8
|
+
* is gone, which is exactly when you wanted it.
|
|
9
|
+
*/
|
|
10
|
+
/** One collector's contribution to a profile. Plain data: it crosses a wire. */
|
|
11
|
+
export type CollectedData = Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Everything kept about one request.
|
|
14
|
+
*
|
|
15
|
+
* `token` identifies it. It travels back on `X-Debug-Token`, so a client — the
|
|
16
|
+
* toolbar, curl, a test — can ask for the profile of a request it just made.
|
|
17
|
+
*/
|
|
18
|
+
export interface Profile {
|
|
19
|
+
token: string;
|
|
20
|
+
/** When the request arrived, epoch milliseconds. */
|
|
21
|
+
time: number;
|
|
22
|
+
method: string;
|
|
23
|
+
url: string;
|
|
24
|
+
status: number;
|
|
25
|
+
/** Wall-clock milliseconds, first middleware to serialised response. */
|
|
26
|
+
duration: number;
|
|
27
|
+
/** Keyed by collector name. */
|
|
28
|
+
data: Record<string, CollectedData>;
|
|
29
|
+
}
|
|
30
|
+
/** A profile without its data — what a listing shows. */
|
|
31
|
+
export type ProfileSummary = Omit<Profile, 'data'> & {
|
|
32
|
+
/** The headline each collector wants on the bar: `{ database: '12 queries' }`. */
|
|
33
|
+
badges: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What a collector is handed while a request runs.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately not the HttpContext: a collector observes, and handing it the
|
|
39
|
+
* context would let it answer the request. What it needs is a place to put
|
|
40
|
+
* what it saw and the ability to know when the request ends.
|
|
41
|
+
*/
|
|
42
|
+
export interface CollectionScope {
|
|
43
|
+
token: string;
|
|
44
|
+
/** Called by the framework when the request is done. */
|
|
45
|
+
readonly startedAt: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A source of data about a request.
|
|
49
|
+
*
|
|
50
|
+
* `start` runs when the request begins and answers a bag the collector will
|
|
51
|
+
* fill; `finish` turns that bag into what is stored. Two methods rather than
|
|
52
|
+
* one, because the interesting collectors — queries, logs, events — are fed by
|
|
53
|
+
* things happening DURING the request, not by looking at it afterwards.
|
|
54
|
+
*/
|
|
55
|
+
export interface ProfilerCollector<Bag = unknown> {
|
|
56
|
+
/** Unique. It keys the data and names the panel. */
|
|
57
|
+
readonly name: string;
|
|
58
|
+
/** Shown on the bar, or nothing when there is nothing worth a badge. */
|
|
59
|
+
badge?: (data: CollectedData) => string | undefined;
|
|
60
|
+
start(scope: CollectionScope): Bag;
|
|
61
|
+
finish(bag: Bag, scope: CollectionScope): CollectedData;
|
|
62
|
+
}
|
|
63
|
+
/** Where profiles are kept. Bounded: a dev session is not an archive. */
|
|
64
|
+
export interface ProfileStore {
|
|
65
|
+
save(profile: Profile): void;
|
|
66
|
+
find(token: string): Profile | undefined;
|
|
67
|
+
recent(limit?: number): ProfileSummary[];
|
|
68
|
+
clear(): void;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/profiler/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;IAChB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CACpC;AAED,yDAAyD;AACzD,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACnD,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B,CAAA;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB,CAAC,GAAG,GAAG,OAAO;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAA;IACnD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,GAAG,CAAA;IAClC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,eAAe,GAAG,aAAa,CAAA;CACxD;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACxC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAAA;IACxC,KAAK,IAAI,IAAI,CAAA;CACd"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the profiler keeps about one request.
|
|
3
|
+
*
|
|
4
|
+
* Shaped after Symfony's profiler rather than Laravel's debugbar, which is a
|
|
5
|
+
* decision about where the data lives: a token per request, an endpoint that
|
|
6
|
+
* serves it, and a history. Inlining it in the page is simpler until the
|
|
7
|
+
* request you need to look at was a POST that redirected — and then the data
|
|
8
|
+
* is gone, which is exactly when you wanted it.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/profiler/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
package/dist/router/Router.d.ts
CHANGED
|
@@ -507,6 +507,17 @@ export declare class Router extends Macroable {
|
|
|
507
507
|
* the server. Powers the aurora client URL helper.
|
|
508
508
|
*/
|
|
509
509
|
namedManifest(): Record<string, string>;
|
|
510
|
+
/**
|
|
511
|
+
* Every NAMED route with its path pattern AND the HTTP methods it answers,
|
|
512
|
+
* e.g. `{ 'users.update': { path: '/users/:id', methods: ['PUT'] } }` — what
|
|
513
|
+
* a client needs to follow a route by name and pick its verb, the way
|
|
514
|
+
* AdonisJS's Tuyau registry gives `route` + `methods`. `namedManifest()`
|
|
515
|
+
* stays the lighter name → path map.
|
|
516
|
+
*/
|
|
517
|
+
namedRoutes(): Record<string, {
|
|
518
|
+
path: string;
|
|
519
|
+
methods: string[];
|
|
520
|
+
}>;
|
|
510
521
|
/**
|
|
511
522
|
* Generate a TypeScript source string typing every named route and its
|
|
512
523
|
* params (AdonisJS `generateTypes`) — a `RouteName` union plus a
|