@alepha/devtools 0.11.5 → 0.11.7

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.
@@ -0,0 +1,134 @@
1
+ import { join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { $batch } from "@alepha/batch";
4
+ import { $hook, $inject, Alepha, pageQuerySchema, t } from "@alepha/core";
5
+ import {
6
+ $logger,
7
+ JsonFormatterProvider,
8
+ type LogEntry,
9
+ logEntrySchema,
10
+ } from "@alepha/logger";
11
+ import { parseQueryString } from "@alepha/postgres";
12
+ import { $route, ServerProvider } from "@alepha/server";
13
+ import { $serve } from "@alepha/server-static";
14
+ import { type DevLogEntry, logs } from "./entities/logs.ts";
15
+ import { DevToolsMetadataProvider } from "./providers/DevToolsMetadataProvider.ts";
16
+ import { LogRepository } from "./repositories/LogRepository.ts";
17
+ import { devMetadataSchema } from "./schemas/DevMetadata.ts";
18
+
19
+ export class DevToolsProvider {
20
+ protected readonly log = $logger();
21
+ protected readonly alepha = $inject(Alepha);
22
+ protected readonly serverProvider = $inject(ServerProvider);
23
+ protected readonly jsonFormatter = $inject(JsonFormatterProvider);
24
+ protected readonly logs = $inject(LogRepository);
25
+ protected readonly devCollectorProvider = $inject(DevToolsMetadataProvider);
26
+
27
+ protected readonly onStart = $hook({
28
+ on: "start",
29
+ handler: () => {
30
+ this.log.info(
31
+ `Devtools available at ${this.serverProvider.hostname}/devtools/`,
32
+ );
33
+ },
34
+ });
35
+
36
+ protected batchLogs = $batch({
37
+ maxSize: 50,
38
+ maxDuration: [10, "seconds"],
39
+ schema: logs.insertSchema,
40
+ handler: async (entries) => {
41
+ await this.logs.createMany(entries);
42
+ },
43
+ });
44
+
45
+ protected readonly onLog = $hook({
46
+ on: "log",
47
+ handler: async (ev: { message?: string; entry: LogEntry }) => {
48
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
49
+ // CAUTION: It's very easy to create an infinite loop here.
50
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
51
+
52
+ try {
53
+ if (!this.alepha.isStarted()) {
54
+ return;
55
+ }
56
+
57
+ if (ev.entry.module === "alepha.devtools") {
58
+ // skip devtools logs to avoid infinite loop
59
+ return;
60
+ }
61
+
62
+ if (ev.entry.level === "TRACE" && ev.entry.module === "alepha.batch") {
63
+ // skip batch trace logs to avoid infinite loop
64
+ return;
65
+ }
66
+
67
+ if (this.alepha.isProduction() && ev.entry.level === "TRACE") {
68
+ // skip trace logs in production
69
+ return;
70
+ }
71
+
72
+ const entry = {
73
+ ...ev.entry,
74
+ data:
75
+ ev.entry.data instanceof Error
76
+ ? this.jsonFormatter.formatJsonError(ev.entry.data)
77
+ : typeof ev.entry.data === "object" &&
78
+ !Array.isArray(ev.entry.data)
79
+ ? ev.entry.data
80
+ : { data: ev.entry.data },
81
+ };
82
+
83
+ await this.batchLogs.push(entry as DevLogEntry);
84
+ } catch (error) {
85
+ // DO TO NOT WITH THE LOGGER HERE TO AVOID INFINITE LOOP
86
+ console.error(error, ev);
87
+ }
88
+ },
89
+ });
90
+
91
+ protected readonly uiRoute = $serve({
92
+ path: "/devtools",
93
+ root: join(fileURLToPath(import.meta.url), "../../assets/devtools"),
94
+ historyApiFallback: true,
95
+ });
96
+
97
+ protected readonly metadataRoute = $route({
98
+ method: "GET",
99
+ path: "/devtools/api/metadata",
100
+ silent: true,
101
+ schema: {
102
+ response: devMetadataSchema,
103
+ },
104
+ handler: () => {
105
+ return this.devCollectorProvider.getMetadata();
106
+ },
107
+ });
108
+
109
+ protected readonly logsRoute = $route({
110
+ method: "GET",
111
+ path: "/devtools/api/logs",
112
+ silent: true,
113
+ schema: {
114
+ query: t.extend(pageQuerySchema, {
115
+ search: t.optional(t.string()),
116
+ }),
117
+ response: t.page(logEntrySchema),
118
+ },
119
+ handler: ({ query }) => {
120
+ query.sort ??= "-timestamp";
121
+ return this.logs.paginate(
122
+ query,
123
+ query.search
124
+ ? {
125
+ where: parseQueryString(query.search),
126
+ }
127
+ : {},
128
+ {
129
+ count: true,
130
+ },
131
+ );
132
+ },
133
+ });
134
+ }
@@ -0,0 +1,21 @@
1
+ import { type Static, t } from "@alepha/core";
2
+ import { $entity, pg } from "@alepha/postgres";
3
+
4
+ export const logs = $entity({
5
+ name: "logs",
6
+ schema: t.object({
7
+ id: pg.primaryKey(),
8
+ level: t.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]),
9
+ message: t.text({
10
+ size: "rich",
11
+ }),
12
+ service: t.text(),
13
+ module: t.text(),
14
+ context: t.optional(t.text()),
15
+ app: t.optional(t.text()),
16
+ data: t.optional(t.json()),
17
+ timestamp: t.datetime(),
18
+ }),
19
+ });
20
+
21
+ export type DevLogEntry = Static<typeof logs.schema>;
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { $module } from "@alepha/core";
2
- import { DevCollectorProvider } from "./DevCollectorProvider.ts";
2
+ import { DevToolsProvider } from "./DevToolsProvider.ts";
3
+ import { DevToolsDatabaseProvider } from "./providers/DevToolsDatabaseProvider.ts";
4
+ import { DevToolsMetadataProvider } from "./providers/DevToolsMetadataProvider.ts";
5
+ import { LogRepository } from "./repositories/LogRepository.ts";
3
6
 
4
7
  // ---------------------------------------------------------------------------------------------------------------------
5
8
 
6
- export * from "./DevCollectorProvider.ts";
9
+ export * from "./providers/DevToolsMetadataProvider.ts";
7
10
  export * from "./schemas/DevActionMetadata.ts";
8
11
  export * from "./schemas/DevBucketMetadata.ts";
9
12
  export * from "./schemas/DevCacheMetadata.ts";
@@ -24,15 +27,22 @@ export * from "./schemas/DevTopicMetadata.ts";
24
27
  * This module provides comprehensive data collection capabilities for tracking application behavior,
25
28
  * performance metrics, and debugging information in real-time.
26
29
  *
27
- * @see {@link DevCollectorProvider}
30
+ * @see {@link DevToolsMetadataProvider}
28
31
  * @module alepha.devtools
29
32
  */
30
33
  export const AlephaDevtools = $module({
31
34
  name: "alepha.devtools",
32
35
  descriptors: [],
33
- services: [DevCollectorProvider],
36
+ services: [
37
+ DevToolsMetadataProvider,
38
+ DevToolsProvider,
39
+ DevToolsDatabaseProvider,
40
+ LogRepository,
41
+ ],
34
42
  register: (alepha) => {
35
- alepha.with(DevCollectorProvider);
36
- alepha.state.push("assets", "@alepha/devtools");
43
+ alepha.with(DevToolsProvider);
44
+ alepha.with(DevToolsDatabaseProvider);
45
+ alepha.with(DevToolsMetadataProvider);
46
+ alepha.state.push("alepha.build.assets", "@alepha/devtools");
37
47
  },
38
48
  });
@@ -0,0 +1,11 @@
1
+ import { NodeSqliteProvider } from "@alepha/postgres";
2
+
3
+ export class DevToolsDatabaseProvider extends NodeSqliteProvider {
4
+ public get name() {
5
+ return "devtools";
6
+ }
7
+
8
+ protected readonly options = {
9
+ path: ":memory:",
10
+ };
11
+ }
@@ -1,90 +1,27 @@
1
- import { join } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
1
  import { $bucket } from "@alepha/bucket";
4
2
  import { $cache } from "@alepha/cache";
5
- import { $hook, $inject, Alepha, t } from "@alepha/core";
6
- import { $logger, type LogEntry, logEntrySchema } from "@alepha/logger";
3
+ import { $inject, Alepha } from "@alepha/core";
4
+ import { $logger } from "@alepha/logger";
7
5
  import { $queue } from "@alepha/queue";
8
- import { $page } from "@alepha/react";
9
6
  import { $scheduler } from "@alepha/scheduler";
10
7
  import { $realm } from "@alepha/security";
11
- import { $action, $route, ServerProvider } from "@alepha/server";
12
- import { $serve } from "@alepha/server-static";
8
+ import { $action } from "@alepha/server";
13
9
  import { $topic } from "@alepha/topic";
14
- import type { DevActionMetadata } from "./schemas/DevActionMetadata.ts";
15
- import type { DevBucketMetadata } from "./schemas/DevBucketMetadata.ts";
16
- import type { DevCacheMetadata } from "./schemas/DevCacheMetadata.ts";
17
- import { type DevMetadata, devMetadataSchema } from "./schemas/DevMetadata.ts";
18
- import type { DevModuleMetadata } from "./schemas/DevModuleMetadata.ts";
19
- import type { DevPageMetadata } from "./schemas/DevPageMetadata.ts";
20
- import type { DevProviderMetadata } from "./schemas/DevProviderMetadata.ts";
21
- import type { DevQueueMetadata } from "./schemas/DevQueueMetadata.ts";
22
- import type { DevRealmMetadata } from "./schemas/DevRealmMetadata.ts";
23
- import type { DevSchedulerMetadata } from "./schemas/DevSchedulerMetadata.ts";
24
- import type { DevTopicMetadata } from "./schemas/DevTopicMetadata.ts";
25
-
26
- export class DevCollectorProvider {
10
+ import type { DevActionMetadata } from "../schemas/DevActionMetadata.ts";
11
+ import type { DevBucketMetadata } from "../schemas/DevBucketMetadata.ts";
12
+ import type { DevCacheMetadata } from "../schemas/DevCacheMetadata.ts";
13
+ import type { DevMetadata } from "../schemas/DevMetadata.ts";
14
+ import type { DevModuleMetadata } from "../schemas/DevModuleMetadata.ts";
15
+ import type { DevPageMetadata } from "../schemas/DevPageMetadata.ts";
16
+ import type { DevProviderMetadata } from "../schemas/DevProviderMetadata.ts";
17
+ import type { DevQueueMetadata } from "../schemas/DevQueueMetadata.ts";
18
+ import type { DevRealmMetadata } from "../schemas/DevRealmMetadata.ts";
19
+ import type { DevSchedulerMetadata } from "../schemas/DevSchedulerMetadata.ts";
20
+ import type { DevTopicMetadata } from "../schemas/DevTopicMetadata.ts";
21
+
22
+ export class DevToolsMetadataProvider {
27
23
  protected readonly alepha = $inject(Alepha);
28
- protected readonly serverProvider = $inject(ServerProvider);
29
24
  protected readonly log = $logger();
30
- protected readonly logs: LogEntry[] = [];
31
- protected readonly maxLogs = 10000;
32
-
33
- protected readonly onStart = $hook({
34
- on: "start",
35
- handler: () => {
36
- this.log.info(
37
- `Devtools available at ${this.serverProvider.hostname}/devtools/`,
38
- );
39
- },
40
- });
41
-
42
- protected readonly onLog = $hook({
43
- on: "log",
44
- handler: (ev: { message?: string; entry: LogEntry }) => {
45
- this.logs.unshift(ev.entry);
46
-
47
- // keep only the last 10000 logs
48
- if (this.logs.length > this.maxLogs) {
49
- this.logs.pop();
50
- }
51
- },
52
- });
53
-
54
- protected readonly uiRoute = $serve({
55
- path: "/devtools",
56
- root: join(fileURLToPath(import.meta.url), "../../assets/devtools"),
57
- });
58
-
59
- protected readonly metadataRoute = $route({
60
- method: "GET",
61
- path: "/devtools/api/metadata",
62
- silent: true,
63
- schema: {
64
- response: devMetadataSchema,
65
- },
66
- handler: () => {
67
- return this.getMetadata();
68
- },
69
- });
70
-
71
- protected readonly logsRoute = $route({
72
- method: "GET",
73
- path: "/devtools/api/logs",
74
- silent: true,
75
- schema: {
76
- response: t.array(logEntrySchema),
77
- },
78
- handler: () => {
79
- return this.getLogs();
80
- },
81
- });
82
-
83
- public getLogs(): LogEntry[] {
84
- return this.logs;
85
- }
86
-
87
- // -------------------------------------------------------------------------------------------------------------------
88
25
 
89
26
  public getActions(): DevActionMetadata[] {
90
27
  const actionDescriptors = this.alepha.descriptors($action);
@@ -191,28 +128,30 @@ export class DevCollectorProvider {
191
128
  }
192
129
 
193
130
  public getPages(): DevPageMetadata[] {
194
- const pageDescriptors = this.alepha.descriptors($page);
195
-
196
- return pageDescriptors.map((page) => ({
197
- name: page.name,
198
- description: page.options.description,
199
- path: page.options.path,
200
- params: page.options.schema?.params,
201
- query: page.options.schema?.query,
202
- hasComponent: !!page.options.component,
203
- hasLazy: !!page.options.lazy,
204
- hasResolve: !!page.options.resolve,
205
- hasChildren: !!page.options.children,
206
- hasParent: !!page.options.parent,
207
- hasErrorHandler: !!page.options.errorHandler,
208
- static:
209
- typeof page.options.static === "boolean"
210
- ? page.options.static
211
- : !!page.options.static,
212
- cache: page.options.cache,
213
- client: page.options.client,
214
- animation: page.options.animation,
215
- }));
131
+ // const pageDescriptors = this.alepha.descriptors($page);
132
+ //
133
+ // return pageDescriptors.map((page) => ({
134
+ // name: page.name,
135
+ // description: page.options.description,
136
+ // path: page.options.path,
137
+ // params: page.options.schema?.params,
138
+ // query: page.options.schema?.query,
139
+ // hasComponent: !!page.options.component,
140
+ // hasLazy: !!page.options.lazy,
141
+ // hasResolve: !!page.options.resolve,
142
+ // hasChildren: !!page.options.children,
143
+ // hasParent: !!page.options.parent,
144
+ // hasErrorHandler: !!page.options.errorHandler,
145
+ // static:
146
+ // typeof page.options.static === "boolean"
147
+ // ? page.options.static
148
+ // : !!page.options.static,
149
+ // cache: page.options.cache,
150
+ // client: page.options.client,
151
+ // animation: page.options.animation,
152
+ // }));
153
+
154
+ return [];
216
155
  }
217
156
 
218
157
  public getProviders(): DevProviderMetadata[] {
@@ -0,0 +1,9 @@
1
+ import { Repository } from "@alepha/postgres";
2
+ import { logs } from "../entities/logs.ts";
3
+ import { DevToolsDatabaseProvider } from "../providers/DevToolsDatabaseProvider.ts";
4
+
5
+ export class LogRepository extends Repository<typeof logs.schema> {
6
+ constructor() {
7
+ super(logs, DevToolsDatabaseProvider);
8
+ }
9
+ }
@@ -1,44 +1,11 @@
1
1
  import { $page } from "@alepha/react";
2
- import { AdminShell, RootRouter, Text, ui } from "@alepha/ui";
2
+ import { RootRouter } from "@alepha/ui";
3
3
  import { IconDashboard, IconLogs } from "@tabler/icons-react";
4
- import DevLogs from "./DevLogs.tsx";
5
4
 
6
5
  export class AppRouter extends RootRouter {
7
6
  layout = $page({
8
7
  parent: this.root,
9
- component: () => (
10
- <AdminShell
11
- appShellProps={{
12
- bg: ui.colors.surface,
13
- }}
14
- appShellNavbarProps={{
15
- bg: ui.colors.transparent,
16
- }}
17
- appShellHeaderProps={{
18
- bg: ui.colors.transparent,
19
- style: {
20
- backdropFilter: "blur(10px)",
21
- },
22
- }}
23
- sidebarProps={{
24
- collapsed: true,
25
- gap: "xs",
26
- }}
27
- appBarProps={{
28
- items: [
29
- { position: "left", type: "burger" },
30
- {
31
- position: "center",
32
- element: (
33
- <Text fw="bold" size="lg">
34
- Alepha DevTools
35
- </Text>
36
- ),
37
- },
38
- ],
39
- }}
40
- />
41
- ),
8
+ lazy: () => import("./components/DevLayout.tsx"),
42
9
  });
43
10
 
44
11
  dashboard = $page({
@@ -56,6 +23,6 @@ export class AppRouter extends RootRouter {
56
23
  icon: <IconLogs />,
57
24
  static: true,
58
25
  parent: this.layout,
59
- component: DevLogs,
26
+ lazy: () => import("./components/DevLogViewer.tsx"),
60
27
  });
61
28
  }
@@ -0,0 +1,80 @@
1
+ import { NestedView } from "@alepha/react";
2
+ import {
3
+ ActionButton,
4
+ AdminShell,
5
+ DarkModeButton,
6
+ Flex,
7
+ OmnibarButton,
8
+ ui,
9
+ } from "@alepha/ui";
10
+ import { IconDashboard, IconLogs, IconTools } from "@tabler/icons-react";
11
+
12
+ export const DevLayout = () => {
13
+ return (
14
+ <AdminShell
15
+ appShellProps={{
16
+ withBorder: false,
17
+ bg: ui.colors.surface,
18
+ }}
19
+ appShellNavbarProps={{
20
+ bg: ui.colors.transparent,
21
+ }}
22
+ appShellHeaderProps={{
23
+ bg: ui.colors.transparent,
24
+ style: {
25
+ backdropFilter: "blur(10px)",
26
+ },
27
+ }}
28
+ sidebarProps={{
29
+ gap: "xs",
30
+ collapsed: true,
31
+ menu: [
32
+ {
33
+ label: "Dashboard",
34
+ icon: <IconDashboard />,
35
+ href: "/",
36
+ },
37
+ {
38
+ label: "Logs",
39
+ icon: <IconLogs />,
40
+ href: "/logs",
41
+ },
42
+ ],
43
+ }}
44
+ appBarProps={{
45
+ items: [
46
+ { position: "left", type: "burger" },
47
+ {
48
+ position: "left",
49
+ element: (
50
+ <ActionButton icon={<IconTools />} href={"/"} active={false}>
51
+ Devtools
52
+ </ActionButton>
53
+ ),
54
+ },
55
+ {
56
+ position: "center",
57
+ element: <OmnibarButton />,
58
+ },
59
+ {
60
+ position: "right",
61
+ element: <DarkModeButton />,
62
+ },
63
+ ],
64
+ }}
65
+ >
66
+ <Flex
67
+ bd={`1px solid ${ui.colors.border}`}
68
+ bg={ui.colors.elevated}
69
+ bdrs={"lg"}
70
+ p={"xl"}
71
+ ml={-8}
72
+ mt={-8}
73
+ >
74
+ <NestedView />
75
+ </Flex>
76
+ </AdminShell>
77
+ );
78
+ };
79
+
80
+ export default DevLayout;