@tschk/moonshine-framework 0.4.1 → 0.4.4
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 +36 -0
- package/package.json +7 -3
- package/src/build.ts +136 -0
- package/src/config.ts +1 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +4 -0
- package/src/renderer.ts +16 -0
- package/src/request.ts +12 -0
- package/src/routes.ts +2 -0
- package/test/adapter.test.ts +0 -26
- package/test/config.test.ts +0 -45
- package/test/contracts.test.ts +0 -13
- package/test/serialize.test.ts +0 -69
- package/tsconfig.json +0 -4
package/README.md
CHANGED
|
@@ -19,6 +19,12 @@ const route = defineRoute({
|
|
|
19
19
|
const config = defineConfig({ runtime: "bun", mode: "auto" });
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Native manifests support Bun, Node, Cloudflare, and Vercel Node functions. Route
|
|
23
|
+
runtimes match their adapter, with Vercel native functions using `node`. Vercel
|
|
24
|
+
Edge remains unsupported. Cloudflare and Vercel reject request context, islands,
|
|
25
|
+
and cache/revalidation options; see the [Cloudflare](../deploy-cloudflare/README.md)
|
|
26
|
+
and [Vercel](../deploy-vercel/README.md) adapter limits.
|
|
27
|
+
|
|
22
28
|
Exports include:
|
|
23
29
|
|
|
24
30
|
- `RouteDefinition`, `RenderMode`, `RuntimeTarget`
|
|
@@ -28,3 +34,33 @@ Exports include:
|
|
|
28
34
|
|
|
29
35
|
See [docs/MANIFEST.md](../../docs/MANIFEST.md) and
|
|
30
36
|
[DESIGN.md](../../DESIGN.md).
|
|
37
|
+
|
|
38
|
+
Server request context is opt-in with `requestContext: true` and requires matching
|
|
39
|
+
Bun or Node deployment adapters and route runtimes. Import `runWithRequest` and
|
|
40
|
+
`getRequest` from `@tschk/moonshine-framework/request`; this server-only subpath
|
|
41
|
+
uses AsyncLocalStorage. The root export contains no request-context runtime.
|
|
42
|
+
`RequestScope` is available as a root type for pipeline integration.
|
|
43
|
+
|
|
44
|
+
Native page and layout server modules may export `metadata`, typed as `Metadata`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import type { Metadata } from "@tschk/moonshine-framework";
|
|
48
|
+
|
|
49
|
+
export const metadata: Metadata = ({ data }) => ({
|
|
50
|
+
title: (data as { title: string }).title,
|
|
51
|
+
description: "A server-rendered page",
|
|
52
|
+
lang: "en",
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`DocumentMetadata` supports only `title`, `description`, and `lang`. Outer layouts,
|
|
57
|
+
inner layouts, then the page override individual fields. Undefined inherits; null
|
|
58
|
+
clears an inherited field (including the default `lang="en"`). Values are escaped
|
|
59
|
+
in the document head and HTML language attribute. Callbacks must be synchronous,
|
|
60
|
+
pure functions of the render context, which contains already-loaded route data;
|
|
61
|
+
Promises and invalid field types are rejected. No arbitrary head HTML is accepted.
|
|
62
|
+
|
|
63
|
+
Metadata applies to native SSR, static output, and SPA document shells without
|
|
64
|
+
mounting the SPA server component. API responses skip it. This adds no client head
|
|
65
|
+
manager: subsequent client-side navigation must update document metadata explicitly.
|
|
66
|
+
Legacy React rendering does not consume this export.
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tschk/moonshine-framework",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./src/index.ts"
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./request": "./src/request.ts"
|
|
10
11
|
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
11
15
|
"scripts": {
|
|
12
16
|
"format": "bunx prettier --check .",
|
|
13
17
|
"lint": "bunx oxlint .",
|
|
@@ -15,7 +19,7 @@
|
|
|
15
19
|
"test": "bun test"
|
|
16
20
|
},
|
|
17
21
|
"dependencies": {
|
|
18
|
-
"@tschk/moonshine": "^0.4.
|
|
22
|
+
"@tschk/moonshine": "^0.4.4"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"@types/bun": "1.3.14",
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { copyFile, mkdir, realpath } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import type { DeploymentAdapter } from "./adapter";
|
|
4
|
+
import type { MoonshineManifest } from "./manifest";
|
|
5
|
+
|
|
6
|
+
export function resolveManifestFiles(
|
|
7
|
+
manifest: MoonshineManifest,
|
|
8
|
+
projectRoot: string,
|
|
9
|
+
): MoonshineManifest {
|
|
10
|
+
return {
|
|
11
|
+
...manifest,
|
|
12
|
+
routes: manifest.routes.map((route) => ({
|
|
13
|
+
...route,
|
|
14
|
+
file: isAbsolute(route.file)
|
|
15
|
+
? route.file
|
|
16
|
+
: resolve(projectRoot, route.file),
|
|
17
|
+
...(route.dataFile && {
|
|
18
|
+
dataFile: isAbsolute(route.dataFile)
|
|
19
|
+
? route.dataFile
|
|
20
|
+
: resolve(projectRoot, route.dataFile),
|
|
21
|
+
}),
|
|
22
|
+
...(route.clientFile && {
|
|
23
|
+
clientFile: isAbsolute(route.clientFile)
|
|
24
|
+
? route.clientFile
|
|
25
|
+
: resolve(projectRoot, route.clientFile),
|
|
26
|
+
}),
|
|
27
|
+
...(route.layouts && {
|
|
28
|
+
layouts: route.layouts.map((layout) =>
|
|
29
|
+
isAbsolute(layout) ? layout : resolve(projectRoot, layout),
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
...(route.middleware && {
|
|
33
|
+
middleware: route.middleware.map((middleware) =>
|
|
34
|
+
isAbsolute(middleware)
|
|
35
|
+
? middleware
|
|
36
|
+
: resolve(projectRoot, middleware),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
...(route.errorBoundary && {
|
|
40
|
+
errorBoundary: isAbsolute(route.errorBoundary)
|
|
41
|
+
? route.errorBoundary
|
|
42
|
+
: resolve(projectRoot, route.errorBoundary),
|
|
43
|
+
}),
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function within(root: string, file: string): boolean {
|
|
49
|
+
const path = relative(root, file);
|
|
50
|
+
return path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function copyAsset(
|
|
54
|
+
asset: MoonshineManifest["assets"][number],
|
|
55
|
+
outDir: string,
|
|
56
|
+
): Promise<string> {
|
|
57
|
+
const source = isAbsolute(asset.file)
|
|
58
|
+
? asset.file
|
|
59
|
+
: resolve(outDir, asset.file);
|
|
60
|
+
const publicRoot = resolve(outDir, "public");
|
|
61
|
+
const [realSource, realPublic] = await Promise.all([
|
|
62
|
+
realpath(source),
|
|
63
|
+
realpath(publicRoot).catch(() => publicRoot),
|
|
64
|
+
]);
|
|
65
|
+
if (within(publicRoot, source) && !within(realPublic, realSource))
|
|
66
|
+
throw new Error(`Public asset escapes its directory: ${asset.file}`);
|
|
67
|
+
if (within(realPublic, realSource)) {
|
|
68
|
+
const stored = within(publicRoot, source)
|
|
69
|
+
? relative(publicRoot, source)
|
|
70
|
+
: relative(realPublic, realSource);
|
|
71
|
+
return `public/${stored.split(sep).join("/")}`;
|
|
72
|
+
}
|
|
73
|
+
const target = resolve(
|
|
74
|
+
outDir,
|
|
75
|
+
(asset.path ?? asset.file).replace(/^\/+/, ""),
|
|
76
|
+
);
|
|
77
|
+
await mkdir(dirname(target), { recursive: true });
|
|
78
|
+
if (source !== target) {
|
|
79
|
+
await copyFile(source, target);
|
|
80
|
+
}
|
|
81
|
+
return (asset.path ?? asset.file).replace(/^\/+/, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function copyAssets(
|
|
85
|
+
assets: MoonshineManifest["assets"],
|
|
86
|
+
outDir: string,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
await Promise.all(assets.map((asset) => copyAsset(asset, outDir)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function validateRuntime(
|
|
92
|
+
adapter: DeploymentAdapter,
|
|
93
|
+
manifest: MoonshineManifest,
|
|
94
|
+
): void {
|
|
95
|
+
const nativeRuntime = adapter.name === "vercel" ? "node" : adapter.name;
|
|
96
|
+
if (
|
|
97
|
+
manifest.renderer === "native" &&
|
|
98
|
+
(!["bun", "node", "cloudflare", "vercel"].includes(adapter.name) ||
|
|
99
|
+
manifest.routes.some((route) => route.runtime !== nativeRuntime))
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Native renderer requires matching Bun, Node or Cloudflare runtimes; Vercel native functions require Node",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
manifest.requestContext &&
|
|
107
|
+
(!["bun", "node"].includes(adapter.name) ||
|
|
108
|
+
manifest.routes.some((route) => route.runtime !== adapter.name))
|
|
109
|
+
) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"Request context supports only matching Bun or Node deployment adapters/runtimes",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const errors: string[] = [];
|
|
115
|
+
for (const route of manifest.routes) {
|
|
116
|
+
if (!adapter.runtimes.includes(route.runtime)) {
|
|
117
|
+
errors.push(
|
|
118
|
+
`${adapter.name} adapter does not support route "${route.path}" with runtime "${route.runtime}"; supported runtimes: ${adapter.runtimes.join(", ")}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (errors.length) throw new Error(errors.join("\n"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function validateCapabilities(
|
|
126
|
+
adapter: DeploymentAdapter,
|
|
127
|
+
manifest: MoonshineManifest,
|
|
128
|
+
): void {
|
|
129
|
+
for (const capability of manifest.capabilities) {
|
|
130
|
+
if (!adapter.capabilities.includes(capability)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${adapter.name} adapter does not support capability "${capability}"; supported capabilities: ${adapter.capabilities.join(", ")}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/config.ts
CHANGED
package/src/index.ts
CHANGED
package/src/manifest.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RenderMode, RouteDefinition, RuntimeTarget } from "./routes";
|
|
2
|
+
import type { RendererName } from "./renderer";
|
|
2
3
|
|
|
3
4
|
export const MANIFEST_VERSION = 1 as const;
|
|
4
5
|
|
|
@@ -14,6 +15,7 @@ export type RouteArtifact = RouteDefinition & {
|
|
|
14
15
|
decision: string;
|
|
15
16
|
serverEntry?: string;
|
|
16
17
|
clientEntries: string[];
|
|
18
|
+
clientStyles?: string[];
|
|
17
19
|
staticOutput?: string;
|
|
18
20
|
cache?: { control?: string; revalidate?: number };
|
|
19
21
|
headers?: Record<string, string>;
|
|
@@ -22,6 +24,8 @@ export type RouteArtifact = RouteDefinition & {
|
|
|
22
24
|
export type MoonshineManifest = {
|
|
23
25
|
version: typeof MANIFEST_VERSION;
|
|
24
26
|
frameworkVersion: string;
|
|
27
|
+
renderer?: RendererName;
|
|
28
|
+
requestContext?: boolean;
|
|
25
29
|
routes: RouteArtifact[];
|
|
26
30
|
assets: Array<{ path: string; file: string; integrity?: string }>;
|
|
27
31
|
entries: { server?: string; client?: string };
|
package/src/renderer.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RouteArtifact } from "./manifest";
|
|
2
2
|
|
|
3
|
+
export type RendererName = "react" | "solid" | "crepus" | "native";
|
|
4
|
+
|
|
3
5
|
export type RenderContext = {
|
|
4
6
|
request: Request;
|
|
5
7
|
route: RouteArtifact;
|
|
@@ -8,8 +10,22 @@ export type RenderContext = {
|
|
|
8
10
|
signal: AbortSignal;
|
|
9
11
|
};
|
|
10
12
|
|
|
13
|
+
/** Native document fields. Undefined inherits; null removes an inherited field. */
|
|
14
|
+
export type DocumentMetadata = {
|
|
15
|
+
title?: string | null;
|
|
16
|
+
description?: string | null;
|
|
17
|
+
lang?: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Synchronous server-module metadata, evaluated with loaded route data. */
|
|
21
|
+
export type Metadata =
|
|
22
|
+
DocumentMetadata | ((context: RenderContext) => DocumentMetadata);
|
|
23
|
+
|
|
11
24
|
export type Renderer = {
|
|
12
25
|
name: string;
|
|
13
26
|
render(context: RenderContext): Promise<Response>;
|
|
14
27
|
prerender(context: RenderContext): Promise<string>;
|
|
15
28
|
};
|
|
29
|
+
|
|
30
|
+
/** Runs the entire request pipeline in an optional server context. */
|
|
31
|
+
export type RequestScope = <T>(request: Request, fn: () => T) => T;
|
package/src/request.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Opt-in server request context; deliberately absent from the root barrel. */
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import type { RequestScope } from "./renderer";
|
|
4
|
+
|
|
5
|
+
const requests = new AsyncLocalStorage<Request>();
|
|
6
|
+
|
|
7
|
+
export const runWithRequest: RequestScope = (request, fn) =>
|
|
8
|
+
requests.run(request, fn);
|
|
9
|
+
|
|
10
|
+
export function getRequest(): Request | undefined {
|
|
11
|
+
return requests.getStore();
|
|
12
|
+
}
|
package/src/routes.ts
CHANGED
package/test/adapter.test.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { defineRoute } from "../src/adapter";
|
|
3
|
-
import type { RouteDefinition } from "../src/routes";
|
|
4
|
-
|
|
5
|
-
describe("defineRoute", () => {
|
|
6
|
-
test("returns the exact route definition provided", () => {
|
|
7
|
-
const route: RouteDefinition = {
|
|
8
|
-
id: "test-route",
|
|
9
|
-
path: "/test",
|
|
10
|
-
file: "test.tsx",
|
|
11
|
-
mode: "ssr",
|
|
12
|
-
runtime: "bun",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const result = defineRoute(route);
|
|
16
|
-
|
|
17
|
-
expect(result).toBe(route);
|
|
18
|
-
expect(result).toEqual({
|
|
19
|
-
id: "test-route",
|
|
20
|
-
path: "/test",
|
|
21
|
-
file: "test.tsx",
|
|
22
|
-
mode: "ssr",
|
|
23
|
-
runtime: "bun",
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
});
|
package/test/config.test.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { defineConfig, type MoonshineConfig } from "../src/config";
|
|
3
|
-
|
|
4
|
-
describe("defineConfig", () => {
|
|
5
|
-
test("returns the exact same object reference", () => {
|
|
6
|
-
const config: MoonshineConfig = {};
|
|
7
|
-
const result = defineConfig(config);
|
|
8
|
-
expect(result).toBe(config);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("handles empty configuration", () => {
|
|
12
|
-
const result = defineConfig({});
|
|
13
|
-
expect(result).toEqual({});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("handles configuration with mode", () => {
|
|
17
|
-
const result = defineConfig({ mode: "island" });
|
|
18
|
-
expect(result).toEqual({ mode: "island" });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("handles configuration with runtime", () => {
|
|
22
|
-
const result = defineConfig({ runtime: "bun" });
|
|
23
|
-
expect(result).toEqual({ runtime: "bun" });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("handles full configuration", () => {
|
|
27
|
-
const config = {
|
|
28
|
-
mode: "ssr" as const,
|
|
29
|
-
runtime: "cloudflare" as const,
|
|
30
|
-
};
|
|
31
|
-
const result = defineConfig(config);
|
|
32
|
-
expect(result).toEqual(config);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("preserves additional properties (type safety with generic T)", () => {
|
|
36
|
-
const config = {
|
|
37
|
-
mode: "spa" as const,
|
|
38
|
-
customPlugin: true,
|
|
39
|
-
};
|
|
40
|
-
const result = defineConfig(config);
|
|
41
|
-
expect(result).toEqual(config);
|
|
42
|
-
// TypeScript check: result.customPlugin should exist
|
|
43
|
-
expect(result.customPlugin).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
});
|
package/test/contracts.test.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { defineConfig, defineRoute, MANIFEST_VERSION } from "../src";
|
|
3
|
-
|
|
4
|
-
test("contracts preserve exact input and version one", () => {
|
|
5
|
-
const route = defineRoute({
|
|
6
|
-
id: "home",
|
|
7
|
-
path: "/",
|
|
8
|
-
file: "src/routes/index.tsx",
|
|
9
|
-
});
|
|
10
|
-
expect(route.path).toBe("/");
|
|
11
|
-
expect(defineConfig({ mode: "auto" }).mode).toBe("auto");
|
|
12
|
-
expect(MANIFEST_VERSION).toBe(1);
|
|
13
|
-
});
|
package/test/serialize.test.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { serializeData } from "../src/serialize";
|
|
3
|
-
|
|
4
|
-
// The single HTML-safe JSON serializer: the server data payload and the island
|
|
5
|
-
// props script are the same XSS-prevention primitive, so they share this one
|
|
6
|
-
// implementation rather than drifting apart in two packages.
|
|
7
|
-
describe("serializeData", () => {
|
|
8
|
-
test("escapes <, >, &, U+2028, and U+2029", () => {
|
|
9
|
-
const out = serializeData({
|
|
10
|
-
html: "<script>alert('&
')</script>",
|
|
11
|
-
});
|
|
12
|
-
expect(out).not.toContain("<");
|
|
13
|
-
expect(out).not.toContain(">");
|
|
14
|
-
expect(out).not.toContain("&");
|
|
15
|
-
expect(out).not.toContain("
");
|
|
16
|
-
expect(out).not.toContain("
");
|
|
17
|
-
expect(out).toContain("\\u003c");
|
|
18
|
-
expect(out).toContain("\\u003e");
|
|
19
|
-
expect(out).toContain("\\u0026");
|
|
20
|
-
expect(out).toContain("\\u2028");
|
|
21
|
-
expect(out).toContain("\\u2029");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("omits undefined properties instead of nulling them", () => {
|
|
25
|
-
expect(serializeData({ a: 1, b: undefined })).toBe('{"a":1}');
|
|
26
|
-
expect(serializeData([1, undefined])).toBe("[1,null]");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("rejects symbol-keyed properties rather than dropping them", () => {
|
|
30
|
-
expect(() => serializeData({ [Symbol("k")]: 1 })).toThrow(/symbol/);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("serializes NaN and Infinity as null", () => {
|
|
34
|
-
expect(serializeData({ a: NaN, b: Infinity, c: -Infinity })).toBe(
|
|
35
|
-
'{"a":null,"b":null,"c":null}',
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("allows shared references and rejects only true cycles", () => {
|
|
40
|
-
const shared = { id: 1 };
|
|
41
|
-
expect(serializeData({ a: shared, b: shared })).toBe(
|
|
42
|
-
'{"a":{"id":1},"b":{"id":1}}',
|
|
43
|
-
);
|
|
44
|
-
expect(serializeData([shared, shared])).toBe('[{"id":1},{"id":1}]');
|
|
45
|
-
|
|
46
|
-
const cyclic: Record<string, unknown> = {};
|
|
47
|
-
cyclic.self = cyclic;
|
|
48
|
-
expect(() => serializeData(cyclic)).toThrow(/cyclic/);
|
|
49
|
-
|
|
50
|
-
const arr: unknown[] = [];
|
|
51
|
-
arr.push(arr);
|
|
52
|
-
expect(() => serializeData(arr)).toThrow(/cyclic/);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("rejects functions, symbols, and bigints", () => {
|
|
56
|
-
expect(() => serializeData({ f: () => 1 })).toThrow(/function/);
|
|
57
|
-
expect(() => serializeData({ s: Symbol("x") })).toThrow(/symbol/);
|
|
58
|
-
expect(() => serializeData({ n: 1n })).toThrow(/bigint/);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("omits properties with throwing getters", () => {
|
|
62
|
-
const obj = {
|
|
63
|
-
get thrower() {
|
|
64
|
-
throw new Error("Getter threw!");
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
expect(serializeData(obj)).toBe("{}");
|
|
68
|
-
});
|
|
69
|
-
});
|
package/tsconfig.json
DELETED