@foldkit/vite-plugin 0.13.1 → 0.15.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/README.md +10 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/ssr.d.ts +27 -0
- package/dist/ssr.d.ts.map +1 -0
- package/dist/ssr.js +153 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -53,6 +53,16 @@ The plugin uses Vite's WebSocket connection to communicate between the dev serve
|
|
|
53
53
|
|
|
54
54
|
Model is preserved across hot reloads but cleared on manual browser refreshes, giving you control over when to reset your app.
|
|
55
55
|
|
|
56
|
+
## Server rendering dev host
|
|
57
|
+
|
|
58
|
+
Pass `ssr` with the path to your server entry to render page requests through it during development:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
plugins: [foldkit({ ssr: { serverEntry: '/src/entry.server.ts' } })]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
With this set, the dev server converts HTML page requests to Web `Request` values, passes them to the entry's `renderPage`, and serves the returned `Response`. Vite continues to serve the client entry, HMR, and assets, and the server entry runs through Vite's module graph, so edits to it apply without a restart. The client side of the handoff needs no plugin configuration: a server-rendered application's client entry calls `Runtime.hydrate` instead of `Runtime.run`, and hydrate adopts the served HTML in place. The option shapes only the dev server; production hosts import the built server entry themselves. See the [Server Rendering documentation](https://foldkit.dev/core/server-rendering) for the full contract.
|
|
65
|
+
|
|
56
66
|
## DevTools overlay
|
|
57
67
|
|
|
58
68
|
When `@foldkit/devtools` is installed as a development dependency, the plugin mounts its overlay automatically during development and leaves it out of production builds. No application import or `devTools.overlay` field is needed.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
+
import { type FoldkitSsrOptions } from './ssr.js';
|
|
2
3
|
export { type BrandDistResult, brandDistDirectory } from './brandDist.js';
|
|
4
|
+
export { type FoldkitSsrOptions, foldkitSsr } from './ssr.js';
|
|
3
5
|
export { type ViewIdentityTransformResult, foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
|
|
4
6
|
/** Options for the `foldkit` Vite plugin. */
|
|
5
7
|
export type FoldkitPluginOptions = Readonly<{
|
|
@@ -10,6 +12,13 @@ export type FoldkitPluginOptions = Readonly<{
|
|
|
10
12
|
* the Foldkit DevTools MCP server.
|
|
11
13
|
*/
|
|
12
14
|
devToolsMcpPort?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Serve server-rendered pages from the Vite dev server. When set, `vite`
|
|
17
|
+
* passes HTML navigations that fall through Vite, plus non-GET requests, to
|
|
18
|
+
* `renderPage` from the module at `ssr.serverEntry`. When `undefined` (the
|
|
19
|
+
* default), the dev server serves the client entry only.
|
|
20
|
+
*/
|
|
21
|
+
ssr?: FoldkitSsrOptions;
|
|
13
22
|
}>;
|
|
14
23
|
/**
|
|
15
24
|
* Foldkit's Vite plugin set: the view-identity branding transform and
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EACV,MAAM,EAIP,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EACV,MAAM,EAIP,MAAM,MAAM,CAAA;AAIb,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,UAAU,CAAA;AAG7D,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAA;CACxB,CAAC,CAAA;AAilBF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAAI,UAAS,oBAAyB,KAAG,KAAK,CAAC,MAAM,CAoExE,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,10 @@ import { createRequire } from 'node:module';
|
|
|
5
5
|
import { resolve } from 'node:path';
|
|
6
6
|
import { WebSocketServer } from 'ws';
|
|
7
7
|
import { devToolsOverlayPlugin } from './devToolsOverlay.js';
|
|
8
|
+
import { foldkitSsr } from './ssr.js';
|
|
8
9
|
import { foldkitViewIdentity } from './viewIdentity.js';
|
|
9
10
|
export { brandDistDirectory } from './brandDist.js';
|
|
11
|
+
export { foldkitSsr } from './ssr.js';
|
|
10
12
|
export { foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
|
|
11
13
|
// NOTE: Vite's dep optimizer scans the consumer's source for `effect`
|
|
12
14
|
// imports and pre-bundles only those exports into a single `effect.js`
|
|
@@ -374,5 +376,12 @@ export const foldkit = (options = {}) => {
|
|
|
374
376
|
return [];
|
|
375
377
|
},
|
|
376
378
|
};
|
|
377
|
-
return
|
|
379
|
+
return options.ssr === undefined
|
|
380
|
+
? [foldkitViewIdentity(), devToolsOverlayPlugin(), hmrPlugin]
|
|
381
|
+
: [
|
|
382
|
+
foldkitViewIdentity(),
|
|
383
|
+
devToolsOverlayPlugin(),
|
|
384
|
+
hmrPlugin,
|
|
385
|
+
foldkitSsr(options.ssr),
|
|
386
|
+
];
|
|
378
387
|
};
|
package/dist/ssr.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
/** Options for serving server-rendered pages from the Vite dev server. */
|
|
3
|
+
export type FoldkitSsrOptions = Readonly<{
|
|
4
|
+
/**
|
|
5
|
+
* Module path of the server entry, resolved by Vite (e.g.
|
|
6
|
+
* `'/src/entry.server.ts'`). The module must export a `renderPage`
|
|
7
|
+
* function taking a Web `Request` and returning a
|
|
8
|
+
* `Promise<EntryResult>`.
|
|
9
|
+
*/
|
|
10
|
+
serverEntry: string;
|
|
11
|
+
/**
|
|
12
|
+
* The `id` of the empty container element in `index.html` the rendered
|
|
13
|
+
* markup replaces. Defaults to `'root'`.
|
|
14
|
+
*/
|
|
15
|
+
containerId?: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Serves server-rendered pages from the Vite dev server.
|
|
19
|
+
*
|
|
20
|
+
* Registered after Vite's own middleware, so the client entry, HMR, and
|
|
21
|
+
* assets are untouched. HTML navigations that fall through, plus non-GET
|
|
22
|
+
* requests, load the server entry through Vite's SSR module loader, call its
|
|
23
|
+
* `renderPage` with a Web `Request`, and send the resulting Web `Response`.
|
|
24
|
+
* Server entry edits take effect without a restart.
|
|
25
|
+
*/
|
|
26
|
+
export declare const foldkitSsr: (options: FoldkitSsrOptions) => Plugin;
|
|
27
|
+
//# sourceMappingURL=ssr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAW,MAAM,EAAiB,MAAM,MAAM,CAAA;AAE1D,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAyLF;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,iBAAiB,KAAG,MA+CtD,CAAA"}
|
package/dist/ssr.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Array, Effect, Predicate } from 'effect';
|
|
2
|
+
import { Server } from 'foldkit/experimental';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
const isEntryModule = (loadedModule) => Predicate.isObject(loadedModule) &&
|
|
7
|
+
Predicate.hasProperty(loadedModule, 'renderPage') &&
|
|
8
|
+
Predicate.isFunction(loadedModule.renderPage);
|
|
9
|
+
const toWebRequest = (url, nodeRequest) => {
|
|
10
|
+
const headers = new Headers();
|
|
11
|
+
for (const [name, value] of Object.entries(nodeRequest.headers)) {
|
|
12
|
+
if (Predicate.isString(value)) {
|
|
13
|
+
headers.set(name, value);
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value) {
|
|
17
|
+
headers.append(name, item);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const host = nodeRequest.headers.host ?? 'localhost';
|
|
22
|
+
const method = nodeRequest.method ?? 'GET';
|
|
23
|
+
const requestInit = { headers, method };
|
|
24
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
25
|
+
// NOTE: Node and the DOM library declare structurally different
|
|
26
|
+
// ReadableStream interfaces even though Node's `Readable.toWeb` returns
|
|
27
|
+
// the Web stream implementation that `Request` consumes at runtime.
|
|
28
|
+
requestInit.body = Readable.toWeb(nodeRequest);
|
|
29
|
+
requestInit.duplex = 'half';
|
|
30
|
+
}
|
|
31
|
+
return new Request(new URL(url, `http://${host}`), requestInit);
|
|
32
|
+
};
|
|
33
|
+
const renderDecision = (nodeRequest) => {
|
|
34
|
+
const method = nodeRequest.method ?? 'GET';
|
|
35
|
+
const url = nodeRequest.originalUrl ?? nodeRequest.url ?? '/';
|
|
36
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
37
|
+
if (Server.resolvesToIndexHtml(url)) {
|
|
38
|
+
return 'Render';
|
|
39
|
+
}
|
|
40
|
+
const accept = nodeRequest.headers.accept;
|
|
41
|
+
return Server.acceptsHtml(Predicate.isString(accept) ? accept : undefined)
|
|
42
|
+
? 'RenderVaryAccept'
|
|
43
|
+
: 'RefusedVaryAccept';
|
|
44
|
+
}
|
|
45
|
+
if (method === 'OPTIONS' || method === 'TRACE') {
|
|
46
|
+
return 'Skip';
|
|
47
|
+
}
|
|
48
|
+
return 'Render';
|
|
49
|
+
};
|
|
50
|
+
const renderRequest = (server, options, nodeRequest) => Effect.gen(function* () {
|
|
51
|
+
const url = nodeRequest.originalUrl ?? nodeRequest.url ?? '/';
|
|
52
|
+
const rawTemplate = yield* Effect.promise(() => readFile(resolve(server.config.root, 'index.html'), 'utf-8'));
|
|
53
|
+
// NOTE: the first argument tells Vite where the HTML lives, and Vite
|
|
54
|
+
// resolves the template's relative URLs (such as a `./src/entry.ts`
|
|
55
|
+
// script) against it. The template always lives at the site root, so
|
|
56
|
+
// that argument must stay `/index.html` no matter which route is being
|
|
57
|
+
// rendered. The third argument, named `originalUrl` in Vite's signature,
|
|
58
|
+
// carries the route actually being requested.
|
|
59
|
+
const template = yield* Effect.promise(() => server.transformIndexHtml('/index.html', rawTemplate, url));
|
|
60
|
+
const loadedModule = yield* Effect.promise(() => server.ssrLoadModule(options.serverEntry));
|
|
61
|
+
if (!isEntryModule(loadedModule)) {
|
|
62
|
+
return yield* Effect.die(new Error(`[foldkit] '${options.serverEntry}' does not export a renderPage function, so the dev server cannot render pages.`));
|
|
63
|
+
}
|
|
64
|
+
const result = yield* Effect.promise(() => loadedModule.renderPage(toWebRequest(url, nodeRequest)));
|
|
65
|
+
return Server.toResponse(template, result, options.containerId === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: { containerId: options.containerId });
|
|
68
|
+
});
|
|
69
|
+
const varyHeaderValue = (value) => {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.join(', ');
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
|
78
|
+
const sendWebResponse = async (webResponse, nodeRequest, nodeResponse, varyAccept) => {
|
|
79
|
+
nodeResponse.statusCode = webResponse.status;
|
|
80
|
+
if (webResponse.statusText !== '') {
|
|
81
|
+
nodeResponse.statusMessage = webResponse.statusText;
|
|
82
|
+
}
|
|
83
|
+
const getSetCookie = Predicate.hasProperty(webResponse.headers, 'getSetCookie')
|
|
84
|
+
? webResponse.headers.getSetCookie
|
|
85
|
+
: undefined;
|
|
86
|
+
const setCookieHeaders = Predicate.isFunction(getSetCookie)
|
|
87
|
+
? getSetCookie.call(webResponse.headers)
|
|
88
|
+
: [];
|
|
89
|
+
for (const [name, value] of webResponse.headers) {
|
|
90
|
+
if (name !== 'set-cookie' || Array.isArrayEmpty(setCookieHeaders)) {
|
|
91
|
+
nodeResponse.setHeader(name, value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArrayNonEmpty(setCookieHeaders)) {
|
|
95
|
+
nodeResponse.setHeader('set-cookie', setCookieHeaders);
|
|
96
|
+
}
|
|
97
|
+
if (varyAccept) {
|
|
98
|
+
// Merge into whatever Vary already sits on the node response (the app's
|
|
99
|
+
// own, plus Vite's own Vary: Origin), so declaring Accept does not drop it.
|
|
100
|
+
nodeResponse.setHeader('vary', Server.varyWithAccept(varyHeaderValue(nodeResponse.getHeader('vary'))));
|
|
101
|
+
}
|
|
102
|
+
if (nodeRequest.method === 'HEAD' || webResponse.body === null) {
|
|
103
|
+
nodeResponse.end();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const body = new Uint8Array(await webResponse.arrayBuffer());
|
|
107
|
+
nodeResponse.end(body);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Serves server-rendered pages from the Vite dev server.
|
|
112
|
+
*
|
|
113
|
+
* Registered after Vite's own middleware, so the client entry, HMR, and
|
|
114
|
+
* assets are untouched. HTML navigations that fall through, plus non-GET
|
|
115
|
+
* requests, load the server entry through Vite's SSR module loader, call its
|
|
116
|
+
* `renderPage` with a Web `Request`, and send the resulting Web `Response`.
|
|
117
|
+
* Server entry edits take effect without a restart.
|
|
118
|
+
*/
|
|
119
|
+
export const foldkitSsr = (options) => ({
|
|
120
|
+
name: 'foldkit-ssr',
|
|
121
|
+
// NOTE: `vite preview` also resolves with command 'serve', but it serves
|
|
122
|
+
// built output and never runs configureServer, so applying there would
|
|
123
|
+
// only set appType 'custom' and strip preview's HTML middleware.
|
|
124
|
+
apply: (_config, env) => env.command === 'serve' && env.isPreview !== true,
|
|
125
|
+
config: () => ({ appType: 'custom' }),
|
|
126
|
+
configureServer: server => () => {
|
|
127
|
+
server.middlewares.use((nodeRequest, nodeResponse, next) => {
|
|
128
|
+
const decision = renderDecision(nodeRequest);
|
|
129
|
+
if (decision === 'Skip') {
|
|
130
|
+
next();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (decision === 'RefusedVaryAccept') {
|
|
134
|
+
// The 404 depends on Accept (an HTML-accepting client would have
|
|
135
|
+
// rendered), so it varies on Accept for a shared cache, the same as
|
|
136
|
+
// a production host's refused response.
|
|
137
|
+
nodeResponse.statusCode = 404;
|
|
138
|
+
nodeResponse.setHeader('vary', Server.varyWithAccept(varyHeaderValue(nodeResponse.getHeader('vary'))));
|
|
139
|
+
nodeResponse.end();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const varyAccept = decision === 'RenderVaryAccept';
|
|
143
|
+
void Effect.runPromise(renderRequest(server, options, nodeRequest))
|
|
144
|
+
.then(response => sendWebResponse(response, nodeRequest, nodeResponse, varyAccept))
|
|
145
|
+
.catch((error) => {
|
|
146
|
+
if (error instanceof Error) {
|
|
147
|
+
server.ssrFixStacktrace(error);
|
|
148
|
+
}
|
|
149
|
+
next(error);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldkit/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Vite plugin for Foldkit hot module reloading with state preservation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"effect": "4.0.0-rc.
|
|
19
|
+
"effect": "4.0.0-rc.109",
|
|
20
20
|
"foldkit": "^0",
|
|
21
21
|
"vite": "^7.0.0 || ^8.0.0"
|
|
22
22
|
},
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^25.9.3",
|
|
29
29
|
"@types/ws": "^8.18.1",
|
|
30
|
-
"effect": "4.0.0-rc.
|
|
30
|
+
"effect": "4.0.0-rc.109",
|
|
31
31
|
"happy-dom": "^20.10.4",
|
|
32
32
|
"rimraf": "^6.1.3",
|
|
33
33
|
"typescript": "^6.0.3",
|
|
34
34
|
"vite": "^8.0.16",
|
|
35
35
|
"vitest": "^4.1.9",
|
|
36
|
-
"foldkit": "0.
|
|
36
|
+
"foldkit": "0.147.0"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
39
|
"vite",
|