vite-ember-ssr 0.1.0-alpha.0

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/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright © 2026 Liam Potter
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # vite-ember-ssr
2
+
3
+ > [!WARNING]
4
+ > **EXPERIMENTAL** — This project is in early development and targets **compatless** Ember apps only (no `@embroider/compat`, no `ember-cli-build.js`, no `classicEmberSupport()`). APIs will change. Do not use in production.
5
+
6
+ Vite plugin and SSR runtime for Ember.js applications. Uses [HappyDOM](https://github.com/capricorn86/happy-dom) for server-side rendering — no FastBoot, no VM sandbox.
7
+
8
+ See the Installation and Setup sections below for installation and copy‑paste integration snippets.
9
+
10
+ ## Architecture
11
+
12
+ - **HappyDOM Window** provides a full per-request browser-like environment. Ember runs directly in the Node.js process with globals swapped per request.
13
+ - **`Application.visit(url)`** drives the entire render cycle server-side.
14
+ - **No hydration yet** — the client boots normally and replaces SSR content. SSR provides the initial visual while JS loads.
15
+ - **Shoebox** — fetch responses captured during SSR are serialized into the HTML and replayed on the client to avoid duplicate API requests.
16
+
17
+ ## Requirements
18
+
19
+ - Ember app built with Embroider in "compatless" mode (no `@embroider/compat`, no `ember-cli-build.js`, and no `classicEmberSupport()`).
20
+ - Your app's `config/environment` must be a direct ES module import (i.e. `import config from './config/environment.ts'`). Do not rely on `<meta>` config injection or `@embroider/config-meta-loader`.
21
+ - Vite 6+
22
+ - Node 22+
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ pnpm add -D vite-ember-ssr
28
+ ```
29
+
30
+ ## Setup
31
+
32
+ ### 1. Vite config
33
+
34
+ ```js
35
+ // vite.config.mjs
36
+ import { defineConfig } from 'vite';
37
+ import { extensions, ember } from '@embroider/vite';
38
+ import { babel } from '@rollup/plugin-babel';
39
+ import { emberSsr } from 'vite-ember-ssr/vite-plugin';
40
+
41
+ export default defineConfig({
42
+ plugins: [
43
+ ember(),
44
+ babel({ babelHelpers: 'runtime', extensions }),
45
+ emberSsr()
46
+ ],
47
+ });
48
+ ```
49
+
50
+ ### 2. HTML template
51
+
52
+ Add SSR markers to `index.html`:
53
+
54
+ ```html
55
+ <!DOCTYPE html>
56
+ <html>
57
+ <head>
58
+ <meta charset="utf-8" />
59
+ <!-- VITE_EMBER_SSR_HEAD -->
60
+ </head>
61
+ <body>
62
+ <!-- VITE_EMBER_SSR_BODY -->
63
+ <script type="module" src="/app/entry.ts"></script>
64
+ </body>
65
+ </html>
66
+ ```
67
+
68
+ ### 3. SSR entry (`app/app-ssr.ts`)
69
+
70
+ Export a factory that creates the Ember app with `autoboot: false`:
71
+
72
+ ```ts
73
+ import EmberApp from 'ember-strict-application-resolver';
74
+ import config from './config/environment.ts';
75
+ import Router from './router.ts';
76
+
77
+ class App extends EmberApp {
78
+ modulePrefix = config.modulePrefix;
79
+ modules = {
80
+ './router': Router,
81
+ ...import.meta.glob('./{routes,templates}/**/*.{ts,gts}', { eager: true }),
82
+ ...import.meta.glob('./services/*.ts', { eager: true }),
83
+ };
84
+ }
85
+
86
+ export function createSsrApp() {
87
+ return App.create({ ...config.APP, autoboot: false });
88
+ }
89
+
90
+ Notes:
91
+ - Place this file in your application package at `app/app-ssr.ts` (the SSR build references that path in examples).
92
+ - `autoboot: false` prevents the Ember application from attempting to boot itself in the server environment; instead we call `Application.visit(url)` to drive rendering.
93
+ - The `modules` map uses `import.meta.glob(..., { eager: true })` to provide the resolver with preloaded route/template/service modules — this mirrors how Embroider/Ember apps load modules at runtime in a static environment.
94
+ - Keep the glob patterns broad enough to include your `routes`, `templates`, and `services` so the resolver can find everything the app needs during SSR.
95
+ ```
96
+
97
+ ### 4. Client entry (`app/entry.ts`)
98
+
99
+ ```ts
100
+ import Application from './app.ts';
101
+ import config from './config/environment.ts';
102
+ import { installShoebox, cleanupSSRContent } from 'vite-ember-ssr/client';
103
+
104
+ installShoebox();
105
+ cleanupSSRContent();
106
+ Application.create(config.APP);
107
+ ```
108
+
109
+ ### 5. Build
110
+
111
+ ```sh
112
+ vite build # client → dist/client
113
+ vite build --ssr app/app-ssr.ts # server → dist/server
114
+ ```
115
+
116
+ ### 6. Server
117
+
118
+ Wire up `render()` in your server's catch-all route. See [examples/fastify.md](https://github.com/evoactivity/vite-ember-ssr/blob/main/examples/fastify.md) for a complete Fastify example with dev and production modes.
119
+
120
+ Shoebox (fetch replay):
121
+
122
+ - Pass `shoebox: true` to `render()` to capture `fetch` responses during SSR and serialize them into the rendered HTML. On the client call `installShoebox()` before boot to replay those responses and avoid duplicate requests.
123
+ - Default: shoebox is opt-in. Use it when your app makes server-side fetch calls that the client would otherwise repeat on first load.
124
+ - Caveats: embedding large API responses increases HTML size; do not serialize sensitive data into the shoebox.
125
+
126
+ ## API
127
+
128
+ ### `vite-ember-ssr/vite-plugin`
129
+
130
+ ```js
131
+ import { emberSsr } from 'vite-ember-ssr/vite-plugin';
132
+ ```
133
+
134
+ Single Vite plugin. Handles all SSR-related configuration:
135
+
136
+ - `ssr.noExternal` for Ember ecosystem packages
137
+ - Build output directories (`dist/client` and `dist/server`)
138
+ - SSR build defaults (`target: 'node22'`, `sourcemap: true`, `minify: false`)
139
+ - Writes `{"type": "module"}` to SSR output directory
140
+
141
+ Options:
142
+
143
+ ```js
144
+ emberSsr({
145
+ clientOutDir: 'dist/client', // default
146
+ serverOutDir: 'dist/server', // default
147
+ additionalNoExternal: ['my-addon'], // extend built-in patterns
148
+ });
149
+ ```
150
+
151
+ ### `vite-ember-ssr/server`
152
+
153
+ ```js
154
+ import { render } from 'vite-ember-ssr/server';
155
+ ```
156
+
157
+ - **`render({ url, template, createApp, shoebox? })`** — render an Ember app and assemble the final HTML. Returns `{ html, statusCode, error }`.
158
+
159
+ Lower-level functions are also exported for advanced use:
160
+
161
+ - **`renderEmberApp({ url, createApp, shoebox? })`** — render only, returns `{ head, body, statusCode, error }`.
162
+ - **`assembleHTML(template, renderResult)`** — replace SSR markers in the HTML template.
163
+
164
+ ### `vite-ember-ssr/client`
165
+
166
+ ```js
167
+ import { installShoebox, cleanupSSRContent, cleanupShoebox, isSSRRendered } from 'vite-ember-ssr/client';
168
+ ```
169
+
170
+ - **`installShoebox()`** — replay server-captured fetch responses, auto-restores `fetch` when all entries consumed.
171
+ - **`cleanupSSRContent()`** — remove SSR-rendered DOM nodes before client Ember boots.
172
+ - **`cleanupShoebox()`** — manually restore original `fetch`.
173
+ - **`isSSRRendered()`** — returns `true` if SSR boundary markers are present in the DOM. Useful for conditionally running client-side setup that should only happen on SSR-rendered pages.
174
+
175
+ ## Monorepo development
176
+
177
+ This repo contains three packages:
178
+
179
+ | Package | Description |
180
+ | ------------------------- | -------------------------------- |
181
+ | `packages/vite-ember-ssr` | Core library |
182
+ | `packages/test-app` | Ember test app |
183
+ | `packages/test-server` | Fastify SSR server + test suites |
184
+
185
+ ```sh
186
+ pnpm install
187
+ pnpm dev # dev server (Fastify + Vite middleware)
188
+ pnpm build # build library + test app
189
+ pnpm demo # build everything, start production server
190
+ pnpm test # vitest SSR tests
191
+ pnpm test:browser # playwright browser tests
192
+ pnpm test:all # both
193
+ ```
194
+
195
+ ## Performance
196
+
197
+ - Server startup: ~1s (no ember-cli build step)
198
+ - First SSR render: ~3s (cold module loading)
199
+ - Warm SSR render: ~24ms
200
+
201
+ ## License
202
+
203
+ MIT
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Client-side utilities for vite-ember-ssr.
3
+ *
4
+ * Currently the client Ember app boots normally and replaces the
5
+ * SSR-rendered content. True DOM hydration is planned for a future
6
+ * phase.
7
+ *
8
+ * For now, the SSR content provides the initial visual while client
9
+ * JavaScript loads, parses, and Ember boots.
10
+ */
11
+ /**
12
+ * Installs the shoebox fetch interceptor on the client.
13
+ *
14
+ * Reads the shoebox data from the server-injected <script> tag,
15
+ * removes the tag from the DOM, and monkey-patches globalThis.fetch
16
+ * to serve cached responses for URLs that match shoebox entries.
17
+ *
18
+ * Each entry is reference-counted: concurrent fetch calls to the same
19
+ * URL all receive the shoebox response. The entry is removed only when
20
+ * the last concurrent consumer has been served.
21
+ *
22
+ * Call this BEFORE creating the Ember application, typically as the
23
+ * first thing in your client entry point.
24
+ *
25
+ * @returns true if shoebox data was found and installed, false otherwise
26
+ */
27
+ export declare function installShoebox(): boolean;
28
+ /**
29
+ * Restores the original fetch function and cleans up shoebox state.
30
+ *
31
+ * Called automatically when all shoebox entries have been consumed,
32
+ * or can be called manually to force cleanup.
33
+ */
34
+ export declare function cleanupShoebox(): void;
35
+ /**
36
+ * Removes the SSR-rendered content from the DOM before the client
37
+ * Ember app boots. This prevents the "double render" where both the
38
+ * server-rendered HTML and the client-rendered HTML are visible.
39
+ *
40
+ * Removes everything between (and including) the SSR boundary markers:
41
+ * <script type="x/boundary" id="ssr-body-start">
42
+ * ...server rendered content...
43
+ * <script type="x/boundary" id="ssr-body-end">
44
+ *
45
+ * Call this in your client entry point BEFORE creating the Ember app.
46
+ */
47
+ export declare function cleanupSSRContent(): void;
48
+ /**
49
+ * Checks if the current page was server-side rendered by looking
50
+ * for SSR boundary markers in the DOM.
51
+ */
52
+ export declare function isSSRRendered(): boolean;
53
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA0BH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAsFxC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAMrC;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
package/dist/client.js ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Client-side utilities for vite-ember-ssr.
3
+ *
4
+ * Currently the client Ember app boots normally and replaces the
5
+ * SSR-rendered content. True DOM hydration is planned for a future
6
+ * phase.
7
+ *
8
+ * For now, the SSR content provides the initial visual while client
9
+ * JavaScript loads, parses, and Ember boots.
10
+ */
11
+ const SHOEBOX_SCRIPT_ID = 'vite-ember-ssr-shoebox';
12
+ // ─── Shoebox: Client-Side Fetch Replay ───────────────────────────────
13
+ /** Original fetch function, saved before monkey-patching */
14
+ let _originalFetch = null;
15
+ /** Map of URL → { entry, refCount } for reference-counted consumption */
16
+ let _shoeboxMap = null;
17
+ /**
18
+ * Installs the shoebox fetch interceptor on the client.
19
+ *
20
+ * Reads the shoebox data from the server-injected <script> tag,
21
+ * removes the tag from the DOM, and monkey-patches globalThis.fetch
22
+ * to serve cached responses for URLs that match shoebox entries.
23
+ *
24
+ * Each entry is reference-counted: concurrent fetch calls to the same
25
+ * URL all receive the shoebox response. The entry is removed only when
26
+ * the last concurrent consumer has been served.
27
+ *
28
+ * Call this BEFORE creating the Ember application, typically as the
29
+ * first thing in your client entry point.
30
+ *
31
+ * @returns true if shoebox data was found and installed, false otherwise
32
+ */
33
+ export function installShoebox() {
34
+ const scriptEl = document.getElementById(SHOEBOX_SCRIPT_ID);
35
+ if (!scriptEl) {
36
+ return false;
37
+ }
38
+ // Parse the shoebox data
39
+ let entries;
40
+ try {
41
+ entries = JSON.parse(scriptEl.textContent ?? '[]');
42
+ }
43
+ catch {
44
+ // Malformed shoebox data — skip
45
+ scriptEl.remove();
46
+ return false;
47
+ }
48
+ // Remove the script tag from the DOM
49
+ scriptEl.remove();
50
+ if (entries.length === 0) {
51
+ return false;
52
+ }
53
+ // Build the lookup map with ref counts
54
+ _shoeboxMap = new Map();
55
+ for (const entry of entries) {
56
+ _shoeboxMap.set(entry.url, { entry, refCount: 1 });
57
+ }
58
+ // Save the original fetch and install our interceptor
59
+ _originalFetch = globalThis.fetch;
60
+ globalThis.fetch = function shoeboxFetch(input, init) {
61
+ // Only intercept GET requests (or requests with no method, which default to GET)
62
+ const method = init?.method?.toUpperCase() ?? 'GET';
63
+ if (method !== 'GET' || !_shoeboxMap || _shoeboxMap.size === 0) {
64
+ return _originalFetch(input, init);
65
+ }
66
+ // Resolve the URL string for matching
67
+ let url;
68
+ try {
69
+ if (typeof input === 'string') {
70
+ url = new URL(input, globalThis.location?.href).href;
71
+ }
72
+ else if (input instanceof URL) {
73
+ url = input.href;
74
+ }
75
+ else if (input instanceof Request) {
76
+ url = input.url;
77
+ }
78
+ else {
79
+ return _originalFetch(input, init);
80
+ }
81
+ }
82
+ catch {
83
+ return _originalFetch(input, init);
84
+ }
85
+ const cached = _shoeboxMap.get(url);
86
+ if (!cached) {
87
+ return _originalFetch(input, init);
88
+ }
89
+ // Decrement ref count and remove if exhausted
90
+ cached.refCount--;
91
+ if (cached.refCount <= 0) {
92
+ _shoeboxMap.delete(url);
93
+ }
94
+ // Construct a Response from the cached data
95
+ const { entry } = cached;
96
+ const response = new Response(entry.body, {
97
+ status: entry.status,
98
+ statusText: entry.statusText,
99
+ headers: new Headers(entry.headers),
100
+ });
101
+ // Auto-cleanup when the map is empty
102
+ if (_shoeboxMap.size === 0) {
103
+ cleanupShoebox();
104
+ }
105
+ return Promise.resolve(response);
106
+ };
107
+ return true;
108
+ }
109
+ /**
110
+ * Restores the original fetch function and cleans up shoebox state.
111
+ *
112
+ * Called automatically when all shoebox entries have been consumed,
113
+ * or can be called manually to force cleanup.
114
+ */
115
+ export function cleanupShoebox() {
116
+ if (_originalFetch) {
117
+ globalThis.fetch = _originalFetch;
118
+ _originalFetch = null;
119
+ }
120
+ _shoeboxMap = null;
121
+ }
122
+ // ─── SSR Content Cleanup ─────────────────────────────────────────────
123
+ /**
124
+ * Removes the SSR-rendered content from the DOM before the client
125
+ * Ember app boots. This prevents the "double render" where both the
126
+ * server-rendered HTML and the client-rendered HTML are visible.
127
+ *
128
+ * Removes everything between (and including) the SSR boundary markers:
129
+ * <script type="x/boundary" id="ssr-body-start">
130
+ * ...server rendered content...
131
+ * <script type="x/boundary" id="ssr-body-end">
132
+ *
133
+ * Call this in your client entry point BEFORE creating the Ember app.
134
+ */
135
+ export function cleanupSSRContent() {
136
+ const start = document.getElementById('ssr-body-start');
137
+ const end = document.getElementById('ssr-body-end');
138
+ if (!start || !end) {
139
+ return; // Not an SSR-rendered page
140
+ }
141
+ // Remove all nodes between start and end markers (inclusive)
142
+ const parent = start.parentNode;
143
+ if (!parent)
144
+ return;
145
+ let node = start;
146
+ while (node) {
147
+ const next = node.nextSibling;
148
+ parent.removeChild(node);
149
+ if (node === end)
150
+ break;
151
+ node = next;
152
+ }
153
+ }
154
+ /**
155
+ * Checks if the current page was server-side rendered by looking
156
+ * for SSR boundary markers in the DOM.
157
+ */
158
+ export function isSSRRendered() {
159
+ return document.getElementById('ssr-body-start') !== null;
160
+ }
161
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Minimal interface for an Ember Application that supports SSR.
3
+ *
4
+ * The app must be created with `autoboot: false` so the server can
5
+ * control boot timing via `app.visit(url, options)`.
6
+ *
7
+ * `visit()` handles the full boot sequence internally:
8
+ * app.boot() → app.buildInstance() → instance.boot(options) → instance.visit(url)
9
+ * and returns a Promise<ApplicationInstance>.
10
+ */
11
+ export interface EmberApplication {
12
+ visit(url: string, options?: BootOptions): Promise<EmberApplicationInstance>;
13
+ destroy(): void;
14
+ }
15
+ export interface EmberApplicationInstance {
16
+ destroy(): void;
17
+ getURL?(): string;
18
+ _booted?: boolean;
19
+ }
20
+ export interface BootOptions {
21
+ /** When false, disables browser-specific features like event delegation. */
22
+ isBrowser: boolean;
23
+ /** The document to render into. For SSR, this is the HappyDOM document. */
24
+ document: Document;
25
+ /** The root DOM element for the Ember app. Typically document.body. */
26
+ rootElement: Element;
27
+ /** Whether Ember should render templates. Set to true for SSR. */
28
+ shouldRender: boolean;
29
+ /** Override the router's location type. */
30
+ location?: string;
31
+ }
32
+ export interface RenderOptions {
33
+ /** The URL path to render, e.g., '/' or '/about' */
34
+ url: string;
35
+ /**
36
+ * Factory function that creates a fresh Ember Application instance.
37
+ * This should be the `createSsrApp` export from the Ember app.
38
+ */
39
+ createApp: () => EmberApplication;
40
+ /**
41
+ * When true, intercepts all fetch() calls during SSR rendering and
42
+ * serializes the responses into a <script> tag in the HTML output.
43
+ * The client can then replay these responses to avoid double-fetching.
44
+ */
45
+ shoebox?: boolean;
46
+ }
47
+ export interface RenderResult {
48
+ /** Rendered HTML from the document's <head> */
49
+ head: string;
50
+ /** Rendered HTML from the document's <body>, wrapped in boundary markers */
51
+ body: string;
52
+ /** HTTP status code (200 by default) */
53
+ statusCode: number;
54
+ /** Any error that occurred during rendering */
55
+ error?: Error;
56
+ }
57
+ /**
58
+ * A captured fetch response for transfer from server to client.
59
+ */
60
+ export interface ShoeboxEntry {
61
+ /** The request URL */
62
+ url: string;
63
+ /** HTTP status code */
64
+ status: number;
65
+ /** HTTP status text */
66
+ statusText: string;
67
+ /** Response headers (serializable subset) */
68
+ headers: Record<string, string>;
69
+ /** Response body as text */
70
+ body: string;
71
+ }
72
+ /**
73
+ * Renders an Ember application at the given URL using HappyDOM.
74
+ *
75
+ * This is the main SSR entry point. It:
76
+ * 1. Creates an isolated HappyDOM Window
77
+ * 2. Temporarily installs browser globals from HappyDOM
78
+ * 3. Creates the Ember app and calls visit()
79
+ * 4. Extracts the rendered HTML
80
+ * 5. Cleans up all resources
81
+ *
82
+ * Each call creates a completely fresh environment — there is no
83
+ * shared state between requests.
84
+ */
85
+ export declare function renderEmberApp(options: RenderOptions): Promise<RenderResult>;
86
+ /**
87
+ * Assembles the final HTML response by inserting rendered content
88
+ * into the index.html template.
89
+ *
90
+ * Replaces:
91
+ * - `<!-- VITE_EMBER_SSR_HEAD -->` with the rendered head content
92
+ * - `<!-- VITE_EMBER_SSR_BODY -->` with the rendered body content
93
+ */
94
+ export declare function assembleHTML(template: string, rendered: Pick<RenderResult, 'head' | 'body'>): string;
95
+ /**
96
+ * Checks whether an HTML template contains the required SSR markers.
97
+ */
98
+ export declare function hasSSRMarkers(html: string): {
99
+ head: boolean;
100
+ body: boolean;
101
+ };
102
+ export interface SSRResult {
103
+ /** Final HTML string with SSR content injected */
104
+ html: string;
105
+ /** HTTP status code (200 by default) */
106
+ statusCode: number;
107
+ /** Any error that occurred during rendering */
108
+ error?: Error;
109
+ }
110
+ /**
111
+ * Renders an Ember app at the given URL and assembles the final HTML
112
+ * in a single call. Combines `renderEmberApp` and `assembleHTML`.
113
+ *
114
+ * @example
115
+ * ```js
116
+ * const { html, statusCode, error } = await render({
117
+ * url: '/about',
118
+ * template,
119
+ * createApp: createSsrApp,
120
+ * shoebox: true,
121
+ * });
122
+ * reply.code(statusCode).type('text/html').send(html);
123
+ * ```
124
+ */
125
+ export declare function render(options: RenderOptions & {
126
+ template: string;
127
+ }): Promise<SSRResult>;
128
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAQA;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC7E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,IAAI,IAAI,CAAC;IAChB,MAAM,CAAC,IAAI,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,EAAE,QAAQ,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAC;IACrB,kEAAkE;IAClE,YAAY,EAAE,OAAO,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,SAAS,EAAE,MAAM,gBAAgB,CAAC;IAElC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAID;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,sBAAsB;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd;AA2LD;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFlF;AAID;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,GAC5C,MAAM,CAeR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAK5E;AAID,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,MAAM,CAC1B,OAAO,EAAE,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,SAAS,CAAC,CAUpB"}