@reckona/mreact-router 0.0.65 → 0.0.67

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.
Files changed (59) hide show
  1. package/package.json +13 -12
  2. package/src/actions.ts +1130 -0
  3. package/src/adapters/aws-lambda.ts +993 -0
  4. package/src/adapters/cloudflare.ts +1286 -0
  5. package/src/adapters/devtools.ts +5 -0
  6. package/src/adapters/edge.ts +70 -0
  7. package/src/adapters/node.ts +126 -0
  8. package/src/adapters/static.ts +61 -0
  9. package/src/app-router-globals.ts +19 -0
  10. package/src/assets.ts +113 -0
  11. package/src/build.ts +2948 -0
  12. package/src/bundle-pipeline.ts +496 -0
  13. package/src/cache-config.ts +35 -0
  14. package/src/cache-stats.ts +54 -0
  15. package/src/cache.ts +418 -0
  16. package/src/cli-options.ts +296 -0
  17. package/src/cli.ts +94 -0
  18. package/src/client.ts +3398 -0
  19. package/src/config.ts +146 -0
  20. package/src/cookies.ts +113 -0
  21. package/src/csp.ts +103 -0
  22. package/src/csrf.ts +132 -0
  23. package/src/deferred.ts +52 -0
  24. package/src/dev-server.ts +262 -0
  25. package/src/file-conventions.ts +88 -0
  26. package/src/http.ts +128 -0
  27. package/src/i18n.ts +98 -0
  28. package/src/import-policy.ts +261 -0
  29. package/src/index.ts +221 -0
  30. package/src/link.ts +47 -0
  31. package/src/logger.ts +149 -0
  32. package/src/module-runner.ts +554 -0
  33. package/src/multipart.ts +577 -0
  34. package/src/native-escape.ts +53 -0
  35. package/src/native-route-matcher.ts +173 -0
  36. package/src/navigation-state.ts +98 -0
  37. package/src/navigation.ts +215 -0
  38. package/src/prerender-store.ts +233 -0
  39. package/src/render.ts +5187 -0
  40. package/src/route-path.ts +5 -0
  41. package/src/route-shells.ts +47 -0
  42. package/src/route-source.ts +224 -0
  43. package/src/route-styles.ts +91 -0
  44. package/src/routes.ts +362 -0
  45. package/src/runtime-cache.ts +8 -0
  46. package/src/runtime-state.ts +37 -0
  47. package/src/security-headers.ts +134 -0
  48. package/src/serve.ts +1265 -0
  49. package/src/session.ts +238 -0
  50. package/src/source-jsx.ts +30 -0
  51. package/src/source-modules.ts +69 -0
  52. package/src/stream-list.ts +45 -0
  53. package/src/trace.ts +114 -0
  54. package/src/types.ts +155 -0
  55. package/src/upgrade.ts +8 -0
  56. package/src/vite-config.ts +63 -0
  57. package/src/vite-plugin-cache-key.ts +53 -0
  58. package/src/vite.ts +690 -0
  59. package/src/workspace-packages.ts +67 -0
@@ -0,0 +1,261 @@
1
+ import { builtinModules, createRequire } from "node:module";
2
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { resolveWorkspacePackageFile } from "./workspace-packages.js";
5
+
6
+ const builtinModuleNames = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
7
+ const alwaysAllowedPackages = new Set([
8
+ "@reckona/mreact",
9
+ "@reckona/mreact-auth",
10
+ "@reckona/mreact-query",
11
+ "@reckona/mreact-reactive-core",
12
+ "@reckona/mreact-router",
13
+ ]);
14
+
15
+ export interface AppRouterImportPolicy {
16
+ allowedPackages?: readonly string[] | undefined;
17
+ allowedSourceDirs?: readonly string[] | undefined;
18
+ projectRoot?: string | undefined;
19
+ }
20
+
21
+ export interface AppRouterImportPolicyPluginOptions {
22
+ allowedSourceDirs?: readonly string[] | undefined;
23
+ appDir: string;
24
+ importPolicy?: AppRouterImportPolicy | undefined;
25
+ label: string;
26
+ projectRoot?: string | undefined;
27
+ }
28
+
29
+ export function createAppRouterImportPolicyPlugin(options: AppRouterImportPolicyPluginOptions) {
30
+ const projectRoot = resolve(
31
+ options.importPolicy?.projectRoot ?? options.projectRoot ?? options.appDir,
32
+ );
33
+ const configuredAllowedSourceDirs =
34
+ options.importPolicy?.allowedSourceDirs ?? options.allowedSourceDirs;
35
+ const allowedSourceDirs = (configuredAllowedSourceDirs ?? [options.appDir]).map((directory) =>
36
+ resolve(projectRoot, directory),
37
+ );
38
+ const allowedPackages = new Set([
39
+ ...alwaysAllowedPackages,
40
+ ...(options.importPolicy?.allowedPackages ?? []),
41
+ ]);
42
+ const customAllowedSourceDirs = configuredAllowedSourceDirs !== undefined;
43
+
44
+ return {
45
+ name: `mreact-router-${options.label}-import-policy`,
46
+ setup(buildApi: {
47
+ onResolve(
48
+ options: { filter: RegExp },
49
+ callback: (args: {
50
+ path: string;
51
+ resolveDir: string;
52
+ }) => { errors?: Array<{ text: string }>; external?: boolean; path?: string } | undefined,
53
+ ): void;
54
+ }) {
55
+ buildApi.onResolve({ filter: /.*/ }, (args) => {
56
+ if (isRelativeImport(args.path)) {
57
+ const baseDir = args.resolveDir === "" ? options.appDir : args.resolveDir;
58
+ if (!isInsideAnyDirectory(allowedSourceDirs, baseDir)) {
59
+ return undefined;
60
+ }
61
+
62
+ const resolvedPath = resolve(baseDir, args.path);
63
+
64
+ return !isInsideDirectory(projectRoot, resolvedPath) ||
65
+ !isInsideAnyDirectory(allowedSourceDirs, resolvedPath)
66
+ ? {
67
+ errors: [
68
+ {
69
+ text: customAllowedSourceDirs
70
+ ? `${options.label} imports must stay inside allowed source directories: ${args.path}`
71
+ : `${options.label} imports must stay inside the app directory: ${args.path}`,
72
+ },
73
+ ],
74
+ }
75
+ : undefined;
76
+ }
77
+
78
+ if (isAbsoluteOrProtocolImport(args.path)) {
79
+ return undefined;
80
+ }
81
+
82
+ if (builtinModuleNames.has(args.path)) {
83
+ return { external: true, path: args.path };
84
+ }
85
+
86
+ const workspacePath = workspacePackagePath(args.path, args.resolveDir);
87
+ if (workspacePath !== undefined) {
88
+ return { path: workspacePath };
89
+ }
90
+
91
+ if (args.resolveDir !== "" && !isInsideAnyDirectory(allowedSourceDirs, args.resolveDir)) {
92
+ return undefined;
93
+ }
94
+
95
+ const packageName = packageNameForSpecifier(args.path);
96
+
97
+ if (!allowedPackages.has(packageName)) {
98
+ return {
99
+ errors: [
100
+ {
101
+ text: importPolicyPackageError(options.label, packageName),
102
+ },
103
+ ],
104
+ };
105
+ }
106
+
107
+ const resolvedPackage = resolvePackageSpecifier(args.path, args.resolveDir, options.appDir);
108
+
109
+ return resolvedPackage === undefined
110
+ ? undefined
111
+ : { external: true, path: pathToFileURL(resolvedPackage).href };
112
+ });
113
+ },
114
+ };
115
+ }
116
+
117
+ function resolvePackageSpecifier(
118
+ specifier: string,
119
+ resolveDir: string,
120
+ appDir: string,
121
+ ): string | undefined {
122
+ const baseDir = resolveDir === "" ? appDir : resolveDir;
123
+
124
+ try {
125
+ return createRequire(join(baseDir, "__mreact_resolve__.cjs")).resolve(specifier);
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ function importPolicyPackageError(label: string, packageName: string): string {
132
+ const normalizedLabel = label.toLowerCase();
133
+ const moduleKind =
134
+ normalizedLabel.includes("loader") ||
135
+ normalizedLabel.includes("middleware") ||
136
+ normalizedLabel.includes("route") ||
137
+ normalizedLabel.includes("metadata") ||
138
+ normalizedLabel.includes("action")
139
+ ? normalizedLabel
140
+ : `${normalizedLabel} module`;
141
+
142
+ return [
143
+ `"${packageName}" is imported by a ${moduleKind} but is not allowed by the app-router import policy.`,
144
+ "",
145
+ "The policy blocks server-side static imports from loader, middleware, route handler, metadata, and server action modules unless the package is explicitly allowed.",
146
+ "",
147
+ "Allow it in the Vite plugin config:",
148
+ " mreactRouter({",
149
+ ` importPolicy: { allowedPackages: [${JSON.stringify(packageName)}] },`,
150
+ " })",
151
+ "",
152
+ "For production builds, also declare the package in package.json dependencies so mreact-router build can derive the generated policy and the package is installed in the runtime artifact.",
153
+ ].join("\n");
154
+ }
155
+
156
+ function workspacePackagePath(specifier: string, resolveDir: string): string | undefined {
157
+ const currentDir = dirname(fileURLToPath(import.meta.url));
158
+ const packageRoot = dirname(currentDir);
159
+ const entries = new Map<
160
+ string,
161
+ string | { entry: string; monorepoDir: string; packageName: string }
162
+ >([
163
+ [
164
+ "@reckona/mreact-auth",
165
+ { entry: "index", monorepoDir: "auth", packageName: "@reckona/mreact-auth" },
166
+ ],
167
+ [
168
+ "@reckona/mreact-compiler",
169
+ { entry: "index", monorepoDir: "compiler", packageName: "@reckona/mreact-compiler" },
170
+ ],
171
+ [
172
+ "@reckona/mreact-query",
173
+ { entry: "index", monorepoDir: "query", packageName: "@reckona/mreact-query" },
174
+ ],
175
+ [
176
+ "@reckona/mreact-reactive-core",
177
+ {
178
+ entry: "index",
179
+ monorepoDir: "reactive-core",
180
+ packageName: "@reckona/mreact-reactive-core",
181
+ },
182
+ ],
183
+ [
184
+ "@reckona/mreact-router",
185
+ join(packageRoot, currentDir.endsWith(`${sep}dist`) ? "dist/index.js" : "src/index.ts"),
186
+ ],
187
+ [
188
+ "@reckona/mreact-router/app-router-globals",
189
+ join(
190
+ packageRoot,
191
+ currentDir.endsWith(`${sep}dist`)
192
+ ? "dist/app-router-globals.js"
193
+ : "src/app-router-globals.ts",
194
+ ),
195
+ ],
196
+ [
197
+ "@reckona/mreact-router/link",
198
+ join(packageRoot, currentDir.endsWith(`${sep}dist`) ? "dist/link.js" : "src/link.ts"),
199
+ ],
200
+ [
201
+ "@reckona/mreact-router/navigation-state",
202
+ join(
203
+ packageRoot,
204
+ currentDir.endsWith(`${sep}dist`) ? "dist/navigation-state.js" : "src/navigation-state.ts",
205
+ ),
206
+ ],
207
+ [
208
+ "@reckona/mreact-router/stream-list",
209
+ join(
210
+ packageRoot,
211
+ currentDir.endsWith(`${sep}dist`) ? "dist/stream-list.js" : "src/stream-list.ts",
212
+ ),
213
+ ],
214
+ [
215
+ "@reckona/mreact-server",
216
+ { entry: "index", monorepoDir: "server", packageName: "@reckona/mreact-server" },
217
+ ],
218
+ ]);
219
+ const entry = entries.get(specifier);
220
+
221
+ if (entry === undefined || typeof entry === "string") {
222
+ return entry;
223
+ }
224
+
225
+ return resolveWorkspacePackageFile({
226
+ currentFileUrl: import.meta.url,
227
+ entry: entry.entry,
228
+ monorepoDir: entry.monorepoDir,
229
+ packageName: entry.packageName,
230
+ resolveDir,
231
+ specifier,
232
+ });
233
+ }
234
+
235
+ function isRelativeImport(path: string): boolean {
236
+ return path === "." || path === ".." || path.startsWith("./") || path.startsWith("../");
237
+ }
238
+
239
+ function isAbsoluteOrProtocolImport(path: string): boolean {
240
+ return path.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(path);
241
+ }
242
+
243
+ function packageNameForSpecifier(specifier: string): string {
244
+ if (!specifier.startsWith("@")) {
245
+ return specifier.split("/")[0] ?? specifier;
246
+ }
247
+
248
+ const [scope, name] = specifier.split("/");
249
+
250
+ return scope !== undefined && name !== undefined ? `${scope}/${name}` : specifier;
251
+ }
252
+
253
+ function isInsideDirectory(directory: string, candidate: string): boolean {
254
+ const relativePath = relative(directory, candidate);
255
+
256
+ return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.startsWith(sep));
257
+ }
258
+
259
+ function isInsideAnyDirectory(directories: readonly string[], candidate: string): boolean {
260
+ return directories.some((directory) => isInsideDirectory(directory, candidate));
261
+ }
package/src/index.ts ADDED
@@ -0,0 +1,221 @@
1
+ export { buildApp, packageAwsLambdaArtifact } from "./build.js";
2
+ export { assetHref, assetPreloadLinks } from "./assets.js";
3
+ export { cacheControl, createMemoryRouteCache, revalidatePath } from "./cache.js";
4
+ export { deleteCookie, parseCookieHeader, serializeCookie, setCookie } from "./cookies.js";
5
+ export { defineMessages, detectLocale } from "./i18n.js";
6
+ export { defer, isDeferredLoaderData } from "./deferred.js";
7
+ export type { DeferredLoaderData } from "./deferred.js";
8
+ export { Link, linkProps } from "./link.js";
9
+ export { parseMultipartStream } from "./multipart.js";
10
+ export type {
11
+ MultipartFixedLengthStream,
12
+ MultipartStreamFieldOptions,
13
+ MultipartStreamParseOptions,
14
+ MultipartStreamPart,
15
+ } from "./multipart.js";
16
+ export { getNavigationState, subscribeNavigationState } from "./navigation-state.js";
17
+ export { getRouterRuntimeCacheStats } from "./runtime-cache.js";
18
+ export type { HttpUpgradeHandler } from "./upgrade.js";
19
+ export {
20
+ cookies,
21
+ headers,
22
+ html,
23
+ isNotFoundError,
24
+ isRedirectError,
25
+ json,
26
+ next,
27
+ notFound,
28
+ parseForm,
29
+ redirect,
30
+ redirect303,
31
+ redirectExternal,
32
+ rewrite,
33
+ textError,
34
+ } from "./navigation.js";
35
+ export type { ParseSchema } from "./navigation.js";
36
+ export { createMemoryPrerenderStore } from "./prerender-store.js";
37
+ export { getServerRuntimeState } from "./runtime-state.js";
38
+ import {
39
+ createMemorySessionStore as createMemorySessionStoreInternal,
40
+ createSession as createSessionInternal,
41
+ destroySession as destroySessionInternal,
42
+ getSession as getSessionInternal,
43
+ rotateSession as rotateSessionInternal,
44
+ } from "./session.js";
45
+ import type {
46
+ MemorySessionStoreOptions as MemorySessionStoreOptionsInternal,
47
+ SessionCookieOptions as SessionCookieOptionsInternal,
48
+ SessionRecord as SessionRecordInternal,
49
+ SessionStore as SessionStoreInternal,
50
+ } from "./session.js";
51
+
52
+ /**
53
+ * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
54
+ */
55
+ export const createMemorySessionStore = createMemorySessionStoreInternal;
56
+ /**
57
+ * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
58
+ */
59
+ export const createSession = createSessionInternal;
60
+ /**
61
+ * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
62
+ */
63
+ export const destroySession = destroySessionInternal;
64
+ /**
65
+ * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
66
+ */
67
+ export const getSession = getSessionInternal;
68
+ /**
69
+ * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
70
+ */
71
+ export const rotateSession = rotateSessionInternal;
72
+ export type {
73
+ AwsLambdaArtifactManifest,
74
+ BuildAppOptions,
75
+ BuildAppResult,
76
+ BuiltImportPolicyArtifact,
77
+ PackageAwsLambdaArtifactOptions,
78
+ } from "./build.js";
79
+ export type {
80
+ InferLoaderData,
81
+ LayoutProps,
82
+ LoaderContext,
83
+ GenerateMetadataContext,
84
+ ManifestContext,
85
+ ManifestDescriptor,
86
+ MetadataImage,
87
+ MetadataScalar,
88
+ MetadataThemeColor,
89
+ MetadataViewport,
90
+ MReactNode,
91
+ PageProps,
92
+ RobotsContext,
93
+ RobotsManifest,
94
+ RobotsRule,
95
+ RouteHeadDescriptor,
96
+ RouteHandlerContext,
97
+ RouteMetadata,
98
+ RouteParams,
99
+ RouteSecurityHeaders,
100
+ RouteStrictTransportSecurity,
101
+ SitemapContext,
102
+ SitemapEntry,
103
+ } from "./types.js";
104
+ export type {
105
+ AppRouterBuildTarget,
106
+ AppRouterClientSourceMapMode,
107
+ AppRouterClientSourceMapOption,
108
+ } from "./config.js";
109
+ export type {
110
+ AssetHelperOptions,
111
+ AssetLinkDescriptor,
112
+ AssetManifest,
113
+ AssetManifestEntry,
114
+ } from "./assets.js";
115
+ export type {
116
+ AppRouterCache,
117
+ AppRouterCacheEntry,
118
+ CacheControlOptions,
119
+ MemoryRouteCacheOptions,
120
+ RouteCachePolicy,
121
+ } from "./cache.js";
122
+ export type { CookieOptions } from "./cookies.js";
123
+ export type { AppRouterImportPolicy } from "./import-policy.js";
124
+ export type {
125
+ LinkOptions,
126
+ LinkPrefetch,
127
+ LinkProps,
128
+ LinkScroll,
129
+ LinkTransition,
130
+ } from "./link.js";
131
+ export type {
132
+ AppRouterNavigationState,
133
+ AppRouterNavigationStateListener,
134
+ AppRouterNavigationType,
135
+ } from "./navigation-state.js";
136
+ export type { RouterRuntimeCacheStat } from "./runtime-cache.js";
137
+ export type { MemorySessionStoreOptionsInternal as MemorySessionStoreOptions };
138
+ export type {
139
+ AppRouterLogError,
140
+ AppRouterLogEvent,
141
+ AppRouterLogger,
142
+ AppRouterLogLevel,
143
+ AppRouterRuntime,
144
+ AppRouterRequestEndLogEvent,
145
+ AppRouterRequestErrorLogEvent,
146
+ AppRouterRenderTimingLogEvent,
147
+ AppRouterRequestStartLogEvent,
148
+ AppRouterRequestTimingLogEvent,
149
+ } from "./logger.js";
150
+ export type { DetectedLocale, LocaleRoutingOptions, MessageTree } from "./i18n.js";
151
+ export {
152
+ createFormCsrfToken,
153
+ formCsrfCookie,
154
+ formCsrfFieldName,
155
+ validateFormCsrf,
156
+ } from "./csrf.js";
157
+ export type {
158
+ AppRouterAllowedServerAction,
159
+ AppRouterServerActionOptions,
160
+ } from "./actions.js";
161
+ /**
162
+ * @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
163
+ */
164
+ export type SessionCookieOptions = SessionCookieOptionsInternal;
165
+ /**
166
+ * @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
167
+ */
168
+ export type SessionRecord<TData = unknown> = SessionRecordInternal<TData>;
169
+ /**
170
+ * @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
171
+ */
172
+ export type SessionStore<TData = unknown> = SessionStoreInternal<TData>;
173
+ export { startDevServer } from "./dev-server.js";
174
+ export type { StartDevServerOptions } from "./dev-server.js";
175
+ export { renderAppRequest } from "./render.js";
176
+ export type {
177
+ AppRouterResponseHook,
178
+ AppRouterResponseHookContext,
179
+ RenderAppRequestOptions,
180
+ } from "./render.js";
181
+ export {
182
+ parseTraceContext,
183
+ traceContextFromRequest,
184
+ } from "./trace.js";
185
+ export type {
186
+ RouterInstrumentation,
187
+ RouterMiddlewareEndInstrumentationEvent,
188
+ RouterMiddlewareInstrumentationEvent,
189
+ RouterRequestEndInstrumentationEvent,
190
+ RouterRequestInstrumentationEvent,
191
+ RouterRouteEndInstrumentationEvent,
192
+ RouterRouteInstrumentationEvent,
193
+ RouterTraceContext,
194
+ } from "./trace.js";
195
+ export type {
196
+ FileSystemPrerenderStoreOptions,
197
+ KeyValuePrerenderStoreAdapter,
198
+ KeyValuePrerenderStoreOptions,
199
+ MemoryPrerenderStoreOptions,
200
+ } from "./prerender-store.js";
201
+ export { createFileSystemPrerenderStore, createKeyValuePrerenderStore } from "./prerender-store.js";
202
+ export { preloadBuiltAppRuntime, renderBuiltAppRequest, startServer } from "./serve.js";
203
+ export type {
204
+ BuiltAppRuntimePreloadMode,
205
+ AppRouterPrerenderStore,
206
+ BuiltAppRuntimePreloadStrategy,
207
+ RenderBuiltAppRequestOptions,
208
+ RequestHostPolicy,
209
+ StartServerOptions,
210
+ } from "./serve.js";
211
+ export { matchRoute, scanAppRoutes } from "./routes.js";
212
+ export type { AppFileConvention } from "./file-conventions.js";
213
+ export type {
214
+ AppAssetRoute,
215
+ AppMetadataRoute,
216
+ AppRoute,
217
+ MatchedRoute,
218
+ PageRoute,
219
+ RouteSegment,
220
+ ServerRoute,
221
+ } from "./routes.js";
package/src/link.ts ADDED
@@ -0,0 +1,47 @@
1
+ import {
2
+ createElement,
3
+ type ReactCompatElement,
4
+ type ReactCompatNode,
5
+ } from "@reckona/mreact-compat";
6
+
7
+ export type LinkPrefetch = "intent" | "viewport" | "none" | false;
8
+ export type LinkScroll = "top" | "preserve";
9
+ export type LinkTransition = "auto" | "none" | false;
10
+
11
+ export interface LinkOptions {
12
+ href: string;
13
+ prefetch?: LinkPrefetch | undefined;
14
+ reload?: boolean | undefined;
15
+ scroll?: LinkScroll | undefined;
16
+ transition?: LinkTransition | undefined;
17
+ }
18
+
19
+ export interface LinkProps extends LinkOptions {
20
+ children?: ReactCompatNode;
21
+ [attribute: string]: unknown;
22
+ }
23
+
24
+ export function linkProps(options: LinkOptions): Record<string, string> {
25
+ return {
26
+ href: options.href,
27
+ ...(options.prefetch === undefined || options.prefetch === "intent"
28
+ ? {}
29
+ : { "data-mreact-prefetch": options.prefetch === false ? "none" : options.prefetch }),
30
+ ...(options.reload === true ? { "data-mreact-reload": "true" } : {}),
31
+ ...(options.scroll === undefined || options.scroll === "top"
32
+ ? {}
33
+ : { "data-mreact-scroll": options.scroll }),
34
+ ...(options.transition === undefined || options.transition === false || options.transition === "none"
35
+ ? {}
36
+ : { "data-mreact-transition": options.transition }),
37
+ };
38
+ }
39
+
40
+ export function Link(props: LinkProps): ReactCompatElement {
41
+ const { href, prefetch, reload, scroll, transition, ...rest } = props;
42
+
43
+ return createElement("a", {
44
+ ...rest,
45
+ ...linkProps({ href, prefetch, reload, scroll, transition }),
46
+ });
47
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,149 @@
1
+ export type AppRouterLogLevel = "debug" | "info" | "warn" | "error";
2
+
3
+ export type AppRouterRuntime = "aws-lambda" | "cloudflare" | "edge" | "node";
4
+
5
+ export interface AppRouterLogger {
6
+ debug?: ((event: AppRouterLogEvent) => void | Promise<void>) | undefined;
7
+ error?: ((event: AppRouterLogEvent) => void | Promise<void>) | undefined;
8
+ info?: ((event: AppRouterLogEvent) => void | Promise<void>) | undefined;
9
+ warn?: ((event: AppRouterLogEvent) => void | Promise<void>) | undefined;
10
+ }
11
+
12
+ export type AppRouterLogEvent =
13
+ | AppRouterRequestStartLogEvent
14
+ | AppRouterRequestEndLogEvent
15
+ | AppRouterRequestErrorLogEvent
16
+ | AppRouterRequestTimingLogEvent
17
+ | AppRouterRenderTimingLogEvent;
18
+
19
+ export interface AppRouterRequestStartLogEvent {
20
+ method: string;
21
+ path: string;
22
+ runtime: AppRouterRuntime;
23
+ type: "router:request:start";
24
+ }
25
+
26
+ export interface AppRouterRequestEndLogEvent {
27
+ durationMs: number;
28
+ method: string;
29
+ path: string;
30
+ runtime: AppRouterRuntime;
31
+ status: number;
32
+ type: "router:request:end";
33
+ }
34
+
35
+ export interface AppRouterRequestErrorLogEvent {
36
+ durationMs: number;
37
+ error: AppRouterLogError;
38
+ method: string;
39
+ path: string;
40
+ runtime: AppRouterRuntime;
41
+ type: "router:request:error";
42
+ }
43
+
44
+ export interface AppRouterRequestTimingLogEvent {
45
+ durationMs: number;
46
+ method: string;
47
+ path: string;
48
+ phases: Record<string, number>;
49
+ runtime: AppRouterRuntime;
50
+ status: number;
51
+ type: "router:request:timing";
52
+ }
53
+
54
+ export interface AppRouterRenderTimingLogEvent {
55
+ method: string;
56
+ path: string;
57
+ phases: Record<string, number>;
58
+ status: number;
59
+ type: "router:render:timing";
60
+ }
61
+
62
+ export interface AppRouterLogError {
63
+ message: string;
64
+ name: string;
65
+ }
66
+
67
+ export interface RouterRequestLogFields {
68
+ method: string;
69
+ path: string;
70
+ runtime: AppRouterRuntime;
71
+ }
72
+
73
+ export function emitRouterLog(
74
+ logger: AppRouterLogger | undefined,
75
+ level: AppRouterLogLevel,
76
+ event: AppRouterLogEvent,
77
+ ): void {
78
+ const sink = logger?.[level];
79
+
80
+ if (sink === undefined) {
81
+ return;
82
+ }
83
+
84
+ queueMicrotask(() => {
85
+ try {
86
+ const result = sink(event);
87
+
88
+ if (isPromiseLike(result)) {
89
+ result.catch(() => {});
90
+ }
91
+ } catch {
92
+ // Logger sinks are best-effort and must never affect request handling.
93
+ }
94
+ });
95
+ }
96
+
97
+ function isPromiseLike(value: unknown): value is Promise<void> {
98
+ return (
99
+ typeof value === "object" &&
100
+ value !== null &&
101
+ "catch" in value &&
102
+ typeof value.catch === "function"
103
+ );
104
+ }
105
+
106
+ export function requestLogFields(
107
+ request: Request,
108
+ runtime: AppRouterRuntime,
109
+ ): RouterRequestLogFields {
110
+ return {
111
+ method: request.method,
112
+ path: new URL(request.url).pathname,
113
+ runtime,
114
+ };
115
+ }
116
+
117
+ export function nodeRequestPath(url: string | undefined): string {
118
+ if (url === undefined || url === "") {
119
+ return "/";
120
+ }
121
+
122
+ try {
123
+ return new URL(url, "http://mreact.local").pathname;
124
+ } catch {
125
+ return "/";
126
+ }
127
+ }
128
+
129
+ export function logNow(): number {
130
+ return performance.now();
131
+ }
132
+
133
+ export function logDurationMs(startedAt: number): number {
134
+ return Math.max(0, Math.round((logNow() - startedAt) * 1000) / 1000);
135
+ }
136
+
137
+ export function logError(error: unknown): AppRouterLogError {
138
+ if (error instanceof Error) {
139
+ return {
140
+ message: error.message,
141
+ name: error.name,
142
+ };
143
+ }
144
+
145
+ return {
146
+ message: String(error),
147
+ name: "Error",
148
+ };
149
+ }