@marko/run 0.0.1-beta2 → 0.0.1-beta4

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 CHANGED
@@ -1,31 +1,33 @@
1
- <h1 align="center">
1
+ <div align="center">
2
2
  <!-- Logo -->
3
- <img src="https://user-images.githubusercontent.com/4985201/115444712-ca550500-a1c9-11eb-9897-238ece59129c.png" height="118"/>
4
- <br/>
5
- @marko/run
6
- <br/>
3
+ <h1>
4
+ <img alt="" src="https://user-images.githubusercontent.com/4985201/115444712-ca550500-a1c9-11eb-9897-238ece59129c.png" height="118"/>
5
+ <br/>
6
+ @marko/run
7
+ </h1>
7
8
 
8
9
  <!-- Language -->
9
- <a href="http://typescriptlang.org">
10
+ <a href="https://www.typescriptlang.org">
10
11
  <img src="https://img.shields.io/badge/%3C%2F%3E-typescript-blue.svg" alt="TypeScript"/>
11
12
  </a>
12
- </h1>
13
+ </div>
13
14
 
14
- Vite plugin for Marko with these features
15
- - Encapsulates [@marko/vite](https://github.com/marko-js/vite)
15
+ `@marko/serve` is a Vite plugin for [Marko](https://markojs.com), with these features:
16
+
17
+ - Encapsulates [`@marko/vite`](https://github.com/marko-js/vite)
16
18
  - File-based routing with layouts and middleware
17
19
  - Efficient routing using a compiled static trie
18
- - Designed with web standards to run anywhere
20
+ - [Designed with web standards](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/URLPattern) to run anywhere
19
21
 
20
- ## Intallation
22
+ ## Installation
21
23
 
22
- ```
24
+ ```sh
23
25
  npm install @marko/run
24
26
  ```
25
27
 
26
28
  ## Vite Plugin
27
29
 
28
- This package provides both a Vite plugin and a runtime import. The Vite plugin is responsible for discovering your route files, generating the routing code and registering the @marko/vite plugin which builds your .marko files.
30
+ This package’s Vite plugin discovers your route files, generates the routing code, and registers the `@marko/vite` plugin to compile your `.marko` files.
29
31
 
30
32
  ```ts
31
33
  // vite.config.ts
@@ -35,27 +37,32 @@ import marko from "@marko/run/vite"; // Import the Vite plugin
35
37
  export default defineConfig({
36
38
  plugins: [marko()], // Register the Vite plugin
37
39
  build: {
38
- sourcemap: true, // Generate sourcemaps for all builds.
39
- emptyOutDir: false, // Avoid server & client deleting files from each other.
40
+ sourcemap: true, // Generate sourcemaps for all builds
41
+ emptyOutDir: false, // Avoid server & client deleting files from each other. TODO: do we have to make the user set this themselves?
40
42
  }
41
43
  })
42
44
  ```
43
45
 
44
46
  ## Runtime
45
47
 
46
- Generally you'll want to use one of the adapters to provide a more convenient experience but this package provides the following runtime interface:
48
+ Generally, using one of the adapters is more convenient <!-- TODO: is an “adapter” the same thing as using the Vite plugin above? -->, but this package also provides a runtime API:
47
49
 
48
50
  ```ts
49
51
  import { router, getMatchedRoute } from '@marko/run`;
50
52
  ```
51
53
 
52
54
  ### `router`
55
+
53
56
  ```ts
54
57
  (request: Request) => Promise<Response>;
55
58
  ```
56
- This asynchronouse function takes a [WHATWG request](https://fetch.spec.whatwg.org/#request-class) object and returns a [response](https://fetch.spec.whatwg.org/#response-class) object generated by executing the corresponding matched route files. If no match is found, a response with a 404 status code is returned. If an unhandled error occurs, a response with a 500 status code will be returned.
59
+
60
+ This asynchronous function takes a [WHATWG `Request` object](https://fetch.spec.whatwg.org/#request-class) and returns a [`Response` object](https://fetch.spec.whatwg.org/#response-class) generated by executing any matched route files.
61
+
62
+ If no match is found, returns a response with a `404` status code. If an unhandled error occurs, returns a response with a `500` status code.
57
63
 
58
64
  ### `getMatchedRoute`
65
+
59
66
  ```ts
60
67
  (method: string, url: URL) => {
61
68
  params: Record<string, string>;
@@ -63,20 +70,25 @@ This asynchronouse function takes a [WHATWG request](https://fetch.spec.whatwg.o
63
70
  invoke(request: Request): Promise<Response>;
64
71
  } | null;
65
72
  ```
66
- This synchronous function takes a HTTP verb method, and URL and returns an object representing the best match or null if no match is found.
67
- - `params` - a key-value collection of any path parameters for the route
68
- - `meta` - meta data for the route
69
- - `invoke` - an asynchronouse function that takes a WHATWG request and returns the response generated by executing the matched route files. Note: unlike the top-level `router` function errors will not be caught.
73
+
74
+ This synchronous function takes an HTTP method + URL, then returns an object representing the best match — or `null` if no match is found.
75
+
76
+ - `params` - a `{ key: value }` collection of any path parameters for the route
77
+ - `meta` - metadata for the route
78
+ - `invoke` - an asynchronous function that takes a `Request` and returns the `Response` generated from matched route files.
79
+ > **Note**: unlike the top-level `router` function, errors will not be caught.
70
80
 
71
81
  ## File-based Routing
72
82
 
73
- ## Nested Routing
83
+ ### Nested Routing
74
84
 
75
- *TODO: provide a quick overview*
85
+ *🎗 TODO: provide a quick overview*
76
86
 
77
87
  ### Routes Directory
78
88
 
79
- By default, the plugin will look for files in the configured _routes directory_. By default it will look in `./src/routes` (relative to the Vite config file) which can be configured:
89
+ The plugin looks for route files in the configured **routes directory**. By default, that’s `./src/routes`, relative to the Vite config file.
90
+
91
+ To change what directory routes are found in:
80
92
 
81
93
  ```ts
82
94
  // vite.config.ts
@@ -86,55 +98,56 @@ import marko from "@marko/run/vite";
86
98
  export default defineConfig({
87
99
  plugins: [marko({
88
100
  routesDir: 'src/pages' // Use `./src/pages` (relative to this file) as the routes directory
89
- })],
90
- //...
101
+ })]
91
102
  })
92
103
  ```
93
104
 
94
105
  ### Routeable Files
95
106
 
96
- In order to allow for co-location of files that should not be served (e.g. tests, stories, assets), the router only recognizes a specific set of file names. The following files will be discovered in any directory within your application's _routes directory_.
107
+ To allow for colocation of files that shouldn’t be served (like tests, assets, etc.), the router only recognizes certain filenames.
108
+
109
+ The following filenames will be discovered in any directory inside your application’s [routes directory](#routes-directory).
97
110
 
98
111
  #### `+page.marko`
99
- These files establish a route at the current directory path which will be served for GET requests with the HTML content of the page. Only one page may exists for any served path.
112
+
113
+ These files establish a route at the current directory path which will be served for `GET` requests with the HTML content of the page. Only one page may exists for any served path.
100
114
 
101
115
  #### `+layout.marko`
102
- These files provide a layout component which will wrap all nested layouts and pages.
103
116
 
104
- <details>
105
- <summary>More Info</summary>
106
-
107
- Layouts are just like any other Marko component with no extra constraints. Each layout receives the request, path params, URL and route meta data as input as well as a renderBody which will be the subsequent layout or page to project.
108
-
109
- ```marko
110
- <div>
111
- <h1>My Products</h1>
112
- <main>
113
- ${input.renderBody} // project the page or layout here
114
- </main>
115
- </div>
116
- ```
117
- </details>
117
+ These files provide a **layout component**, which will wrap all nested layouts and pages.
118
+
119
+ Layouts are like any other Marko component with no extra constraints. Each layout receives the request, path params, URL, and route metadata as input, as well as a `renderBody` which will be the next layout or page to project.
120
+
121
+ ```marko
122
+ <main>
123
+ <h1>My Products</h1>
124
+
125
+ ${input.renderBody} // render the page or layout here
126
+ </main>
127
+ ```
118
128
 
119
129
  #### `+handler.*`
120
- These files establish a route at the current directory path which can handle requests for GET, POST, PUT and DELETE HTTP verbs. Typically these will be .js or .ts files depending on your project. Just like pages, only one handler may exist for any served path. A handler should export functions
130
+
131
+ These files establish a route at the current directory path which can handle requests for `GET`, `POST`, `PUT`, and `DELETE` HTTP methods. <!-- TODO: what about HEAD? -->
132
+
133
+ Typically, these will be `.js` or `.ts` files depending on your project. Like pages, only one handler may exist for any served path. A handler should export functions
121
134
 
122
135
  <details>
123
136
  <summary>More Info</summary>
124
137
 
125
- - Valid exports are: `get`, `post`, `put`, `del`.
126
- - Each method takes a `context` and `next` argument and should return a WHATWG response either synchronously or asynchronously.
127
- - The `context` argument contains the WHATWG request object, path parameters, URL and route meta data.
128
- - The `next` argument will call the page for get requests where applicable or return a 204 response.
138
+ - Valid exports are functions named `get`, `post`, `put`, or `del`.
139
+ - Each export receives a `context` and `next` argument, and should return a WHATWG `Response` either synchronously or asynchronously.
140
+ - The `context` argument contains the WHATWG request object, path parameters, URL, and route metadata.
141
+ - The `next` argument will call the page for get requests where applicable or return a `204` response.
129
142
 
130
- ```ts
143
+ ```js
131
144
  export function post(context, next) {
132
145
  const { request, params, url, meta } = context;
133
- return new Response('updated', { status: 200 });
146
+ return new Response('Successfully updated', { status: 200 });
134
147
  }
135
148
 
136
149
  export function put(context, next) {
137
- return new Response('created', { status: 201 }); // handle the request
150
+ return new Response('Successfully created', { status: 201 }); // handle the request
138
151
  }
139
152
 
140
153
  export function get(context, next) {
@@ -142,29 +155,33 @@ These files establish a route at the current directory path which can handle req
142
155
  }
143
156
 
144
157
  export function del(context, next) {
145
- return new Response('removed', { status: 204 });
158
+ return new Response('Successfully removed', { status: 204 });
146
159
  }
147
160
  ```
148
161
  </details>
149
162
 
150
163
 
151
- #### `+middlware.*`
152
- These files are analagous to layouts for handlers. Middlware get called before handlers and let you perform arbitrary work before and after. Unlike handlers, middleware run for all HTTP verbs.
164
+ #### `+middleware.*`
165
+
166
+ These files are like layouts, but for handlers. Middleware get called before handlers and let you perform arbitrary work before and after.
167
+
168
+ > **Note**: Unlike handlers, middleware run for all HTTP methods.
153
169
 
154
170
  <details>
155
171
  <summary>More Info</summary>
156
172
 
157
- - Expects default export that takes a `context` and `next` argument and should return a WHATWG response either synchronously or asynchronously.
158
- - The `context` argument contains the WHATWG request object, path parameters, URL and route meta data.
159
- - The `next` argument will call the next middleware, handler or page as applicable for the route.
173
+ Expects a `default` export that receives a `context` and `next` argument, and should return a WHATWG response either synchronously or asynchronously.
174
+
175
+ - The `context` argument contains the WHATWG `Request` object, path parameters, URL, and route metadata.
176
+ - The `next` argument will call the next middleware, handler, or page for the route.
160
177
 
161
178
  ```ts
162
179
  export default async function(context, next) {
163
- const requestName = `${ctx.request.method} ${ctx.url.href}`;
180
+ const requestName = `${ctx.request.method} ${ctx.url.href}`; // TODO: could this be just `ctx.url` with a `toString` that then grabs `.href`?
164
181
  let success = true;
165
182
  console.log(`${requestName} request started`)
166
183
  try {
167
- return await next(); // wait for subsequent middleware/handler/page
184
+ return await next(); // Wait for subsequent middleware/handler/page
168
185
  } catch (err) {
169
186
  success = false;
170
187
  throw err;
@@ -176,30 +193,42 @@ These files are analagous to layouts for handlers. Middlware get called before h
176
193
  </details>
177
194
 
178
195
  #### `+meta.*`
179
- These files represent some meta data to attach to the route. This meta data will be automatically imported by the router and provided on the the route context when invoking a route.
196
+
197
+ These files represent metadata to attach to the route. This metadata will be automatically provided on the the route `context` when invoking a route.
180
198
 
181
199
  ### Special Files
182
200
 
183
- In addition to the files above which can be defined in any directory under the _routes directory_, there are some special files which can only be defined within the _routes directory_:
201
+ In addition to the files above which can be defined in any directory under the _routes directory_, there are some special files which can only be defined at the top-level of the _routes directory_. <!-- TODO: do we want to keep this restriction? Having nested 404s would be handy for disambiguating things like “there’s no user with that name” or “that promotion wasn’t found, it may have expired” -->
202
+
203
+ These special pages are subject to a root layout file (`pages/+layout.marko` in the default configuration).
184
204
 
185
205
  #### `+404.marko`
186
- This is a special page wich will be rendered for any request whose `Accept` header includes `text/html` if no other handler or page handled the request. This page will be subject to the root layout file (ie. +layout.marko file in the _routes directory_). Responses with this page will have a `404` status code.
206
+
207
+ This special page responds to any request where:
208
+
209
+ - The `Accept` request header includes `text/html`
210
+ - *And* no other handler or page rendered the request
211
+
212
+ Responses with this page will have a `404` status code.
187
213
 
188
214
  #### `+500.marko`
189
- This is a special page wich will be rendered for any request whose `Accept` header includes `text/html` if an uncaught error occurs while handling the request. This page will be subject to the root layout file (ie. +layout.marko file in the _routes directory_). Responses with this page will have a `500` status code.
190
215
 
216
+ This special page responds to any request where:
217
+
218
+ - The `Accept` request header includes `text/html`
219
+ - *And* an uncaught error occurs while serving the request
220
+
221
+ Responses with this page will have a `500` status code.
191
222
 
192
223
  ### Execution Order
193
224
 
194
- For a matched route the routable files execute in the following order
225
+ For a matched route, the routable files execute in the following order:
226
+
195
227
  1. Middlewares from root-most to leaf-most
196
228
  2. Handler
197
229
  3. Layouts from root-most to leaf-most
198
230
  4. Page
199
231
 
200
- <details>
201
- <summary>Diagram</summary>
202
-
203
232
  ```mermaid
204
233
  sequenceDiagram
205
234
  participant Middleware1
@@ -218,13 +247,12 @@ sequenceDiagram
218
247
  Handler-->>Middleware2: Response
219
248
  Middleware2-->>Middleware1: Response
220
249
  ```
221
- </details>
222
250
 
223
251
  ### Path Structure
224
252
 
225
- Within the _routes directory_ the directory structure will determine the path the route will be served. There are four types of directory names:
253
+ Within the _routes directory_, the directory structure will determine the path the route will be served. There are four types of directory names: static, pathless, dynamic, and catch-all.
226
254
 
227
- - **Static directories** - The most common type of directory. Each static directory will contribute its name as a segment in the route's served path like a traditional file server. This is the default and unless the directory matches the requirements for one of the below types, it will be a static directory.
255
+ 1. **Static directories** - The most common type. Each static directory contributes its name as a segment in the route's served path, like a traditional fileserver. Unless a directory name matches the requirements for one of the below types, it defaults to a static directory.
228
256
 
229
257
  Examples:
230
258
  ```
@@ -233,7 +261,7 @@ Within the _routes directory_ the directory structure will determine the path th
233
261
  /projects
234
262
  ```
235
263
 
236
- - **Pathless directories** - These directories do not contribute their name to the route's served path. Directory names that start with an underscore ('_') as well as directories named 'index' will be a pathless directory.
264
+ 2. **Pathless directories** - These directories do **not** contribute their name to the route's served path. Directory names that start with an underscore (`_`) or directories named `index` will be a pathless directory.
237
265
 
238
266
  Examples:
239
267
  ```
@@ -242,7 +270,7 @@ Within the _routes directory_ the directory structure will determine the path th
242
270
  /index
243
271
  ```
244
272
 
245
- - **Dynamic directories** - These directories introduce a dynamic parameter to the route's served path and will match any value at that segment. Any directory name that starts with a single dollar sign ('\$') will be a dynamic directory and the remaining directory name will be the parameter at runtime. In the case the directory name is '$', the parameter will not exist at runtime but will be matched.
273
+ 3. **Dynamic directories** - These directories introduce a dynamic parameter to the route's served path and will match any value at that segment. Any directory name that starts with a single dollar sign (`$`) will be a dynamic directory, and the remaining directory name will be the parameter at runtime. If the directory name is exactly `$/`, the parameter will not exist at runtime but will be matched.
246
274
 
247
275
  Examples:
248
276
  ```
@@ -251,9 +279,9 @@ Within the _routes directory_ the directory structure will determine the path th
251
279
  /$
252
280
  ```
253
281
 
254
- - **Catcha-all directories** - These directories are similar to dynamic directories and introduce a dynamic parameter but instead of matching a single sement of the path, they match to the end of the path. Any directory that starts with two dollar signs ('\$\$') will be a catch-all directory and the remaining directory name will be the parameter at runtime. In the case of a directory name '$$', the parameter name at runtime will be '*'. Catch-all directories can be used to make not found/404 routes at any level including the root.
282
+ 4. **Catch-all directories** - These directories are similar to dynamic directories and introduce a dynamic parameter, but instead of matching a single path segment, they match to the end of the path. Any directory that starts with two dollar signs (`$$`) will be a catch-all directory, and the remaining directory name will be the parameter at runtime. In the case of a directory named `$$/`, the parameter name at runtime will be `*`. Catch-all directories can be used to make `404` Not Found routes at any level, including the root.
255
283
 
256
- Because catch-all directories always match and consume all the remaining path, you cannot have nested routes within and no further directories will be traversed.
284
+ Because catch-all directories match any path segment and consume the rest of the path, you cannot nest route files in them and no further directories will be traversed.
257
285
 
258
286
  Examples:
259
287
  ```
@@ -1,12 +1,16 @@
1
1
  import createStaticServe from "serve-static";
2
+ import compression from "compression";
2
3
  import { createServer } from "http";
3
- import { createMiddleware } from "@hattip/adapter-node";
4
- import { handler } from "@marko/run/router";
4
+ import createMiddleware from "@marko/run/adapter/middleware";
5
+ import { router } from "@marko/run/router";
5
6
 
6
7
  const { PORT = 3456 } = process.env;
7
8
 
8
9
  const dir = process.cwd();
9
- const middleware = createMiddleware(handler);
10
+ const middleware = createMiddleware(router);
11
+ const compress = compression({
12
+ threshold: 500,
13
+ });
10
14
  const staticServe = createStaticServe(dir, {
11
15
  index: false,
12
16
  immutable: true,
@@ -14,5 +18,7 @@ const staticServe = createStaticServe(dir, {
14
18
  });
15
19
 
16
20
  createServer((req, res) =>
21
+ compress(req, res, () =>
17
22
  staticServe(req, res, () => middleware(req, res))
23
+ )
18
24
  ).listen(PORT);
@@ -1,4 +1,4 @@
1
1
  import { ViteDevServer } from "vite";
2
- import { type NodeMiddleware } from "@hattip/adapter-node";
2
+ import { type NodeMiddleware } from "./middleware";
3
3
  export declare function createViteDevMiddleware<T>(devServer: ViteDevServer, load: (prev: T | undefined) => Promise<T>, factory: (value: T) => NodeMiddleware): NodeMiddleware;
4
4
  export declare function createDevServer(configFile?: string): Promise<import("vite").Connect.Server>;
@@ -36,7 +36,173 @@ var import_url = require("url");
36
36
 
37
37
  // src/adapter/dev-server.ts
38
38
  var import_vite = require("vite");
39
- var import_adapter_node = require("@hattip/adapter-node");
39
+
40
+ // src/adapter/middleware.ts
41
+ var webStream = __toESM(require("stream/web"), 1);
42
+ var import_crypto = __toESM(require("@hattip/polyfills/crypto"), 1);
43
+ (0, import_crypto.default)();
44
+ for (const key of Object.keys(webStream)) {
45
+ if (!(key in global)) {
46
+ global[key] = webStream[key];
47
+ }
48
+ }
49
+ function getForwardedHeader(req, name) {
50
+ const value = req.headers["x-forwarded-" + name];
51
+ if (value) {
52
+ if (typeof value === "string") {
53
+ const index = value.indexOf(",");
54
+ return index < 0 ? value : value.slice(0, index);
55
+ }
56
+ return value[0];
57
+ }
58
+ }
59
+ function getOrigin(req, protocol, host, trustProxy) {
60
+ var _a;
61
+ protocol ?? (protocol = req.protocol || trustProxy && getForwardedHeader(req, "proto") || ((_a = req.socket) == null ? void 0 : _a.encrypted) && "https" || "http");
62
+ host ?? (host = trustProxy && getForwardedHeader(req, "host") || req.headers.host);
63
+ if (!host) {
64
+ if (process.env.NODE_ENV !== "production") {
65
+ host = "localhost";
66
+ console.warn(
67
+ `Could not automatically determine the origin host, using 'localhost'. Use the 'origin' option or the 'ORIGIN' environment variable to set the origin explicitly.`
68
+ );
69
+ } else {
70
+ throw new Error(
71
+ `Could not automatically determine the origin host. Use the 'origin' option or the 'ORIGIN' environment variable to set the origin explicitly.`
72
+ );
73
+ }
74
+ }
75
+ return `${protocol}://${host}`;
76
+ }
77
+ function createMiddleware(router, options = {}) {
78
+ const { trustProxy = process.env.TRUST_PROXY === "1" } = options;
79
+ let { origin = process.env.ORIGIN } = options;
80
+ let protocol;
81
+ let host;
82
+ if (origin) {
83
+ ({ protocol, host } = new URL(origin));
84
+ protocol = protocol.slice(0, -1);
85
+ }
86
+ return async (req, res, next) => {
87
+ origin ?? (origin = getOrigin(req, protocol, host, trustProxy));
88
+ const url = new URL(req.url, origin);
89
+ const ip = req.ip || trustProxy && getForwardedHeader(req, "for") || req.socket.remoteAddress || "";
90
+ const requestContext = {
91
+ method: req.method,
92
+ url,
93
+ platform: {
94
+ ip,
95
+ request: req,
96
+ response: res,
97
+ setCookie(cookie) {
98
+ res.appendHeader("set-cookie", cookie);
99
+ }
100
+ }
101
+ };
102
+ Object.defineProperty(requestContext, "request", {
103
+ get() {
104
+ const headers = req.headers;
105
+ const body = req.method === "GET" || req.method === "HEAD" ? void 0 : req.socket ? req : new ReadableStream({
106
+ start(controller) {
107
+ req.on("data", (chunk) => controller.enqueue(chunk));
108
+ req.on("end", () => controller.close());
109
+ req.on("error", (err) => controller.error(err));
110
+ }
111
+ });
112
+ const request = new Request(url, {
113
+ method: req.method,
114
+ headers,
115
+ body,
116
+ duplex: "half"
117
+ });
118
+ Object.defineProperty(this, "request", {
119
+ value: request,
120
+ enumerable: true,
121
+ configurable: true
122
+ });
123
+ return request;
124
+ },
125
+ enumerable: true,
126
+ configurable: true
127
+ });
128
+ const response = await router(requestContext);
129
+ if (!response) {
130
+ if (next) {
131
+ next();
132
+ } else {
133
+ res.statusCode = 404;
134
+ res.setHeader("content-length", "0");
135
+ res.end();
136
+ return;
137
+ }
138
+ return;
139
+ }
140
+ res.statusCode = response.status;
141
+ for (const [key, value] of response.headers) {
142
+ if (key === "set-cookie") {
143
+ let sepIndex = value.indexOf(",") + 1;
144
+ if (!sepIndex) {
145
+ res.setHeader(key, value);
146
+ } else {
147
+ let index = 0;
148
+ do {
149
+ res.appendHeader(key, value.slice(index, sepIndex - 1));
150
+ index = sepIndex;
151
+ sepIndex = value.indexOf(",", sepIndex) + 1;
152
+ } while (sepIndex);
153
+ res.appendHeader(key, value.slice(index));
154
+ }
155
+ } else {
156
+ res.setHeader(key, value);
157
+ }
158
+ }
159
+ if (!response.body) {
160
+ if (!response.headers.has("content-length")) {
161
+ res.setHeader("content-length", "0");
162
+ }
163
+ res.end();
164
+ return;
165
+ }
166
+ const reader = response.body.getReader();
167
+ if (res.destroyed) {
168
+ reader.cancel();
169
+ return;
170
+ }
171
+ res.on("close", cancel);
172
+ res.on("error", cancel);
173
+ write();
174
+ function cancel(error) {
175
+ res.off("close", cancel);
176
+ res.off("error", cancel);
177
+ reader.cancel(error).catch(() => {
178
+ });
179
+ error && res.destroy(error);
180
+ }
181
+ async function write() {
182
+ try {
183
+ while (true) {
184
+ const { done, value } = await reader.read();
185
+ if (done) {
186
+ res.end();
187
+ return;
188
+ } else if (!res.write(value)) {
189
+ res.once("drain", write);
190
+ return;
191
+ } else if (res.flush) {
192
+ res.flush();
193
+ }
194
+ }
195
+ } catch (err) {
196
+ const error = err instanceof Error ? err : new Error("Error while writing to node response", {
197
+ cause: err
198
+ });
199
+ cancel(error);
200
+ }
201
+ }
202
+ };
203
+ }
204
+
205
+ // src/adapter/dev-server.ts
40
206
  function createViteDevMiddleware(devServer, load, factory) {
41
207
  let value;
42
208
  let middleware;
@@ -64,8 +230,8 @@ async function createDevServer(configFile) {
64
230
  });
65
231
  const middleware = createViteDevMiddleware(
66
232
  devServer,
67
- async () => (await devServer.ssrLoadModule("@marko/run/router")).handler,
68
- import_adapter_node.createMiddleware
233
+ async () => (await devServer.ssrLoadModule("@marko/run/router")).router,
234
+ createMiddleware
69
235
  );
70
236
  return devServer.middlewares.use(middleware);
71
237
  }
@@ -73,16 +239,30 @@ async function createDevServer(configFile) {
73
239
  // src/vite/utils/server.ts
74
240
  var import_net = __toESM(require("net"), 1);
75
241
  var import_child_process = __toESM(require("child_process"), 1);
76
- async function spawnServer(cmd, port = 0, cwd = process.cwd(), wait = 3e4) {
242
+ var import_dotenv = require("dotenv");
243
+ var import_fs = __toESM(require("fs"), 1);
244
+ async function parseEnv(envFile) {
245
+ if (import_fs.default.existsSync(envFile)) {
246
+ const content = await import_fs.default.promises.readFile(envFile, "utf8");
247
+ return (0, import_dotenv.parse)(content);
248
+ }
249
+ }
250
+ function loadEnv(envFile) {
251
+ (0, import_dotenv.config)({ path: envFile });
252
+ }
253
+ async function spawnServer(cmd, port = 0, env, cwd = process.cwd(), wait = 3e4) {
77
254
  if (port <= 0) {
78
255
  port = await getAvailablePort();
79
256
  }
257
+ if (typeof env === "string") {
258
+ env = await parseEnv(env);
259
+ }
80
260
  const proc = import_child_process.default.spawn(cmd, {
81
261
  cwd,
82
262
  shell: true,
83
263
  stdio: "inherit",
84
264
  windowsHide: true,
85
- env: { NODE_ENV: "development", ...process.env, PORT: `${port}` }
265
+ env: { ...env, NODE_ENV: "development", ...process.env, PORT: `${port}` }
86
266
  });
87
267
  const close = () => {
88
268
  proc.unref();
@@ -136,7 +316,8 @@ function adapter() {
136
316
  const entry = import_path.default.join(__dirname, "default-entry");
137
317
  return entry;
138
318
  },
139
- async startDev(configFile, port) {
319
+ async startDev(configFile, port, envFile) {
320
+ envFile && await loadEnv(envFile);
140
321
  const server = await createDevServer(configFile);
141
322
  return new Promise((resolve) => {
142
323
  const listener = server.listen(port, () => {
@@ -146,8 +327,8 @@ function adapter() {
146
327
  });
147
328
  });
148
329
  },
149
- async startPreview(dir, entry, port) {
150
- const server = await spawnServer(`node ${entry}`, port, dir);
330
+ async startPreview(dir, entry, port, envFile) {
331
+ const server = await spawnServer(`node ${entry}`, port, envFile, dir);
151
332
  console.log(`Preview server started: http://localhost:${server.port}`);
152
333
  }
153
334
  };
@@ -1,4 +1,5 @@
1
1
  import type { Adapter } from "../vite";
2
2
  export { createDevServer, createViteDevMiddleware } from "./dev-server";
3
3
  export type { Adapter };
4
+ export type { NodePlatformInfo } from './middleware';
4
5
  export default function adapter(): Adapter;