@foldkit/vite-plugin 0.26.1-canary.085b787da0a5 → 0.26.1-canary.6bd9ee8e728c

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
@@ -65,53 +65,24 @@ With this set, the dev server converts HTML page requests to Web `Request` value
65
65
 
66
66
  Vite retains ownership of configured proxy routes before Foldkit handles application requests. Vite's `server.cors` option applies to Vite-owned source modules, assets, and HMR. It does not add headers to application responses or answer their preflights. Preflight ownership follows `Access-Control-Request-Method`, so a preflight for an application `POST` reaches `renderPage` even when its path looks like an asset. An `OPTIONS` request without both `Origin` and `Access-Control-Request-Method` is not a preflight and also reaches `renderPage`. Define application CORS in `renderPage`, where development and the deployed host share one policy. Vite's `allowedHosts` check runs before proxy and application handling, including `OPTIONS` and methods the Web `Request` API cannot represent.
67
67
 
68
- ## Server-rendered documents
69
-
70
- With `ssr.build`, the client input is a script and the server entry produces the complete HTML document. Configure the script beside the server entry:
71
-
72
- ```typescript
73
- foldkit({
74
- ssr: {
75
- clientEntry: '/src/entry.ts',
76
- serverEntry: '/src/entry.server.ts',
77
- build: true,
78
- },
79
- })
80
- ```
81
-
82
- Import CSS from the client script. Export `renderDocument` alongside `renderPage` in the server entry:
83
-
84
- ```typescript
85
- export const renderDocument = Server.renderDocument
86
- ```
87
-
88
- `Server.renderDocument(application, assets, options)` assembles the document with the application's title, language, direction, canonical URL, and Open Graph URL. `assets` contains the emitted `entryScript`, ordered `stylesheets`, and `modulePreloads`. The plugin collects static imports and their CSS, leaves lazy imports to Vite's runtime, and supplies the same assets to request-time rendering and prerendering. The default document includes UTF-8 and viewport metadata. Wrap the helper to set a default `lang` or add trusted author-owned `head` markup, such as a favicon. Never interpolate unescaped request data into `head`.
89
-
90
- The client build starts from a script rather than HTML. `index.html` appears in the client output only when prerendering generates `/`. The build refuses an existing root document copied from `publicDir`, emitted by another plugin, or left by an earlier build when `emptyOutDir` is disabled. Remove the source `index.html` when migrating an SSR build, move its stylesheet links into client imports, and move document tags into `renderDocument`. Build-time `transformIndexHtml` hooks do not run with a script input. In development, Vite transforms the rendered document for HMR and dev HTML hooks. `containerId` belongs only to a template-based custom host and cannot be combined with `clientEntry`.
91
-
92
- Use a root-relative `clientEntry`, such as `/src/entry.ts`, and an absolute-path or full-URL Vite `base`. Relative bases (`''` and `'./'`) are rejected because their asset URLs would resolve differently on nested routes. `modulePreload` configuration and absolute `experimental.renderBuiltUrl` results apply to the generated document; relative and runtime `renderBuiltUrl` results are rejected.
93
-
94
- The standalone build plugin takes `foldkitBuild(serverEntry, { clientEntry, ...options })`. Omitting `clientEntry` and `ssr.build` selects the template-based development host for a custom build pipeline. Hosts that own an HTML template can use the lower-level `injectIntoTemplate`, `toResponse`, and `handleRequest(request, { template, renderPage })` APIs.
95
-
96
68
  ## Foldkit package resolution
97
69
 
98
70
  Each module graph must load one Foldkit copy. The plugin configures Vite for that:
99
71
 
100
72
  - `resolve.dedupe` lists `foldkit`, `@foldkit/ui`, and `@foldkit/devtools`, each only when it resolves from the application root.
101
- - In server builds and in every server environment of the dev server, the plugin bundles those three packages, plus every installed package whose `dependencies` or `peerDependencies` include `foldkit` or an `@foldkit/*` package, such as `@foldkit/markdown`. The plugin lists them in `resolve.noExternal`, which Vite applies to every environment, and in `ssr.noExternal`. In a Node server environment of the dev server, these packages run through Vite's module runner instead of Node's own import. An explicit `ssr.external` or `resolve.external` entry still keeps a package external.
73
+ - Server builds and the dev server's server render bundle those three packages, plus every installed package whose `dependencies` or `peerDependencies` include `foldkit` or an `@foldkit/*` package, such as `@foldkit/markdown`. In the dev server, these `ssr.noExternal` packages run through Vite's module runner instead of Node's own import. An explicit `ssr.external` entry still keeps a package external.
102
74
  - The plugin finds these packages by crawling from the application's `package.json`. It follows the application's `dependencies` and `devDependencies`, then the `dependencies` of each package it bundles, plus the `devDependencies` of a bundled package that is a private workspace package.
103
- - The plugin excludes `foldkit` from dependency pre-bundling in every environment, so the build id transform runs on it. It also pre-bundles the Effect entries Foldkit imports in every environment whose optimizer is enabled, for example a Cloudflare Worker environment, so Foldkit and the application share one Effect instance. Applications need no `optimizeDeps` settings for Foldkit.
104
75
 
105
- A package the crawl does not reach stays external. For example: a peer the application does not declare, or a package reached only through a package that does not depend on Foldkit. Such a package loads a second Foldkit copy from `node_modules` at runtime. Declare it in the application's `package.json`, or add it to `resolve.noExternal`, which Vite applies to every environment:
76
+ A package the crawl does not reach stays external. For example: a peer the application does not declare, or a package reached only through a package that does not depend on Foldkit. Such a package loads a second Foldkit copy from `node_modules` at runtime. Declare it in the application's `package.json`, or add it to `ssr.noExternal`:
106
77
 
107
78
  ```typescript
108
79
  export default defineConfig({
109
80
  plugins: [foldkit()],
110
- resolve: { noExternal: ['foldkit-component-library'] },
81
+ ssr: { noExternal: ['foldkit-component-library'] },
111
82
  })
112
83
  ```
113
84
 
114
- Vitest copies each environment's `resolve.noExternal` into `server.deps.inline`. A Vitest config that includes `foldkit()` therefore also inlines the crawled packages in tests.
85
+ Vitest copies SSR `noExternal` into `server.deps.inline`. A Vitest config that includes `foldkit()` therefore also inlines the crawled packages in tests.
115
86
 
116
87
  ## Completed build metadata
117
88
 
package/dist/build.d.ts CHANGED
@@ -15,15 +15,25 @@ export type FoldkitPrerenderOptions = Readonly<{
15
15
  * from `Request.url`, in which case it should match the published origin.
16
16
  */
17
17
  origin?: string;
18
+ /**
19
+ * The `id` of the placeholder element in `index.html` the rendered markup
20
+ * replaces. Defaults to `'root'`, and the aggregate plugin passes whatever
21
+ * `ssr.containerId` names, so a renamed container is renamed once.
22
+ */
23
+ containerId?: string;
18
24
  }>;
19
25
  /** How `vite build` builds a server entry and what it generates from it. */
20
26
  export type FoldkitBuildOptions = Readonly<{
21
- /** Root-relative browser script entry resolved by Vite, such as `'/src/entry.ts'`. Import stylesheets from this module. */
22
- clientEntry: string;
23
27
  /** Where the browser build is written. */
24
28
  clientOutDir?: string;
25
29
  /** Where the server build is written. */
26
30
  serverOutDir?: string;
31
+ /**
32
+ * The `id` of the empty container in `index.html` the fetch handler
33
+ * replaces. Defaults to `'root'`. The aggregate plugin copies
34
+ * `ssr.containerId` here.
35
+ */
36
+ containerId?: string;
27
37
  /**
28
38
  * Generate static HTML for a set of URLs after both builds. `true` takes the
29
39
  * paths from the entry's `prerenderPaths` export.
@@ -124,9 +134,8 @@ export declare const renderTargetFor: (clientDirectory: string, path: string, or
124
134
  *
125
135
  * Vite builds both environments, so a deployment target that runs `vite build`
126
136
  * gets the browser and server bundles. The `fetch` handler and generated pages
127
- * use the server entry's `renderDocument` export with the emitted browser assets.
128
- * The browser build has a script input and emits no HTML until prerendering.
129
- * The server bundle's default export is `{ fetch }`.
137
+ * use the HTML emitted by the browser build, but the unrendered template is not
138
+ * published with the assets. The server bundle's default export is `{ fetch }`.
130
139
  */
131
- export declare const foldkitBuild: (serverEntry: string, options: FoldkitBuildOptions) => Plugin<FoldkitBuildApi>;
140
+ export declare const foldkitBuild: (serverEntry: string, options?: FoldkitBuildOptions) => Plugin<FoldkitBuildApi>;
132
141
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,MAAM,EAAE,MAAM,QAAQ,CAAA;AAS1C,OAAO,QAA8B,MAAM,WAAW,CAAA;AAEtD,OAAO,KAAK,EAGV,MAAM,EAGP,MAAM,MAAM,CAAA;AAEb,mEAAmE;AACnE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,2HAA2H;IAC3H,WAAW,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAA;CAC9C,CAAC,CAAA;AAEF,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,0BAA0B,CAAA;AAE9D;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;IAC/B;;;;OAIG;;IAEH;;;;OAIG;;IAEH,+EAA+E;;IAE/E,2DAA2D;;IAE3D,6EAA6E;;EAE7E,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB;IAC/B,+CAA+C;;IAE/C,kDAAkD;;IAElD,iDAAiD;;IAEjD,kDAAkD;;IAElD,0DAA0D;;QAtC1D;;;;WAIG;;QAEH;;;;WAIG;;QAEH,+EAA+E;;QAE/E,2DAA2D;;QAE3D,6EAA6E;;;EAwB7E,CAAA;AAEF,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,aAAa,EAAE,OAAO,uBAAuB,CAAA;IAC7C;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;CAC7C,CAAC,CAAA;AAcF,eAAO,MAAM,YAAY,SACjB,MAAM,aACD,MAAM,YACR,OAAO,QAAQ,KACvB,MAQF,CAAA;AA6FD,eAAO,MAAM,eAAe,oBACT,MAAM,QACjB,MAAM,UACJ,MAAM,YACL,OAAO,QAAQ,KACvB,QAAQ,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA0CrC,CAAA;AAqLD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,gBACV,MAAM,WACV,mBAAmB,KAC3B,MAAM,CAAC,eAAe,CAwSxB,CAAA"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAI/B,OAAO,QAA8B,MAAM,WAAW,CAAA;AAEtD,OAAO,KAAK,EAGV,MAAM,EAEP,MAAM,MAAM,CAAA;AAEb,mEAAmE;AACnE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAA;CAC9C,CAAC,CAAA;AAEF,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,0BAA0B,CAAA;AAE9D;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;IAC/B;;;;OAIG;;IAEH;;;;OAIG;;IAEH,+EAA+E;;IAE/E,2DAA2D;;IAE3D,6EAA6E;;EAE7E,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB;IAC/B,+CAA+C;;IAE/C,kDAAkD;;IAElD,iDAAiD;;IAEjD,kDAAkD;;IAElD,0DAA0D;;QAtC1D;;;;WAIG;;QAEH;;;;WAIG;;QAEH,+EAA+E;;QAE/E,2DAA2D;;QAE3D,6EAA6E;;;EAwB7E,CAAA;AAEF,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,aAAa,EAAE,OAAO,uBAAuB,CAAA;IAC7C;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;CAC7C,CAAC,CAAA;AAcF,eAAO,MAAM,YAAY,SACjB,MAAM,aACD,MAAM,YACR,OAAO,QAAQ,KACvB,MAQF,CAAA;AAyFD,eAAO,MAAM,eAAe,oBACT,MAAM,QACjB,MAAM,UACJ,MAAM,YACL,OAAO,QAAQ,KACvB,QAAQ,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA0CrC,CAAA;AAsFD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,gBACV,MAAM,YACV,mBAAmB,KAC3B,MAAM,CAAC,eAAe,CAqPxB,CAAA"}
package/dist/build.js CHANGED
@@ -1,7 +1,6 @@
1
- import { Predicate, Schema } from 'effect';
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { existsSync } from 'node:fs';
4
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { Schema } from 'effect';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
5
4
  import nodePath, { dirname, resolve } from 'node:path';
6
5
  import { pathToFileURL } from 'node:url';
7
6
  /** Vite module id of the fetch handler Foldkit emits as the server entry. */
@@ -65,15 +64,12 @@ export const manifestPath = (root, directory, pathApi = nodePath) => {
65
64
  return related.split(pathApi.sep).join('/');
66
65
  };
67
66
  const MANIFEST_FILE_NAME = 'foldkit.build.json';
67
+ const TEMPLATE_FILE_NAME = 'index.html';
68
68
  const DEFAULT_CLIENT_OUT_DIR = 'dist/client';
69
69
  const DEFAULT_SERVER_OUT_DIR = 'dist/server';
70
70
  const DEFAULT_PRERENDER_ORIGIN = 'http://localhost';
71
71
  const FETCH_CHUNK_NAME = 'fetch';
72
72
  const RESOLVED_FETCH_MODULE_ID = `\0${FOLDKIT_FETCH_MODULE_ID}`;
73
- const CLIENT_MODULE_ID = 'virtual:foldkit/client';
74
- const RESOLVED_CLIENT_MODULE_ID = `\0${CLIENT_MODULE_ID}`;
75
- const CLIENT_CHUNK_NAME = 'app';
76
- const VITE_CONSOLIDATED_CSS_ORIGINAL_NAME = 'style.css';
77
73
  // The chunk built from the configured entry, by name rather than by position.
78
74
  //
79
75
  // An SSR environment can carry more than one input, and prerendering imports
@@ -172,104 +168,39 @@ const prerenderOptionsFrom = (prerender) => {
172
168
  }
173
169
  return prerender === true ? {} : prerender;
174
170
  };
175
- const documentHash = (document) => createHash('sha256').update(document).digest('hex');
176
- const collectDocumentAssets = (outputs, config) => {
177
- const entryFile = serverEntryFile(outputs, CLIENT_CHUNK_NAME);
178
- const chunks = new Map(outputs.flatMap(output => output.type === 'chunk' ? [[output.fileName, output]] : []));
179
- const visited = new Set();
180
- const stylesheets = new Set();
181
- const modulePreloads = new Set();
182
- const visit = (fileName) => {
183
- if (visited.has(fileName)) {
184
- return;
185
- }
186
- visited.add(fileName);
187
- const chunk = chunks.get(fileName);
188
- if (chunk === undefined) {
189
- return;
190
- }
191
- for (const imported of chunk.imports) {
192
- visit(imported);
193
- }
194
- for (const stylesheet of chunk.viteMetadata?.importedCss ?? []) {
195
- stylesheets.add(stylesheet);
196
- }
197
- if (fileName !== entryFile) {
198
- modulePreloads.add(fileName);
199
- }
200
- };
201
- visit(entryFile);
202
- if (!config.build.cssCodeSplit) {
203
- for (const output of outputs) {
204
- if (output.type === 'asset' &&
205
- output.originalFileNames.includes(VITE_CONSOLIDATED_CSS_ORIGINAL_NAME)) {
206
- stylesheets.add(output.fileName);
207
- }
208
- }
209
- }
210
- const { modulePreload } = config.build;
211
- const preloadFiles = modulePreload === false
212
- ? []
213
- : (modulePreload.resolveDependencies?.(entryFile, [...modulePreloads], {
214
- hostId: 'index.html',
215
- hostType: 'html',
216
- }) ?? [...modulePreloads]);
217
- const assetUrl = (fileName) => {
218
- if (/^([a-z]+:)?\/\//.test(fileName)) {
219
- return fileName;
220
- }
221
- const builtUrl = config.experimental.renderBuiltUrl?.(fileName, {
222
- hostId: 'index.html',
223
- hostType: 'html',
224
- type: 'asset',
225
- ssr: false,
226
- });
227
- if (Predicate.isString(builtUrl)) {
228
- if (!builtUrl.startsWith('/') && !/^https?:\/\//.test(builtUrl)) {
229
- throw new Error('[foldkit] renderBuiltUrl must return an absolute URL or root-relative URL for document assets.');
230
- }
231
- return builtUrl;
232
- }
233
- if (builtUrl?.runtime !== undefined || builtUrl?.relative === true) {
234
- throw new Error('[foldkit] document assets cannot use runtime or relative renderBuiltUrl results.');
235
- }
236
- return `${config.base}${encodeURI(fileName).replace(/[?#]/g, encodeURIComponent)}`;
237
- };
238
- return Object.freeze({
239
- entryScript: assetUrl(entryFile),
240
- stylesheets: Object.freeze([...stylesheets].map(assetUrl)),
241
- modulePreloads: Object.freeze(preloadFiles.map(assetUrl)),
242
- });
243
- };
244
- const fetchModuleSource = (serverEntry, assets) => {
171
+ const fetchModuleSource = (serverEntry, template, containerId) => {
172
+ const containerLiteral = containerId === undefined ? 'undefined' : JSON.stringify(containerId);
245
173
  // NOTE: `export *` re-exports whatever the application entry actually names,
246
174
  // so a missing `prerenderPaths` is absent rather than a Vite undefined-import
247
175
  // warning.
248
176
  return `${[
249
177
  `import { handleRequest } from 'foldkit/experimental/server'`,
250
- `import { renderDocument } from ${JSON.stringify(serverEntry)}`,
251
178
  `import * as server from ${JSON.stringify(serverEntry)}`,
252
179
  `export * from ${JSON.stringify(serverEntry)}`,
253
- `const assets = ${JSON.stringify(assets)}`,
254
- `Object.freeze(assets.stylesheets)`,
255
- `Object.freeze(assets.modulePreloads)`,
256
- `Object.freeze(assets)`,
180
+ `const template = ${JSON.stringify(template)}`,
181
+ `const containerId = ${containerLiteral}`,
257
182
  `export default {`,
258
183
  ` fetch(request) {`,
259
184
  ` return handleRequest(request, {`,
260
185
  ` renderPage: server.renderPage,`,
261
- ` renderDocument: application => renderDocument(application, assets),`,
186
+ ` template,`,
187
+ ` containerId,`,
262
188
  ` })`,
263
189
  ` },`,
264
190
  `}`,
265
191
  ``,
266
192
  ].join('\n')}`;
267
193
  };
268
- const assetsForFetchModule = (assets) => {
269
- if (assets === undefined) {
270
- throw new Error('[foldkit] the browser build has not emitted its script entry. Build the "client" environment before "ssr" so the document can reference the emitted assets.');
194
+ // The template is what the browser build emitted in this same `vite build`,
195
+ // never a file on disk: `dist/client/index.html` could only be the previous
196
+ // build's shell with its old asset hashes, and the source `index.html` still
197
+ // names `/src/entry.ts`. Either would bundle into a handler that serves a
198
+ // page which cannot hydrate, from a build that reported success.
199
+ const templateForFetchModule = (capturedTemplate) => {
200
+ if (capturedTemplate === undefined) {
201
+ throw new Error(`[foldkit] the browser build has not emitted ${TEMPLATE_FILE_NAME}, so the fetch handler has no template to render into. Build the "client" environment before "ssr", and give the client an HTML entry.`);
271
202
  }
272
- return assets;
203
+ return capturedTemplate;
273
204
  };
274
205
  /**
275
206
  * Builds a Web `fetch` handler alongside the browser build, and generates
@@ -277,21 +208,21 @@ const assetsForFetchModule = (assets) => {
277
208
  *
278
209
  * Vite builds both environments, so a deployment target that runs `vite build`
279
210
  * gets the browser and server bundles. The `fetch` handler and generated pages
280
- * use the server entry's `renderDocument` export with the emitted browser assets.
281
- * The browser build has a script input and emits no HTML until prerendering.
282
- * The server bundle's default export is `{ fetch }`.
211
+ * use the HTML emitted by the browser build, but the unrendered template is not
212
+ * published with the assets. The server bundle's default export is `{ fetch }`.
283
213
  */
284
- export const foldkitBuild = (serverEntry, options) => {
214
+ export const foldkitBuild = (serverEntry, options = {}) => {
285
215
  const state = {};
286
216
  let metadata;
287
217
  const clientOutDir = options.clientOutDir ?? DEFAULT_CLIENT_OUT_DIR;
288
218
  const serverOutDir = options.serverOutDir ?? DEFAULT_SERVER_OUT_DIR;
289
219
  const prerender = prerenderOptionsFrom(options.prerender ?? false);
220
+ const containerId = prerender?.containerId ?? options.containerId;
290
221
  // Prerendering imports the server bundle and runs it in the build process,
291
222
  // with the build's own privileges. That module is the application's own code
292
223
  // and its dependencies, built from the configured entry, and is trusted on
293
224
  // exactly those terms. Nothing here is imported when prerendering is off.
294
- const generatePages = async (builder, clientDirectory, serverDirectory, entryFileName) => {
225
+ const generatePages = async (builder, template, clientDirectory, serverDirectory, entryFileName) => {
295
226
  if (prerender === undefined) {
296
227
  return [];
297
228
  }
@@ -311,19 +242,15 @@ export const foldkitBuild = (serverEntry, options) => {
311
242
  if (paths === undefined) {
312
243
  throw new Error(`[foldkit] cannot generate pages: "${entry}" exports no prerenderPaths and the build configured no paths.`);
313
244
  }
314
- if (!Predicate.isFunction(entry.renderDocument)) {
315
- throw new Error(`[foldkit] "${entryFileName}" exports no renderDocument function.`);
316
- }
317
- const assets = assetsForFetchModule(state.assets);
245
+ const { injectIntoTemplate } = await import('foldkit/experimental/server');
318
246
  for (const path of paths) {
319
247
  const { url, file } = renderTargetFor(clientDirectory, path, origin);
320
248
  const result = await entry.renderPage(new Request(url));
321
- const html = entry.renderDocument(renderedApplication(path, result), assets);
249
+ const html = injectIntoTemplate(template(), renderedApplication(path, result), prerender.containerId === undefined
250
+ ? undefined
251
+ : { containerId: prerender.containerId });
322
252
  await mkdir(dirname(file), { recursive: true });
323
253
  await writeFile(file, html);
324
- if (path === '/') {
325
- state.prerenderedRootHash = documentHash(html);
326
- }
327
254
  builder.config.logger.info(` generated ${path}`);
328
255
  }
329
256
  return paths;
@@ -337,16 +264,15 @@ export const foldkitBuild = (serverEntry, options) => {
337
264
  if (state.serverEntryFile === undefined) {
338
265
  throw new Error('[foldkit] the server environment produced no entry chunk, so there is nothing to deploy or generate from.');
339
266
  }
340
- const rootDocumentPath = resolve(clientDirectory, 'index.html');
341
- if (existsSync(rootDocumentPath) &&
342
- documentHash(await readFile(rootDocumentPath, 'utf8')) !==
343
- state.prerenderedRootHash) {
344
- throw new Error('[foldkit] the browser output contains index.html before prerendering. Remove it from publicDir or the plugin that emits it, and clear stale output when emptyOutDir is disabled. Only prerendering may generate the root document in an ssr.build output.');
345
- }
346
- const prerendered = await generatePages(builder, clientDirectory, serverDirectory, state.serverEntryFile);
347
- if (!prerendered.includes('/') && existsSync(rootDocumentPath)) {
348
- throw new Error('[foldkit] index.html remains in the browser output, but this build did not prerender "/". Clear the previous root document before building without it.');
349
- }
267
+ // Read only when a page is actually generated: a build that generates
268
+ // nothing has no use for an HTML entry and must not require one.
269
+ const template = () => {
270
+ if (state.template === undefined) {
271
+ throw new Error(`[foldkit] the browser build emitted no ${TEMPLATE_FILE_NAME} to generate pages from. Prerendering needs an HTML entry.`);
272
+ }
273
+ return state.template;
274
+ };
275
+ const prerendered = await generatePages(builder, template, clientDirectory, serverDirectory, state.serverEntryFile);
350
276
  const manifest = FoldkitBuildManifest.make({
351
277
  schemaVersion: MANIFEST_SCHEMA_VERSION,
352
278
  client: manifestPath(builder.config.root, clientDirectory),
@@ -385,17 +311,9 @@ export const foldkitBuild = (serverEntry, options) => {
385
311
  order: 'pre',
386
312
  handler() {
387
313
  if (this.environment.name === 'client') {
388
- delete state.assets;
314
+ delete state.template;
389
315
  delete state.serverEntryFile;
390
316
  metadata = undefined;
391
- const { input } = this.environment.config.build.rolldownOptions;
392
- const inputs = Predicate.isString(input)
393
- ? [input]
394
- : Object.values(input ?? {});
395
- if (!inputs.includes(CLIENT_MODULE_ID) ||
396
- inputs.some(entry => /\.html(?:[?#]|$)/.test(entry))) {
397
- throw new Error('[foldkit] ssr.build owns the browser script input. Configure ssr.clientEntry instead of an HTML input or a replacement client input.');
398
- }
399
317
  }
400
318
  else if (this.environment.name === 'ssr') {
401
319
  delete state.serverEntryFile;
@@ -404,27 +322,20 @@ export const foldkitBuild = (serverEntry, options) => {
404
322
  },
405
323
  },
406
324
  resolveId(id) {
407
- if (id === CLIENT_MODULE_ID) {
408
- return RESOLVED_CLIENT_MODULE_ID;
409
- }
410
325
  if (id === FOLDKIT_FETCH_MODULE_ID) {
411
326
  return RESOLVED_FETCH_MODULE_ID;
412
327
  }
413
328
  return undefined;
414
329
  },
415
330
  load(id) {
416
- if (id === RESOLVED_CLIENT_MODULE_ID) {
417
- const { modulePreload } = this.environment.config.build;
418
- const polyfill = modulePreload !== false && modulePreload.polyfill
419
- ? "import 'vite/modulepreload-polyfill'\n"
420
- : '';
421
- return `${polyfill}import ${JSON.stringify(options.clientEntry)}\n`;
422
- }
423
331
  if (id !== RESOLVED_FETCH_MODULE_ID) {
424
332
  return;
425
333
  }
426
- return fetchModuleSource(serverEntry, assetsForFetchModule(state.assets));
334
+ const template = templateForFetchModule(state.template);
335
+ return fetchModuleSource(serverEntry, template, containerId);
427
336
  },
337
+ // NOTE: `order: 'post'` because Vite's own HTML plugin emits `index.html`
338
+ // from a `generateBundle` of its own; post is guaranteed to run after it.
428
339
  generateBundle: {
429
340
  order: 'post',
430
341
  handler(_options, bundle) {
@@ -435,15 +346,17 @@ export const foldkitBuild = (serverEntry, options) => {
435
346
  if (this.environment.name !== 'client') {
436
347
  return;
437
348
  }
438
- state.assets = collectDocumentAssets(Object.values(bundle), this.environment.config);
349
+ const html = bundle[TEMPLATE_FILE_NAME];
350
+ if (html === undefined || html.type !== 'asset') {
351
+ return;
352
+ }
353
+ state.template = String(html.source);
354
+ delete bundle[TEMPLATE_FILE_NAME];
439
355
  },
440
356
  },
441
357
  config: userConfig => {
442
358
  const client = {
443
- build: {
444
- outDir: clientOutDir,
445
- rolldownOptions: { input: { [CLIENT_CHUNK_NAME]: CLIENT_MODULE_ID } },
446
- },
359
+ build: { outDir: clientOutDir },
447
360
  };
448
361
  const ssr = {
449
362
  build: {
@@ -474,16 +387,6 @@ export const foldkitBuild = (serverEntry, options) => {
474
387
  }
475
388
  : { environments: { client, ssr } };
476
389
  },
477
- configResolved(config) {
478
- if (config.base === '' || config.base === './') {
479
- throw new Error('[foldkit] ssr.build requires an absolute URL or root-relative base, such as "/" or "/app/". A relative base cannot locate browser assets consistently across server-rendered routes.');
480
- }
481
- if (!options.clientEntry?.startsWith('/') ||
482
- options.clientEntry.startsWith('//') ||
483
- /\.html(?:[?#]|$)/.test(options.clientEntry)) {
484
- throw new Error("[foldkit] ssr.build requires clientEntry to name a browser script. Move stylesheet imports into that script and document markup into the server entry's renderDocument export.");
485
- }
486
- },
487
390
  // The composable finalization point. `order: 'post'` runs this after the
488
391
  // config-level orchestrator — the host's, or the default above — no matter
489
392
  // where this plugin sits in the plugin list, which a wrapped
@@ -12,6 +12,6 @@ export declare const isFoldkitSingletonPackageSpecifier: (specifier: string) =>
12
12
  */
13
13
  export declare const crawlFoldkitPackages: (root: string, isBuild: boolean, viteUserConfig: UserConfig) => Promise<Readonly<{
14
14
  dedupe: Array<string>;
15
- noExternal: Array<string>;
15
+ ssrNoExternal: Array<string>;
16
16
  }>>;
17
17
  //# sourceMappingURL=foldkitPackages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"foldkitPackages.d.ts","sourceRoot":"","sources":["../src/foldkitPackages.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAA0B,MAAM,MAAM,CAAA;AAS9D;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,cAClC,MAAM,KAChB,OAKA,CAAA;AA8CH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,SACzB,MAAM,WACH,OAAO,kBACA,UAAU,KACzB,OAAO,CAAC,QAAQ,CAAC;IAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAoCxE,CAAA"}
1
+ {"version":3,"file":"foldkitPackages.d.ts","sourceRoot":"","sources":["../src/foldkitPackages.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAA0B,MAAM,MAAM,CAAA;AAS9D;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,cAClC,MAAM,KAChB,OAKA,CAAA;AA8CH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,SACzB,MAAM,WACH,OAAO,kBACA,UAAU,KACzB,OAAO,CACR,QAAQ,CAAC;IAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAAC,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAqClE,CAAA"}
@@ -58,11 +58,11 @@ export const crawlFoldkitPackages = async (root, isBuild, viteUserConfig) => {
58
58
  viteUserConfig,
59
59
  isSemiFrameworkPkgByJson: dependsOnFoldkit,
60
60
  });
61
- const noExternal = Array.dedupe([
61
+ const ssrNoExternal = Array.dedupe([
62
62
  ...FOLDKIT_SINGLETON_PACKAGES,
63
63
  ...crawl.ssr.noExternal,
64
64
  ]);
65
65
  const maybeResolvableSingletons = await Promise.all(Array.map(FOLDKIT_SINGLETON_PACKAGES, async (packageName) => pipe(await findDepPkgJsonPath(packageName, crawlRoot), Option.fromUndefinedOr, Option.as(packageName))));
66
66
  const dedupe = Array.getSomes(maybeResolvableSingletons);
67
- return { dedupe, noExternal };
67
+ return { dedupe, ssrNoExternal };
68
68
  };
package/dist/index.d.ts CHANGED
@@ -25,10 +25,7 @@ export type FoldkitPluginOptions = Readonly<{
25
25
  * `ssr.serverEntry`. When `undefined` (the default), the dev server
26
26
  * serves the client entry only.
27
27
  */
28
- ssr?: Omit<FoldkitSsrOptions, 'buildId' | 'quietStandDown' | 'clientEntry' | 'containerId'> & (Readonly<{
29
- /** Root-relative browser script for the server entry's code-rendered document. */
30
- clientEntry: string;
31
- containerId?: never;
28
+ ssr?: Omit<FoldkitSsrOptions, 'buildId' | 'quietStandDown'> & Readonly<{
32
29
  /**
33
30
  * Build a Web `fetch` handler alongside the browser build, and generate
34
31
  * static HTML from the server entry, inside this project's own
@@ -38,13 +35,8 @@ export type FoldkitPluginOptions = Readonly<{
38
35
  *
39
36
  * When this is absent, `vite build` builds the browser bundle only.
40
37
  */
41
- build?: boolean | Omit<FoldkitBuildOptions, 'clientEntry'>;
42
- }> | Readonly<{
43
- /** Use a custom template-based development and build pipeline. */
44
- clientEntry?: never;
45
- containerId?: string;
46
- build?: false;
47
- }>);
38
+ build?: boolean | FoldkitBuildOptions;
39
+ }>;
48
40
  /**
49
41
  * An explicit identity for the deployment this build belongs to. Foldkit
50
42
  * normally generates an opaque identity when one Vite app build coordinates
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAGV,MAAM,EAIP,MAAM,MAAM,CAAA;AAOb,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,YAAY,CAAA;AAKnE,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,UAAU,CAAA;AAG7D,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,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;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAChC;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,IAAI,CACR,iBAAiB,EACjB,SAAS,GAAG,gBAAgB,GAAG,aAAa,GAAG,aAAa,CAC7D,GACC,CACI,QAAQ,CAAC;QACP,kFAAkF;QAClF,WAAW,EAAE,MAAM,CAAA;QACnB,WAAW,CAAC,EAAE,KAAK,CAAA;QACnB;;;;;;;;WAQG;QACH,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,mBAAmB,EAAE,aAAa,CAAC,CAAA;KAC3D,CAAC,GACF,QAAQ,CAAC;QACP,kEAAkE;QAClE,WAAW,CAAC,EAAE,KAAK,CAAA;QACnB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,KAAK,CAAC,EAAE,KAAK,CAAA;KACd,CAAC,CACL,CAAA;IACH;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAC,CAAA;AAg6BF,eAAO,MAAM,OAAO,aAAa,oBAAoB,KAAQ,KAAK,CAAC,MAAM,CA+IxE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAEV,MAAM,EAIP,MAAM,MAAM,CAAA;AAOb,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,YAAY,CAAA;AAKnE,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,UAAU,CAAA;AAG7D,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,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;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAChC;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,SAAS,GAAG,gBAAgB,CAAC,GACzD,QAAQ,CAAC;QACP;;;;;;;;WAQG;QACH,KAAK,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAA;KACtC,CAAC,CAAA;IACJ;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAC,CAAA;AAs6BF,eAAO,MAAM,OAAO,aAAa,oBAAoB,KAAQ,KAAK,CAAC,MAAM,CAyHxE,CAAA"}
package/dist/index.js CHANGED
@@ -19,12 +19,12 @@ export { FOLDKIT_FETCH_MODULE_ID, FoldkitBuildManifest, FoldkitBuildMetadata, fo
19
19
  export { foldkitSsr } from './ssr.js';
20
20
  export { foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
21
21
  // NOTE: Vite does not scan imports through `foldkit` because the plugin
22
- // excludes the package from optimization in every environment. A consumer can
23
- // import only Effect subpaths while Foldkit's compiled distribution imports the
24
- // bare barrel, so include both the barrel and every top-level namespace Foldkit
25
- // imports. This keeps them in one optimized dependency graph. Over-inclusion is
26
- // harmless; under-inclusion is the bug. `scripts/check-effect-prebundle.ts`
27
- // keeps the entries in sync with Foldkit's source and runs in `pnpm check`.
22
+ // excludes the package from optimization. A consumer can import only Effect
23
+ // subpaths while Foldkit's compiled distribution imports the bare barrel, so
24
+ // include both the barrel and every top-level namespace Foldkit imports. This
25
+ // keeps them in one optimized dependency graph. Over-inclusion is harmless;
26
+ // under-inclusion is the bug. `scripts/check-effect-prebundle.ts` keeps the
27
+ // entries in sync with Foldkit's source and runs in `pnpm check`.
28
28
  const FORCE_INCLUDED_EFFECT_ENTRIES = [
29
29
  'effect',
30
30
  'effect/Array',
@@ -70,16 +70,6 @@ const FORCE_INCLUDED_EFFECT_ENTRIES = [
70
70
  'effect/SubscriptionRef',
71
71
  'effect/Types',
72
72
  ];
73
- // NOTE: Adding includes to an environment whose optimizer is otherwise disabled
74
- // turns on Vite's explicit optimizer, which would pre-bundle Effect in Vite's
75
- // default Node `ssr` environment.
76
- const shouldForceEffectEntries = (name, config) => {
77
- const consumer = config.consumer ?? (name === 'client' ? 'client' : 'server');
78
- const isClientEnvironment = consumer === 'client';
79
- const isDiscoveryEnabled = config.optimizeDeps?.noDiscovery === false;
80
- const isExplicitOptimizationEnabled = Array.isArrayNonEmpty(config.optimizeDeps?.include ?? []);
81
- return (isClientEnvironment || isDiscoveryEnabled || isExplicitOptimizationEnabled);
82
- };
83
73
  const Event = Data.taggedEnum();
84
74
  const makeState = Effect.gen(function* () {
85
75
  const preservedModels = yield* Ref.make(HashMap.empty());
@@ -484,6 +474,27 @@ const main = (server, events, options) => Effect.gen(function* () {
484
474
  * an array; Vite flattens nested plugin arrays, so `plugins: [foldkit()]`
485
475
  * keeps working.
486
476
  */
477
+ // The container is named once, on `ssr`, and reaches both the dev host and the
478
+ // build from there. A `build.prerender` that names its own wins, so a project
479
+ // that needs them to differ still can.
480
+ const withContainerId = (build, containerId) => {
481
+ const options = build === true ? {} : build;
482
+ if (containerId === undefined) {
483
+ return options;
484
+ }
485
+ const withContainer = { ...options, containerId };
486
+ if (options.prerender === undefined) {
487
+ return withContainer;
488
+ }
489
+ const prerender = options.prerender === true ? {} : options.prerender;
490
+ if (prerender === false) {
491
+ return withContainer;
492
+ }
493
+ return {
494
+ ...withContainer,
495
+ prerender: { containerId, ...prerender },
496
+ };
497
+ };
487
498
  const relayRegistryLayer = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, NodeCrypto.layer);
488
499
  export const foldkit = (options = {}) => {
489
500
  // NOTE: During a Vite restart, old and new servers overlap. Separate queues
@@ -502,15 +513,11 @@ export const foldkit = (options = {}) => {
502
513
  const reloadPlugin = {
503
514
  name: 'foldkit',
504
515
  apply: 'serve',
505
- // NOTE: The `post` order runs this hook after every default-order
506
- // `configEnvironment` hook, so the predicate sees discovery or includes
507
- // that another plugin turns on there.
508
- configEnvironment: {
509
- order: 'post',
510
- handler: (name, config) => shouldForceEffectEntries(name, config)
511
- ? { optimizeDeps: { include: [...FORCE_INCLUDED_EFFECT_ENTRIES] } }
512
- : undefined,
513
- },
516
+ config: () => ({
517
+ optimizeDeps: {
518
+ include: [...FORCE_INCLUDED_EFFECT_ENTRIES],
519
+ },
520
+ }),
514
521
  configureServer: server => {
515
522
  const events = Effect.runSync(Queue.unbounded());
516
523
  // NOTE: The default ConfigProvider snapshots the environment. Create a
@@ -540,27 +547,24 @@ export const foldkit = (options = {}) => {
540
547
  config: async (userConfig, { command }) => {
541
548
  const foldkitPackages = await crawlFoldkitPackages(userConfig.root ?? process.cwd(), command === 'build', userConfig);
542
549
  return {
550
+ optimizeDeps: {
551
+ exclude: ['foldkit'],
552
+ },
543
553
  resolve: {
544
554
  dedupe: foldkitPackages.dedupe,
545
- noExternal: foldkitPackages.noExternal,
546
555
  },
547
556
  ssr: {
548
- noExternal: foldkitPackages.noExternal,
557
+ noExternal: foldkitPackages.ssrNoExternal,
549
558
  },
550
559
  environments: {
551
560
  ssr: {
552
561
  resolve: {
553
- noExternal: foldkitPackages.noExternal,
562
+ noExternal: foldkitPackages.ssrNoExternal,
554
563
  },
555
564
  },
556
565
  },
557
566
  };
558
567
  },
559
- configEnvironment: () => ({
560
- optimizeDeps: {
561
- exclude: ['foldkit'],
562
- },
563
- }),
564
568
  };
565
569
  const shared = [
566
570
  resolutionPlugin,
@@ -573,9 +577,6 @@ export const foldkit = (options = {}) => {
573
577
  return shared;
574
578
  }
575
579
  const { build, ...ssr } = options.ssr;
576
- if (ssr.clientEntry !== undefined && ssr.containerId !== undefined) {
577
- throw new Error('[foldkit] containerId belongs to an HTML template. A clientEntry build takes its complete document from renderDocument instead.');
578
- }
579
580
  const servePages = foldkitSsr({
580
581
  ...ssr,
581
582
  ...(options.buildId === undefined ? {} : { buildId: options.buildId }),
@@ -584,15 +585,9 @@ export const foldkit = (options = {}) => {
584
585
  if (build === undefined || build === false) {
585
586
  return [...shared, servePages];
586
587
  }
587
- if (ssr.clientEntry === undefined) {
588
- throw new Error('[foldkit] ssr.build requires ssr.clientEntry to name the browser script and the server entry to export renderDocument.');
589
- }
590
588
  return [
591
589
  ...shared,
592
590
  servePages,
593
- foldkitBuild(ssr.serverEntry, {
594
- ...(build === true ? {} : build),
595
- clientEntry: ssr.clientEntry,
596
- }),
591
+ foldkitBuild(ssr.serverEntry, withContainerId(build, ssr.containerId)),
597
592
  ];
598
593
  };
package/dist/ssr.d.ts CHANGED
@@ -8,12 +8,6 @@ export type FoldkitSsrOptions = Readonly<{
8
8
  * `Promise<EntryResult>`.
9
9
  */
10
10
  serverEntry: string;
11
- /**
12
- * Root-relative browser script entry for a code-rendered document. The server entry must
13
- * also export `renderDocument(application, assets)`. When absent, the dev
14
- * host uses `index.html` for a custom template-based build pipeline.
15
- */
16
- clientEntry?: string;
17
11
  /**
18
12
  * The `id` of the empty container element in `index.html` the rendered
19
13
  * markup replaces. Defaults to `'root'`.
package/dist/ssr.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,MAAM,EAGP,MAAM,MAAM,CAAA;AAIb,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC,CAAA;AA6tBF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,YAAa,iBAAiB,KAAG,MAyGvD,CAAA"}
1
+ {"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,MAAM,EAGP,MAAM,MAAM,CAAA;AAIb,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC,CAAA;AAosBF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,YAAa,iBAAiB,KAAG,MAgEvD,CAAA"}
package/dist/ssr.js CHANGED
@@ -132,32 +132,19 @@ const renderDecision = (nodeRequest, requestUrl) => {
132
132
  const renderRequest = (server, options, nodeRequest, requestUrl) => Effect.gen(function* () {
133
133
  const { pathname, search } = new URL(requestUrl);
134
134
  const route = `${pathname}${search}`;
135
+ const rawTemplate = yield* Effect.promise(() => readFile(resolve(server.config.root, 'index.html'), 'utf-8'));
136
+ // NOTE: the first argument tells Vite where the HTML lives, and Vite
137
+ // resolves the template's relative URLs (such as a `./src/entry.ts`
138
+ // script) against it. The template always lives at the site root, so
139
+ // that argument must stay `/index.html` no matter which route is being
140
+ // rendered. The third argument, named `originalUrl` in Vite's signature,
141
+ // carries the route actually being requested.
142
+ const template = yield* Effect.promise(() => server.transformIndexHtml('/index.html', rawTemplate, route));
135
143
  const loadedModule = yield* Effect.promise(() => server.ssrLoadModule(options.serverEntry));
136
144
  if (!isEntryModule(loadedModule)) {
137
145
  return yield* Effect.die(new Error(`[foldkit] '${options.serverEntry}' does not export a renderPage function, so the dev server cannot render pages.`));
138
146
  }
139
147
  const result = yield* Effect.promise(() => loadedModule.renderPage(toWebRequest(requestUrl, nodeRequest)));
140
- if (result._tag === 'Responded') {
141
- return result.response;
142
- }
143
- if (options.clientEntry !== undefined) {
144
- if (!('renderDocument' in loadedModule) ||
145
- !Predicate.isFunction(loadedModule.renderDocument)) {
146
- return yield* Effect.die(new Error(`[foldkit] '${options.serverEntry}' must export renderDocument(application, assets) when clientEntry is configured.`));
147
- }
148
- const assets = Server.DocumentAssets.make({
149
- entryScript: options.clientEntry,
150
- stylesheets: [],
151
- modulePreloads: [],
152
- });
153
- const document = loadedModule.renderDocument(result.application, assets);
154
- const transformed = yield* Effect.promise(() => server.transformIndexHtml(pathname, document, route));
155
- return Server.toResponse(() => transformed, result);
156
- }
157
- const rawTemplate = yield* Effect.promise(() => readFile(resolve(server.config.root, 'index.html'), 'utf-8'));
158
- // NOTE: Vite resolves template-relative URLs against the first argument;
159
- // the template-based host's index lives at the root for nested page requests.
160
- const template = yield* Effect.promise(() => server.transformIndexHtml('/index.html', rawTemplate, route));
161
148
  return Server.toResponse(template, result, options.containerId === undefined
162
149
  ? {}
163
150
  : { containerId: options.containerId });
@@ -474,32 +461,9 @@ const renderMiddleware = (server, options, requestUrls, stateByRequest) => (node
474
461
  * responses. Server entry edits take effect without a restart.
475
462
  */
476
463
  export const foldkitSsr = (options) => {
477
- let isDevelopmentHost = false;
478
464
  return {
479
465
  name: 'foldkit-ssr',
480
- configResolved(config) {
481
- if (!isDevelopmentHost || options.clientEntry === undefined) {
482
- return;
483
- }
484
- if (!options.clientEntry.startsWith('/') ||
485
- options.clientEntry.startsWith('//') ||
486
- /\.html(?:[?#]|$)/.test(options.clientEntry)) {
487
- throw new Error('[foldkit] clientEntry must be a root-relative browser script URL, such as "/src/entry.ts".');
488
- }
489
- if (options.containerId !== undefined) {
490
- throw new Error('[foldkit] containerId is only used by the template-based dev host. A code-rendered document owns its application placement.');
491
- }
492
- if (config.base === '' || config.base === './') {
493
- throw new Error('[foldkit] code-rendered documents require an absolute URL or root-relative base so browser assets resolve consistently on nested routes.');
494
- }
495
- },
496
- config: (config, { command, isPreview }) => {
497
- isDevelopmentHost = command === 'serve' && isPreview !== true;
498
- if (isDevelopmentHost &&
499
- options.clientEntry !== undefined &&
500
- (config.base === '' || config.base === './')) {
501
- throw new Error('[foldkit] code-rendered documents require an absolute URL or root-relative base so browser assets resolve consistently on nested routes.');
502
- }
466
+ config: (_config, { command, isPreview }) => {
503
467
  const buildId = buildIdForCommand(command, options.buildId);
504
468
  return {
505
469
  // NOTE: `vite preview` also resolves with command 'serve', but it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/vite-plugin",
3
- "version": "0.26.1-canary.085b787da0a5",
3
+ "version": "0.26.1-canary.6bd9ee8e728c",
4
4
  "description": "Vite plugin for Foldkit with state-preserving live reload",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "peerDependencies": {
19
19
  "effect": "4.0.0",
20
- "foldkit": "0.166.0-canary.085b787da0a5",
20
+ "foldkit": "0.166.0-canary.6bd9ee8e728c",
21
21
  "vite": "^8.0.0"
22
22
  },
23
23
  "dependencies": {
@@ -38,7 +38,7 @@
38
38
  "vite": "^8.3.1",
39
39
  "vite-host": "npm:vite@8.2.1",
40
40
  "vitest": "^4.1.11",
41
- "foldkit": "0.166.0-canary.085b787da0a5"
41
+ "foldkit": "0.166.0-canary.6bd9ee8e728c"
42
42
  },
43
43
  "keywords": [
44
44
  "vite",