@foldkit/vite-plugin 0.26.0 → 0.26.1-canary.085b787da0a5
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 +48 -0
- package/dist/build.d.ts +6 -15
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +146 -49
- package/dist/devToolsOverlay.d.ts.map +1 -1
- package/dist/devToolsOverlay.js +15 -22
- package/dist/foldkitPackages.d.ts +7 -2
- package/dist/foldkitPackages.d.ts.map +1 -1
- package/dist/foldkitPackages.js +51 -20
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -46
- package/dist/ssr.d.ts +6 -0
- package/dist/ssr.d.ts.map +1 -1
- package/dist/ssr.js +45 -9
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -65,6 +65,54 @@ 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
|
+
## Foldkit package resolution
|
|
97
|
+
|
|
98
|
+
Each module graph must load one Foldkit copy. The plugin configures Vite for that:
|
|
99
|
+
|
|
100
|
+
- `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.
|
|
102
|
+
- 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
|
+
|
|
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:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export default defineConfig({
|
|
109
|
+
plugins: [foldkit()],
|
|
110
|
+
resolve: { noExternal: ['foldkit-component-library'] },
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
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.
|
|
115
|
+
|
|
68
116
|
## Completed build metadata
|
|
69
117
|
|
|
70
118
|
Deployment tools that run Vite in process can read `foldkit:build` through Vite's standard plugin `api` field. Await the full application build before reading:
|
package/dist/build.d.ts
CHANGED
|
@@ -15,25 +15,15 @@ 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;
|
|
24
18
|
}>;
|
|
25
19
|
/** How `vite build` builds a server entry and what it generates from it. */
|
|
26
20
|
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;
|
|
27
23
|
/** Where the browser build is written. */
|
|
28
24
|
clientOutDir?: string;
|
|
29
25
|
/** Where the server build is written. */
|
|
30
26
|
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;
|
|
37
27
|
/**
|
|
38
28
|
* Generate static HTML for a set of URLs after both builds. `true` takes the
|
|
39
29
|
* paths from the entry's `prerenderPaths` export.
|
|
@@ -134,8 +124,9 @@ export declare const renderTargetFor: (clientDirectory: string, path: string, or
|
|
|
134
124
|
*
|
|
135
125
|
* Vite builds both environments, so a deployment target that runs `vite build`
|
|
136
126
|
* gets the browser and server bundles. The `fetch` handler and generated pages
|
|
137
|
-
* use the
|
|
138
|
-
*
|
|
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 }`.
|
|
139
130
|
*/
|
|
140
|
-
export declare const foldkitBuild: (serverEntry: string, options
|
|
131
|
+
export declare const foldkitBuild: (serverEntry: string, options: FoldkitBuildOptions) => Plugin<FoldkitBuildApi>;
|
|
141
132
|
//# sourceMappingURL=build.d.ts.map
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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"}
|
package/dist/build.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Schema } from 'effect';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
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';
|
|
4
5
|
import nodePath, { dirname, resolve } from 'node:path';
|
|
5
6
|
import { pathToFileURL } from 'node:url';
|
|
6
7
|
/** Vite module id of the fetch handler Foldkit emits as the server entry. */
|
|
@@ -64,12 +65,15 @@ export const manifestPath = (root, directory, pathApi = nodePath) => {
|
|
|
64
65
|
return related.split(pathApi.sep).join('/');
|
|
65
66
|
};
|
|
66
67
|
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';
|
|
73
77
|
// The chunk built from the configured entry, by name rather than by position.
|
|
74
78
|
//
|
|
75
79
|
// An SSR environment can carry more than one input, and prerendering imports
|
|
@@ -168,39 +172,104 @@ const prerenderOptionsFrom = (prerender) => {
|
|
|
168
172
|
}
|
|
169
173
|
return prerender === true ? {} : prerender;
|
|
170
174
|
};
|
|
171
|
-
const
|
|
172
|
-
|
|
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) => {
|
|
173
245
|
// NOTE: `export *` re-exports whatever the application entry actually names,
|
|
174
246
|
// so a missing `prerenderPaths` is absent rather than a Vite undefined-import
|
|
175
247
|
// warning.
|
|
176
248
|
return `${[
|
|
177
249
|
`import { handleRequest } from 'foldkit/experimental/server'`,
|
|
250
|
+
`import { renderDocument } from ${JSON.stringify(serverEntry)}`,
|
|
178
251
|
`import * as server from ${JSON.stringify(serverEntry)}`,
|
|
179
252
|
`export * from ${JSON.stringify(serverEntry)}`,
|
|
180
|
-
`const
|
|
181
|
-
`
|
|
253
|
+
`const assets = ${JSON.stringify(assets)}`,
|
|
254
|
+
`Object.freeze(assets.stylesheets)`,
|
|
255
|
+
`Object.freeze(assets.modulePreloads)`,
|
|
256
|
+
`Object.freeze(assets)`,
|
|
182
257
|
`export default {`,
|
|
183
258
|
` fetch(request) {`,
|
|
184
259
|
` return handleRequest(request, {`,
|
|
185
260
|
` renderPage: server.renderPage,`,
|
|
186
|
-
`
|
|
187
|
-
` containerId,`,
|
|
261
|
+
` renderDocument: application => renderDocument(application, assets),`,
|
|
188
262
|
` })`,
|
|
189
263
|
` },`,
|
|
190
264
|
`}`,
|
|
191
265
|
``,
|
|
192
266
|
].join('\n')}`;
|
|
193
267
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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.`);
|
|
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.');
|
|
202
271
|
}
|
|
203
|
-
return
|
|
272
|
+
return assets;
|
|
204
273
|
};
|
|
205
274
|
/**
|
|
206
275
|
* Builds a Web `fetch` handler alongside the browser build, and generates
|
|
@@ -208,21 +277,21 @@ const templateForFetchModule = (capturedTemplate) => {
|
|
|
208
277
|
*
|
|
209
278
|
* Vite builds both environments, so a deployment target that runs `vite build`
|
|
210
279
|
* gets the browser and server bundles. The `fetch` handler and generated pages
|
|
211
|
-
* use the
|
|
212
|
-
*
|
|
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 }`.
|
|
213
283
|
*/
|
|
214
|
-
export const foldkitBuild = (serverEntry, options
|
|
284
|
+
export const foldkitBuild = (serverEntry, options) => {
|
|
215
285
|
const state = {};
|
|
216
286
|
let metadata;
|
|
217
287
|
const clientOutDir = options.clientOutDir ?? DEFAULT_CLIENT_OUT_DIR;
|
|
218
288
|
const serverOutDir = options.serverOutDir ?? DEFAULT_SERVER_OUT_DIR;
|
|
219
289
|
const prerender = prerenderOptionsFrom(options.prerender ?? false);
|
|
220
|
-
const containerId = prerender?.containerId ?? options.containerId;
|
|
221
290
|
// Prerendering imports the server bundle and runs it in the build process,
|
|
222
291
|
// with the build's own privileges. That module is the application's own code
|
|
223
292
|
// and its dependencies, built from the configured entry, and is trusted on
|
|
224
293
|
// exactly those terms. Nothing here is imported when prerendering is off.
|
|
225
|
-
const generatePages = async (builder,
|
|
294
|
+
const generatePages = async (builder, clientDirectory, serverDirectory, entryFileName) => {
|
|
226
295
|
if (prerender === undefined) {
|
|
227
296
|
return [];
|
|
228
297
|
}
|
|
@@ -242,15 +311,19 @@ export const foldkitBuild = (serverEntry, options = {}) => {
|
|
|
242
311
|
if (paths === undefined) {
|
|
243
312
|
throw new Error(`[foldkit] cannot generate pages: "${entry}" exports no prerenderPaths and the build configured no paths.`);
|
|
244
313
|
}
|
|
245
|
-
|
|
314
|
+
if (!Predicate.isFunction(entry.renderDocument)) {
|
|
315
|
+
throw new Error(`[foldkit] "${entryFileName}" exports no renderDocument function.`);
|
|
316
|
+
}
|
|
317
|
+
const assets = assetsForFetchModule(state.assets);
|
|
246
318
|
for (const path of paths) {
|
|
247
319
|
const { url, file } = renderTargetFor(clientDirectory, path, origin);
|
|
248
320
|
const result = await entry.renderPage(new Request(url));
|
|
249
|
-
const html =
|
|
250
|
-
? undefined
|
|
251
|
-
: { containerId: prerender.containerId });
|
|
321
|
+
const html = entry.renderDocument(renderedApplication(path, result), assets);
|
|
252
322
|
await mkdir(dirname(file), { recursive: true });
|
|
253
323
|
await writeFile(file, html);
|
|
324
|
+
if (path === '/') {
|
|
325
|
+
state.prerenderedRootHash = documentHash(html);
|
|
326
|
+
}
|
|
254
327
|
builder.config.logger.info(` generated ${path}`);
|
|
255
328
|
}
|
|
256
329
|
return paths;
|
|
@@ -264,15 +337,16 @@ export const foldkitBuild = (serverEntry, options = {}) => {
|
|
|
264
337
|
if (state.serverEntryFile === undefined) {
|
|
265
338
|
throw new Error('[foldkit] the server environment produced no entry chunk, so there is nothing to deploy or generate from.');
|
|
266
339
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
276
350
|
const manifest = FoldkitBuildManifest.make({
|
|
277
351
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
278
352
|
client: manifestPath(builder.config.root, clientDirectory),
|
|
@@ -311,9 +385,17 @@ export const foldkitBuild = (serverEntry, options = {}) => {
|
|
|
311
385
|
order: 'pre',
|
|
312
386
|
handler() {
|
|
313
387
|
if (this.environment.name === 'client') {
|
|
314
|
-
delete state.
|
|
388
|
+
delete state.assets;
|
|
315
389
|
delete state.serverEntryFile;
|
|
316
390
|
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
|
+
}
|
|
317
399
|
}
|
|
318
400
|
else if (this.environment.name === 'ssr') {
|
|
319
401
|
delete state.serverEntryFile;
|
|
@@ -322,20 +404,27 @@ export const foldkitBuild = (serverEntry, options = {}) => {
|
|
|
322
404
|
},
|
|
323
405
|
},
|
|
324
406
|
resolveId(id) {
|
|
407
|
+
if (id === CLIENT_MODULE_ID) {
|
|
408
|
+
return RESOLVED_CLIENT_MODULE_ID;
|
|
409
|
+
}
|
|
325
410
|
if (id === FOLDKIT_FETCH_MODULE_ID) {
|
|
326
411
|
return RESOLVED_FETCH_MODULE_ID;
|
|
327
412
|
}
|
|
328
413
|
return undefined;
|
|
329
414
|
},
|
|
330
415
|
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
|
+
}
|
|
331
423
|
if (id !== RESOLVED_FETCH_MODULE_ID) {
|
|
332
424
|
return;
|
|
333
425
|
}
|
|
334
|
-
|
|
335
|
-
return fetchModuleSource(serverEntry, template, containerId);
|
|
426
|
+
return fetchModuleSource(serverEntry, assetsForFetchModule(state.assets));
|
|
336
427
|
},
|
|
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.
|
|
339
428
|
generateBundle: {
|
|
340
429
|
order: 'post',
|
|
341
430
|
handler(_options, bundle) {
|
|
@@ -346,17 +435,15 @@ export const foldkitBuild = (serverEntry, options = {}) => {
|
|
|
346
435
|
if (this.environment.name !== 'client') {
|
|
347
436
|
return;
|
|
348
437
|
}
|
|
349
|
-
|
|
350
|
-
if (html === undefined || html.type !== 'asset') {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
state.template = String(html.source);
|
|
354
|
-
delete bundle[TEMPLATE_FILE_NAME];
|
|
438
|
+
state.assets = collectDocumentAssets(Object.values(bundle), this.environment.config);
|
|
355
439
|
},
|
|
356
440
|
},
|
|
357
441
|
config: userConfig => {
|
|
358
442
|
const client = {
|
|
359
|
-
build: {
|
|
443
|
+
build: {
|
|
444
|
+
outDir: clientOutDir,
|
|
445
|
+
rolldownOptions: { input: { [CLIENT_CHUNK_NAME]: CLIENT_MODULE_ID } },
|
|
446
|
+
},
|
|
360
447
|
};
|
|
361
448
|
const ssr = {
|
|
362
449
|
build: {
|
|
@@ -387,6 +474,16 @@ export const foldkitBuild = (serverEntry, options = {}) => {
|
|
|
387
474
|
}
|
|
388
475
|
: { environments: { client, ssr } };
|
|
389
476
|
},
|
|
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
|
+
},
|
|
390
487
|
// The composable finalization point. `order: 'post'` runs this after the
|
|
391
488
|
// config-level orchestrator — the host's, or the default above — no matter
|
|
392
489
|
// where this plugin sits in the plugin list, which a wrapped
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devToolsOverlay.d.ts","sourceRoot":"","sources":["../src/devToolsOverlay.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"devToolsOverlay.d.ts","sourceRoot":"","sources":["../src/devToolsOverlay.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAoIlC;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,YAC7B,OAAO,GAAG,OAAO,QACpB,MAAM,KACX,OAQF,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,QAAO,MAmExC,CAAA"}
|
package/dist/devToolsOverlay.js
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Option, Record, Schema } from 'effect';
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
4
|
const DEV_TOOLS_PACKAGE_NAME = '@foldkit/devtools';
|
|
5
|
-
const FOLDKIT_PACKAGE_NAME = 'foldkit';
|
|
6
5
|
const DEV_TOOLS_VITE_EXPORT = './vite';
|
|
7
6
|
const DEV_TOOLS_OVERLAY_MODULE_ID = 'virtual:foldkit-devtools-overlay';
|
|
8
7
|
const RESOLVED_DEV_TOOLS_OVERLAY_MODULE_ID = `\0${DEV_TOOLS_OVERLAY_MODULE_ID}`;
|
|
9
8
|
const DEV_TOOLS_VITE_IMPORT_SPECIFIER = `${DEV_TOOLS_PACKAGE_NAME}${DEV_TOOLS_VITE_EXPORT.slice(1)}`;
|
|
10
9
|
const DEV_TOOLS_HOST_IMPORT_SPECIFIER = 'foldkit/devtools-host';
|
|
11
|
-
// NOTE: Vite's dependency scan cannot discover this virtual module's imports.
|
|
12
|
-
// Declaring registry-installed imports before the first request avoids a
|
|
13
|
-
// mid-session reoptimization and page reload.
|
|
14
|
-
const DEV_TOOLS_OVERLAY_IMPORTS = [
|
|
15
|
-
{
|
|
16
|
-
specifier: DEV_TOOLS_VITE_IMPORT_SPECIFIER,
|
|
17
|
-
packageName: DEV_TOOLS_PACKAGE_NAME,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
specifier: DEV_TOOLS_HOST_IMPORT_SPECIFIER,
|
|
21
|
-
packageName: FOLDKIT_PACKAGE_NAME,
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
10
|
const DEV_TOOLS_OVERLAY_MODULE_SOURCE = `
|
|
25
11
|
import { overlay } from '${DEV_TOOLS_VITE_IMPORT_SPECIFIER}'
|
|
26
12
|
import { __setDevToolsOverlay } from '${DEV_TOOLS_HOST_IMPORT_SPECIFIER}'
|
|
@@ -79,8 +65,7 @@ const resolvePackageDirectory = Option.liftThrowable((packageJsonPath) => realpa
|
|
|
79
65
|
// NOTE: Vite serves linked packages from source rather than pre-bundling them.
|
|
80
66
|
// A registry-installed package has a real path under `node_modules`, while a
|
|
81
67
|
// workspace link resolves to its source checkout; force-including a link would
|
|
82
|
-
// cache its current source and hide edits.
|
|
83
|
-
// because one package may be linked while the other is installed.
|
|
68
|
+
// cache its current source and hide edits.
|
|
84
69
|
const isPackageResolvedIntoNodeModules = (root, packageName) => findInstalledPackageJsonPath(root, packageName).pipe(Option.flatMap(resolvePackageDirectory), Option.exists(packageDirectory => packageDirectory.split(sep).includes('node_modules')));
|
|
85
70
|
const isDevToolsProductionDependency = (root) => findApplicationPackageJsonPath(root).pipe(Option.flatMap(packageJsonPath => readPackageJson(decodeApplicationPackageJson, packageJsonPath)), Option.flatMapNullishOr(packageJson => packageJson.dependencies), Option.exists(Record.has(DEV_TOOLS_PACKAGE_NAME)));
|
|
86
71
|
/**
|
|
@@ -111,11 +96,19 @@ export const devToolsOverlayPlugin = () => {
|
|
|
111
96
|
!shouldInjectDevToolsOverlay(environment.command, root)) {
|
|
112
97
|
return undefined;
|
|
113
98
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
99
|
+
// NOTE: Vite's dependency scan cannot discover the virtual module's
|
|
100
|
+
// imports, so a registry-installed `@foldkit/devtools/vite` is declared
|
|
101
|
+
// before the first request to avoid a mid-session reoptimization and page
|
|
102
|
+
// reload. `foldkit/devtools-host` is deliberately not declared. The plugin
|
|
103
|
+
// excludes `foldkit` and serves it from source. Vite resolves a
|
|
104
|
+
// force-included specifier to its pre-bundle before consulting `exclude`.
|
|
105
|
+
// That would give the overlay its own copy of the DevTools config.
|
|
106
|
+
if (isPackageResolvedIntoNodeModules(root, DEV_TOOLS_PACKAGE_NAME)) {
|
|
107
|
+
return { optimizeDeps: { include: [DEV_TOOLS_VITE_IMPORT_SPECIFIER] } };
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
119
112
|
},
|
|
120
113
|
configResolved: config => {
|
|
121
114
|
isInjectionEnabled = shouldInjectDevToolsOverlay(config.command, config.root);
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { type UserConfig } from 'vite';
|
|
1
2
|
/** Tests whether an import refers to a package that must share Foldkit's
|
|
2
3
|
* runtime instance.
|
|
3
4
|
*
|
|
4
5
|
* @internal
|
|
5
6
|
*/
|
|
6
7
|
export declare const isFoldkitSingletonPackageSpecifier: (specifier: string) => boolean;
|
|
7
|
-
/**
|
|
8
|
+
/** Finds the installed packages an application's server render must bundle
|
|
9
|
+
* and the Foldkit packages Vite must deduplicate.
|
|
8
10
|
*
|
|
9
11
|
* @internal
|
|
10
12
|
*/
|
|
11
|
-
export declare const
|
|
13
|
+
export declare const crawlFoldkitPackages: (root: string, isBuild: boolean, viteUserConfig: UserConfig) => Promise<Readonly<{
|
|
14
|
+
dedupe: Array<string>;
|
|
15
|
+
noExternal: Array<string>;
|
|
16
|
+
}>>;
|
|
12
17
|
//# sourceMappingURL=foldkitPackages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"foldkitPackages.d.ts","sourceRoot":"","sources":["../src/foldkitPackages.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/foldkitPackages.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Array,
|
|
2
|
-
import {
|
|
1
|
+
import { Array, Option, Record, Schema, pipe } from 'effect';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
+
import { searchForWorkspaceRoot } from 'vite';
|
|
5
|
+
import { crawlFrameworkPkgs, findDepPkgJsonPath } from 'vitefu';
|
|
4
6
|
const FOLDKIT_SINGLETON_PACKAGES = [
|
|
5
7
|
'foldkit',
|
|
6
8
|
'@foldkit/ui',
|
|
@@ -12,26 +14,55 @@ const FOLDKIT_SINGLETON_PACKAGES = [
|
|
|
12
14
|
* @internal
|
|
13
15
|
*/
|
|
14
16
|
export const isFoldkitSingletonPackageSpecifier = (specifier) => Array.some(FOLDKIT_SINGLETON_PACKAGES, packageName => specifier === packageName || specifier.startsWith(`${packageName}/`));
|
|
15
|
-
|
|
17
|
+
const isFoldkitPackageName = (name) => name === 'foldkit' || name.startsWith('@foldkit/');
|
|
18
|
+
const decodeDependencyRecord = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Unknown));
|
|
19
|
+
const dependencyNames = (field) => Option.match(decodeDependencyRecord(field), {
|
|
20
|
+
onNone: () => [],
|
|
21
|
+
onSome: Record.keys,
|
|
22
|
+
});
|
|
23
|
+
const dependsOnFoldkit = (packageJson) => pipe([packageJson['dependencies'], packageJson['peerDependencies']], Array.flatMap(dependencyNames), Array.some(isFoldkitPackageName));
|
|
24
|
+
const toRealPath = (path) => {
|
|
25
|
+
try {
|
|
26
|
+
return realpathSync(path);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const toCrawlRoot = (root, isPreservingSymlinks) => {
|
|
33
|
+
const absoluteRoot = resolve(root);
|
|
34
|
+
// NOTE: The crawl and the dedupe lookup must start from the root Vite
|
|
35
|
+
// resolves from: the path as given under resolve.preserveSymlinks, and its
|
|
36
|
+
// realpath otherwise. Any other root makes them walk a different directory
|
|
37
|
+
// chain than Vite's resolver.
|
|
38
|
+
if (isPreservingSymlinks) {
|
|
39
|
+
return absoluteRoot;
|
|
40
|
+
}
|
|
41
|
+
return toRealPath(absoluteRoot);
|
|
42
|
+
};
|
|
43
|
+
/** Finds the installed packages an application's server render must bundle
|
|
44
|
+
* and the Foldkit packages Vite must deduplicate.
|
|
16
45
|
*
|
|
17
46
|
* @internal
|
|
18
47
|
*/
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// MODULE_NOT_FOUND proves this consumer does not have the package.
|
|
32
|
-
return !(error instanceof Error &&
|
|
33
|
-
Predicate.hasProperty(error, 'code') &&
|
|
34
|
-
error.code === 'MODULE_NOT_FOUND');
|
|
35
|
-
}
|
|
48
|
+
export const crawlFoldkitPackages = async (root, isBuild, viteUserConfig) => {
|
|
49
|
+
const crawlRoot = toCrawlRoot(root, viteUserConfig.resolve?.preserveSymlinks ?? false);
|
|
50
|
+
// NOTE: vitefu realpaths each dependency's package.json before comparing it
|
|
51
|
+
// with workspaceRoot, including when Vite preserves symlinks. The workspace
|
|
52
|
+
// boundary must use the same form so private workspace packages are found.
|
|
53
|
+
const workspaceRoot = toRealPath(searchForWorkspaceRoot(crawlRoot));
|
|
54
|
+
const crawl = await crawlFrameworkPkgs({
|
|
55
|
+
root: crawlRoot,
|
|
56
|
+
workspaceRoot,
|
|
57
|
+
isBuild,
|
|
58
|
+
viteUserConfig,
|
|
59
|
+
isSemiFrameworkPkgByJson: dependsOnFoldkit,
|
|
36
60
|
});
|
|
61
|
+
const noExternal = Array.dedupe([
|
|
62
|
+
...FOLDKIT_SINGLETON_PACKAGES,
|
|
63
|
+
...crawl.ssr.noExternal,
|
|
64
|
+
]);
|
|
65
|
+
const maybeResolvableSingletons = await Promise.all(Array.map(FOLDKIT_SINGLETON_PACKAGES, async (packageName) => pipe(await findDepPkgJsonPath(packageName, crawlRoot), Option.fromUndefinedOr, Option.as(packageName))));
|
|
66
|
+
const dedupe = Array.getSomes(maybeResolvableSingletons);
|
|
67
|
+
return { dedupe, noExternal };
|
|
37
68
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,10 @@ 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'> & Readonly<{
|
|
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;
|
|
29
32
|
/**
|
|
30
33
|
* Build a Web `fetch` handler alongside the browser build, and generate
|
|
31
34
|
* static HTML from the server entry, inside this project's own
|
|
@@ -35,8 +38,13 @@ export type FoldkitPluginOptions = Readonly<{
|
|
|
35
38
|
*
|
|
36
39
|
* When this is absent, `vite build` builds the browser bundle only.
|
|
37
40
|
*/
|
|
38
|
-
build?: boolean | FoldkitBuildOptions
|
|
39
|
-
}
|
|
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
|
+
}>);
|
|
40
48
|
/**
|
|
41
49
|
* An explicit identity for the deployment this build belongs to. Foldkit
|
|
42
50
|
* normally generates an opaque identity when one Vite app build coordinates
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as NodePath from '@effect/platform-node/NodePath';
|
|
|
10
10
|
import { foldkitBuild } from './build.js';
|
|
11
11
|
import { foldkitBuildToken } from './buildToken.js';
|
|
12
12
|
import { devToolsOverlayPlugin } from './devToolsOverlay.js';
|
|
13
|
-
import {
|
|
13
|
+
import { crawlFoldkitPackages } from './foldkitPackages.js';
|
|
14
14
|
import { publishRelayRecord, retireRelayRecord } from './relayRegistry.js';
|
|
15
15
|
import { foldkitSsr } from './ssr.js';
|
|
16
16
|
import { foldkitViewIdentity } from './viewIdentity.js';
|
|
@@ -18,16 +18,15 @@ export { brandDistDirectory } from './brandDist.js';
|
|
|
18
18
|
export { FOLDKIT_FETCH_MODULE_ID, FoldkitBuildManifest, FoldkitBuildMetadata, foldkitBuild, } from './build.js';
|
|
19
19
|
export { foldkitSsr } from './ssr.js';
|
|
20
20
|
export { foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
|
|
21
|
-
// NOTE: Vite
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const FORCE_INCLUDED_EFFECT_NAMESPACES = [
|
|
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`.
|
|
28
|
+
const FORCE_INCLUDED_EFFECT_ENTRIES = [
|
|
29
|
+
'effect',
|
|
31
30
|
'effect/Array',
|
|
32
31
|
'effect/Boolean',
|
|
33
32
|
'effect/Cause',
|
|
@@ -55,6 +54,7 @@ const FORCE_INCLUDED_EFFECT_NAMESPACES = [
|
|
|
55
54
|
'effect/PubSub',
|
|
56
55
|
'effect/Queue',
|
|
57
56
|
'effect/Record',
|
|
57
|
+
'effect/Redacted',
|
|
58
58
|
'effect/Ref',
|
|
59
59
|
'effect/Result',
|
|
60
60
|
'effect/Runtime',
|
|
@@ -70,6 +70,16 @@ const FORCE_INCLUDED_EFFECT_NAMESPACES = [
|
|
|
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
|
+
};
|
|
73
83
|
const Event = Data.taggedEnum();
|
|
74
84
|
const makeState = Effect.gen(function* () {
|
|
75
85
|
const preservedModels = yield* Ref.make(HashMap.empty());
|
|
@@ -474,27 +484,6 @@ const main = (server, events, options) => Effect.gen(function* () {
|
|
|
474
484
|
* an array; Vite flattens nested plugin arrays, so `plugins: [foldkit()]`
|
|
475
485
|
* keeps working.
|
|
476
486
|
*/
|
|
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
|
-
};
|
|
498
487
|
const relayRegistryLayer = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, NodeCrypto.layer);
|
|
499
488
|
export const foldkit = (options = {}) => {
|
|
500
489
|
// NOTE: During a Vite restart, old and new servers overlap. Separate queues
|
|
@@ -513,11 +502,15 @@ export const foldkit = (options = {}) => {
|
|
|
513
502
|
const reloadPlugin = {
|
|
514
503
|
name: 'foldkit',
|
|
515
504
|
apply: 'serve',
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
},
|
|
521
514
|
configureServer: server => {
|
|
522
515
|
const events = Effect.runSync(Queue.unbounded());
|
|
523
516
|
// NOTE: The default ConfigProvider snapshots the environment. Create a
|
|
@@ -544,27 +537,30 @@ export const foldkit = (options = {}) => {
|
|
|
544
537
|
};
|
|
545
538
|
const resolutionPlugin = {
|
|
546
539
|
name: 'foldkit:resolution',
|
|
547
|
-
config: userConfig => {
|
|
548
|
-
const
|
|
540
|
+
config: async (userConfig, { command }) => {
|
|
541
|
+
const foldkitPackages = await crawlFoldkitPackages(userConfig.root ?? process.cwd(), command === 'build', userConfig);
|
|
549
542
|
return {
|
|
550
|
-
optimizeDeps: {
|
|
551
|
-
exclude: ['foldkit'],
|
|
552
|
-
},
|
|
553
543
|
resolve: {
|
|
554
|
-
dedupe:
|
|
544
|
+
dedupe: foldkitPackages.dedupe,
|
|
545
|
+
noExternal: foldkitPackages.noExternal,
|
|
555
546
|
},
|
|
556
547
|
ssr: {
|
|
557
|
-
noExternal:
|
|
548
|
+
noExternal: foldkitPackages.noExternal,
|
|
558
549
|
},
|
|
559
550
|
environments: {
|
|
560
551
|
ssr: {
|
|
561
552
|
resolve: {
|
|
562
|
-
noExternal:
|
|
553
|
+
noExternal: foldkitPackages.noExternal,
|
|
563
554
|
},
|
|
564
555
|
},
|
|
565
556
|
},
|
|
566
557
|
};
|
|
567
558
|
},
|
|
559
|
+
configEnvironment: () => ({
|
|
560
|
+
optimizeDeps: {
|
|
561
|
+
exclude: ['foldkit'],
|
|
562
|
+
},
|
|
563
|
+
}),
|
|
568
564
|
};
|
|
569
565
|
const shared = [
|
|
570
566
|
resolutionPlugin,
|
|
@@ -577,6 +573,9 @@ export const foldkit = (options = {}) => {
|
|
|
577
573
|
return shared;
|
|
578
574
|
}
|
|
579
575
|
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
|
+
}
|
|
580
579
|
const servePages = foldkitSsr({
|
|
581
580
|
...ssr,
|
|
582
581
|
...(options.buildId === undefined ? {} : { buildId: options.buildId }),
|
|
@@ -585,9 +584,15 @@ export const foldkit = (options = {}) => {
|
|
|
585
584
|
if (build === undefined || build === false) {
|
|
586
585
|
return [...shared, servePages];
|
|
587
586
|
}
|
|
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
|
+
}
|
|
588
590
|
return [
|
|
589
591
|
...shared,
|
|
590
592
|
servePages,
|
|
591
|
-
foldkitBuild(ssr.serverEntry,
|
|
593
|
+
foldkitBuild(ssr.serverEntry, {
|
|
594
|
+
...(build === true ? {} : build),
|
|
595
|
+
clientEntry: ssr.clientEntry,
|
|
596
|
+
}),
|
|
592
597
|
];
|
|
593
598
|
};
|
package/dist/ssr.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ 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;
|
|
11
17
|
/**
|
|
12
18
|
* The `id` of the empty container element in `index.html` the rendered
|
|
13
19
|
* 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;;;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;
|
|
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"}
|
package/dist/ssr.js
CHANGED
|
@@ -132,19 +132,32 @@ 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));
|
|
143
135
|
const loadedModule = yield* Effect.promise(() => server.ssrLoadModule(options.serverEntry));
|
|
144
136
|
if (!isEntryModule(loadedModule)) {
|
|
145
137
|
return yield* Effect.die(new Error(`[foldkit] '${options.serverEntry}' does not export a renderPage function, so the dev server cannot render pages.`));
|
|
146
138
|
}
|
|
147
139
|
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));
|
|
148
161
|
return Server.toResponse(template, result, options.containerId === undefined
|
|
149
162
|
? {}
|
|
150
163
|
: { containerId: options.containerId });
|
|
@@ -461,9 +474,32 @@ const renderMiddleware = (server, options, requestUrls, stateByRequest) => (node
|
|
|
461
474
|
* responses. Server entry edits take effect without a restart.
|
|
462
475
|
*/
|
|
463
476
|
export const foldkitSsr = (options) => {
|
|
477
|
+
let isDevelopmentHost = false;
|
|
464
478
|
return {
|
|
465
479
|
name: 'foldkit-ssr',
|
|
466
|
-
config
|
|
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
|
+
}
|
|
467
503
|
const buildId = buildIdForCommand(command, options.buildId);
|
|
468
504
|
return {
|
|
469
505
|
// 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.
|
|
3
|
+
"version": "0.26.1-canary.085b787da0a5",
|
|
4
4
|
"description": "Vite plugin for Foldkit with state-preserving live reload",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
],
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"effect": "4.0.0",
|
|
20
|
-
"foldkit": "
|
|
20
|
+
"foldkit": "0.166.0-canary.085b787da0a5",
|
|
21
21
|
"vite": "^8.0.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@effect/platform-node": "4.0.0",
|
|
25
25
|
"@effect/platform-node-shared": "4.0.0",
|
|
26
26
|
"magic-string": "^1.4.2",
|
|
27
|
+
"vitefu": "^1.1.3",
|
|
27
28
|
"ws": "^8.21.3"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"vite": "^8.3.1",
|
|
38
39
|
"vite-host": "npm:vite@8.2.1",
|
|
39
40
|
"vitest": "^4.1.11",
|
|
40
|
-
"foldkit": "0.
|
|
41
|
+
"foldkit": "0.166.0-canary.085b787da0a5"
|
|
41
42
|
},
|
|
42
43
|
"keywords": [
|
|
43
44
|
"vite",
|