@foldkit/node 0.0.0 → 0.1.0-canary.49dc36ef672d
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 +2 -0
- package/dist/node.d.ts +3 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +80 -48
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ NodeRuntime.runMain(Node.serve({ port, origin }))
|
|
|
30
30
|
`origin` is the public URL of the deployment. When it is `None`, the adapter
|
|
31
31
|
uses `http://localhost:<port>`. Set it behind a proxy or TLS terminator so the
|
|
32
32
|
server entry receives the public `Request.url`.
|
|
33
|
+
The supplied value must be an HTTP or HTTPS origin without credentials, a path,
|
|
34
|
+
query, or fragment. Invalid values fail during startup.
|
|
33
35
|
|
|
34
36
|
The adapter serves matching client files only for `GET` and `HEAD`. It sends
|
|
35
37
|
the Vite-base `index.html` path, such as `/app/index.html` for `/app/`, and
|
package/dist/node.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export type ServeOptions = Readonly<{
|
|
|
6
6
|
port: Config.Config<number>;
|
|
7
7
|
/**
|
|
8
8
|
* The origin Config for requests that reach the Fetch handler. Use `None`
|
|
9
|
-
* to derive `http://localhost:<port>` from the resolved port.
|
|
9
|
+
* to derive `http://localhost:<port>` from the resolved port. A supplied
|
|
10
|
+
* origin must use HTTP or HTTPS and have no credentials, path, query, or
|
|
11
|
+
* fragment.
|
|
10
12
|
*/
|
|
11
13
|
origin: Config.Config<Option.Option<string>>;
|
|
12
14
|
/**
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,MAAM,EAIN,MAAM,EACN,aAAa,EAEb,MAAM,EACP,MAAM,QAAQ,CAAA;AACf,OAAO,EAGL,eAAe,EAIhB,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,MAAM,EAIN,MAAM,EACN,aAAa,EAEb,MAAM,EACP,MAAM,QAAQ,CAAA;AACf,OAAO,EAGL,eAAe,EAIhB,MAAM,aAAa,CAAA;AAmCpB,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC3B;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IAC5C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAC,CAAA;AAoWF;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,YAAa,YAAY,4IACqC,CAAA"}
|
package/dist/node.js
CHANGED
|
@@ -7,8 +7,10 @@ import { dirname, isAbsolute, posix, relative, resolve, sep } from 'node:path';
|
|
|
7
7
|
import { Readable } from 'node:stream';
|
|
8
8
|
import { ReadableStream } from 'node:stream/web';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
-
import {
|
|
10
|
+
import { NodeHttpServer, NodeHttpServerRequest } from '@effect/platform-node';
|
|
11
11
|
const DEFAULT_MANIFEST_PATH = 'dist/server/foldkit.build.json';
|
|
12
|
+
const INVALID_ORIGIN_MESSAGE = 'origin must be an HTTP or HTTPS origin without credentials, path, query, or fragment';
|
|
13
|
+
const ORIGIN_SYNTAX = /^https?:\/\/[^/?#\\\s@]+\/?$/i;
|
|
12
14
|
const FoldkitBuildManifest = Schema.Struct({
|
|
13
15
|
schemaVersion: Schema.Literals([1]),
|
|
14
16
|
client: Schema.String,
|
|
@@ -22,26 +24,43 @@ const isRouteNotFound = (error) => error.reason._tag === 'RouteNotFound';
|
|
|
22
24
|
const hostSettledResponse = () => HttpServerResponse.setHeader(HttpServerResponse.empty({
|
|
23
25
|
status: Server.HOST_METHOD_ANSWERS.refusedStatus,
|
|
24
26
|
}), 'allow', Server.HOST_METHOD_ANSWERS.allow);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const isPublicOriginUrl = (url) => (url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
28
|
+
url.username === '' &&
|
|
29
|
+
url.password === '' &&
|
|
30
|
+
url.pathname === '/' &&
|
|
31
|
+
url.search === '' &&
|
|
32
|
+
url.hash === '';
|
|
33
|
+
const originFrom = (origin) => Effect.gen(function* () {
|
|
34
|
+
if (!ORIGIN_SYNTAX.test(origin)) {
|
|
35
|
+
return yield* Effect.fail(new Error(INVALID_ORIGIN_MESSAGE));
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const url = yield* Effect.try({
|
|
38
|
+
try: () => new URL(origin),
|
|
39
|
+
catch: () => new Error(INVALID_ORIGIN_MESSAGE),
|
|
40
|
+
});
|
|
41
|
+
if (!isPublicOriginUrl(url)) {
|
|
42
|
+
return yield* Effect.fail(new Error(INVALID_ORIGIN_MESSAGE));
|
|
33
43
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
return url.origin;
|
|
45
|
+
});
|
|
46
|
+
const isRootRelativeBasePath = (basePath, url) => basePath.startsWith('/') &&
|
|
47
|
+
!basePath.startsWith('//') &&
|
|
48
|
+
!basePath.includes('\\') &&
|
|
49
|
+
basePath.endsWith('/') &&
|
|
50
|
+
url.origin === 'http://localhost' &&
|
|
51
|
+
url.search === '' &&
|
|
52
|
+
url.hash === '';
|
|
53
|
+
const basePathFrom = (basePath) => Effect.gen(function* () {
|
|
54
|
+
const resolvedBasePath = basePath ?? '/';
|
|
55
|
+
const url = yield* Effect.try({
|
|
56
|
+
try: () => new URL(resolvedBasePath, 'http://localhost'),
|
|
57
|
+
catch: () => new Error('basePath must be a root-relative path ending with `/`'),
|
|
58
|
+
});
|
|
59
|
+
if (!isRootRelativeBasePath(resolvedBasePath, url)) {
|
|
60
|
+
return yield* Effect.fail(new Error('basePath must be a root-relative path ending with `/`'));
|
|
42
61
|
}
|
|
43
62
|
return url.pathname;
|
|
44
|
-
};
|
|
63
|
+
});
|
|
45
64
|
const staticPathFor = (pathname, basePath) => {
|
|
46
65
|
if (basePath === '/') {
|
|
47
66
|
return pathname;
|
|
@@ -51,51 +70,65 @@ const staticPathFor = (pathname, basePath) => {
|
|
|
51
70
|
}
|
|
52
71
|
return `/${pathname.slice(basePath.length)}`;
|
|
53
72
|
};
|
|
54
|
-
const decodeManifest = (content) => Effect.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
const decodeManifest = (content) => Effect.gen(function* () {
|
|
74
|
+
const parsed = yield* Effect.try({
|
|
75
|
+
try: () => JSON.parse(content),
|
|
76
|
+
catch: cause => new Error('foldkit.build.json is not valid JSON', { cause }),
|
|
77
|
+
});
|
|
78
|
+
return yield* Schema.decodeUnknownEffect(FoldkitBuildManifest)(parsed);
|
|
79
|
+
});
|
|
58
80
|
const buildPaths = (manifestPath, manifest, rootDirectory) => {
|
|
59
81
|
const serverDirectory = dirname(manifestPath);
|
|
60
82
|
const normalizedServerDirectory = posix.normalize(manifest.server);
|
|
61
83
|
if (rootDirectory === undefined &&
|
|
62
84
|
(normalizedServerDirectory === '..' ||
|
|
63
85
|
normalizedServerDirectory.startsWith('../'))) {
|
|
64
|
-
|
|
86
|
+
return Effect.fail(new Error('foldkit.build.json describes a server directory outside the Vite root. Pass rootDirectory so @foldkit/node can locate the client output.'));
|
|
65
87
|
}
|
|
66
88
|
const resolvedRootDirectory = rootDirectory === undefined
|
|
67
89
|
? resolve(serverDirectory, relative(manifest.server, '.'))
|
|
68
90
|
: resolve(process.cwd(), rootDirectory);
|
|
69
91
|
const expectedServerDirectory = resolve(resolvedRootDirectory, manifest.server);
|
|
70
92
|
if (expectedServerDirectory !== serverDirectory) {
|
|
71
|
-
|
|
93
|
+
return Effect.fail(new Error(`rootDirectory resolves ${manifest.server} to ${expectedServerDirectory}, but the manifest is at ${serverDirectory}`));
|
|
72
94
|
}
|
|
73
|
-
return {
|
|
95
|
+
return Effect.succeed({
|
|
74
96
|
clientDirectory: resolve(resolvedRootDirectory, manifest.client),
|
|
75
97
|
fetchHandlerPath: resolve(serverDirectory, manifest.serverEntry),
|
|
76
|
-
};
|
|
98
|
+
});
|
|
77
99
|
};
|
|
78
|
-
const readBuildPaths = (manifestPath, rootDirectory) => Effect.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
cause
|
|
82
|
-
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
const readBuildPaths = (manifestPath, rootDirectory) => Effect.gen(function* () {
|
|
101
|
+
const content = yield* Effect.tryPromise({
|
|
102
|
+
try: () => readFile(manifestPath, 'utf8'),
|
|
103
|
+
catch: cause => new Error(`could not read Foldkit build manifest at ${manifestPath}`, {
|
|
104
|
+
cause,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
const manifest = yield* decodeManifest(content);
|
|
108
|
+
return yield* buildPaths(manifestPath, manifest, rootDirectory);
|
|
109
|
+
});
|
|
110
|
+
const loadFetchHandler = (fetchHandlerPath) => Effect.gen(function* () {
|
|
111
|
+
const loaded = yield* Effect.tryPromise({
|
|
112
|
+
try: () => import(pathToFileURL(fetchHandlerPath).href),
|
|
113
|
+
catch: cause => new Error(`could not load the Foldkit fetch handler at ${fetchHandlerPath}. Run \`vite build\` first.`, { cause }),
|
|
114
|
+
});
|
|
115
|
+
if (!isFetchModule(loaded)) {
|
|
116
|
+
return yield* Effect.fail(new Error(`${fetchHandlerPath} must default-export a Web fetch handler`));
|
|
117
|
+
}
|
|
118
|
+
return loaded.default;
|
|
119
|
+
});
|
|
90
120
|
const fetchResponse = (app, request, requestUrl) => Effect.gen(function* () {
|
|
91
121
|
const signal = yield* Effect.abortSignal;
|
|
92
122
|
const webRequest = yield* HttpServerRequest.toWeb(request, { signal });
|
|
123
|
+
// NOTE: A rejected Fetch handler promise is an unhandled application
|
|
124
|
+
// defect. Expected application failures should be returned as Responses.
|
|
93
125
|
const response = yield* Effect.promise(() => app.fetch(new Request(requestUrl, webRequest)));
|
|
94
126
|
const headers = new Headers(response.headers);
|
|
95
127
|
const setCookieHeaders = headers.getSetCookie();
|
|
96
128
|
headers.delete('set-cookie');
|
|
129
|
+
const nodeResponse = NodeHttpServerRequest.toServerResponse(request);
|
|
97
130
|
if (setCookieHeaders.length > 0) {
|
|
98
|
-
|
|
131
|
+
nodeResponse.setHeader('set-cookie', setCookieHeaders);
|
|
99
132
|
}
|
|
100
133
|
const responseOptions = {
|
|
101
134
|
status: response.status,
|
|
@@ -118,7 +151,6 @@ const fetchResponse = (app, request, requestUrl) => Effect.gen(function* () {
|
|
|
118
151
|
},
|
|
119
152
|
cancel: reason => webReader.cancel(reason),
|
|
120
153
|
}));
|
|
121
|
-
const nodeResponse = NodeHttpServerRequest.toServerResponse(request);
|
|
122
154
|
const destroyBody = () => nodeBody.destroy();
|
|
123
155
|
nodeResponse.once('close', destroyBody);
|
|
124
156
|
nodeBody.once('close', () => nodeResponse.off('close', destroyBody));
|
|
@@ -131,16 +163,17 @@ const confinedStaticFiles = (clientDirectory) => Effect.gen(function* () {
|
|
|
131
163
|
const fileSystem = yield* FileSystem.FileSystem;
|
|
132
164
|
const httpPlatform = yield* HttpPlatform.HttpPlatform;
|
|
133
165
|
const root = yield* fileSystem.realPath(clientDirectory);
|
|
134
|
-
const confinedRealPath = (path) => Effect.
|
|
166
|
+
const confinedRealPath = (path) => Effect.gen(function* () {
|
|
167
|
+
const canonicalPath = yield* fileSystem.realPath(path);
|
|
135
168
|
const fromRoot = relative(root, canonicalPath);
|
|
136
169
|
const isContained = fromRoot === '' ||
|
|
137
170
|
(fromRoot !== '..' &&
|
|
138
171
|
!fromRoot.startsWith(`..${sep}`) &&
|
|
139
172
|
!isAbsolute(fromRoot));
|
|
140
173
|
if (isContained) {
|
|
141
|
-
return
|
|
174
|
+
return canonicalPath;
|
|
142
175
|
}
|
|
143
|
-
return Effect.fail(PlatformError.systemError({
|
|
176
|
+
return yield* Effect.fail(PlatformError.systemError({
|
|
144
177
|
_tag: 'NotFound',
|
|
145
178
|
module: 'FileSystem',
|
|
146
179
|
method: 'realPath',
|
|
@@ -157,13 +190,12 @@ const confinedStaticFiles = (clientDirectory) => Effect.gen(function* () {
|
|
|
157
190
|
});
|
|
158
191
|
return yield* HttpStaticServer.make({ root, index: undefined }).pipe(Effect.provideService(FileSystem.FileSystem, confinedFileSystem), Effect.provideService(HttpPlatform.HttpPlatform, confinedPlatform));
|
|
159
192
|
});
|
|
160
|
-
const makeHandler = (options) => Effect.gen(function* () {
|
|
161
|
-
const
|
|
162
|
-
const origin = Option.getOrElse(yield* options.origin, () => `http://localhost:${globalThis.String(port)}`);
|
|
193
|
+
const makeHandler = (options, port) => Effect.gen(function* () {
|
|
194
|
+
const origin = yield* originFrom(Option.getOrElse(yield* options.origin, () => `http://localhost:${globalThis.String(port)}`));
|
|
163
195
|
const manifestPath = resolve(process.cwd(), options.manifestPath ?? DEFAULT_MANIFEST_PATH);
|
|
164
196
|
const paths = yield* readBuildPaths(manifestPath, options.rootDirectory);
|
|
197
|
+
const basePath = yield* basePathFrom(options.basePath);
|
|
165
198
|
const app = yield* loadFetchHandler(paths.fetchHandlerPath);
|
|
166
|
-
const basePath = basePathFrom(options.basePath);
|
|
167
199
|
const staticFiles = yield* confinedStaticFiles(paths.clientDirectory);
|
|
168
200
|
return HttpServerRequest.HttpServerRequest.use(request => {
|
|
169
201
|
const requestUrl = Server.resolveRequestUrl(request.url, origin);
|
|
@@ -193,7 +225,7 @@ const makeHandler = (options) => Effect.gen(function* () {
|
|
|
193
225
|
return Effect.provideService(response, HttpServerRequest.HttpServerRequest, staticRequest ?? fetchRequest);
|
|
194
226
|
}).pipe(Effect.interruptible);
|
|
195
227
|
});
|
|
196
|
-
const serverLayer = (options) => Layer.unwrap(Effect.map(makeHandler(options), handler => HttpServer.serve(handler))).pipe(HttpServer.withLogAddress, Layer.provide(NodeHttpServer.
|
|
228
|
+
const serverLayer = (options, port) => Layer.unwrap(Effect.map(makeHandler(options, port), handler => HttpServer.serve(handler))).pipe(HttpServer.withLogAddress, Layer.provide(NodeHttpServer.layer(createServer, { port })));
|
|
197
229
|
/**
|
|
198
230
|
* Starts a Node HTTP server for a built Foldkit application.
|
|
199
231
|
*
|
|
@@ -201,4 +233,4 @@ const serverLayer = (options) => Layer.unwrap(Effect.map(makeHandler(options), h
|
|
|
201
233
|
* and HEAD requests, and sends every other application request to the emitted
|
|
202
234
|
* Web Fetch handler. Interrupting the returned Effect closes the HTTP server.
|
|
203
235
|
*/
|
|
204
|
-
export const serve = (options) => Layer.launch(serverLayer(options));
|
|
236
|
+
export const serve = (options) => Effect.flatMap(options.port, port => Layer.launch(serverLayer(options, port)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldkit/node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.1.0-canary.49dc36ef672d",
|
|
4
4
|
"description": "Node.js host adapter for Foldkit server-rendered applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"effect": "4.0.0",
|
|
24
|
-
"foldkit": "
|
|
24
|
+
"foldkit": "0.167.0-canary.49dc36ef672d"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "26.4.0",
|
|
28
28
|
"effect": "4.0.0",
|
|
29
|
+
"foldkit": "0.167.0-canary.49dc36ef672d",
|
|
29
30
|
"rimraf": "^6.1.3",
|
|
30
31
|
"typescript": "^7.0.2",
|
|
31
|
-
"vitest": "^4.1.11"
|
|
32
|
-
"foldkit": "0.166.0"
|
|
32
|
+
"vitest": "^4.1.11"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"foldkit",
|