@foldkit/vite-plugin 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ export default defineConfig({
30
30
 
31
31
  Foldkit's differ tracks two independent kinds of identity: user keys, which match siblings in dynamic lists, and a framework-managed identity, which decides whether a matched position is still the same thing. When the producing view function changes, the differ replaces the node instead of patching it, so DOM state cannot bleed across an identity change. Branches rendered inline by one view function share that function's identity and patch in place, exactly as same-type elements do in React; extracting the branches into named view functions makes them identity boundaries.
32
32
 
33
- This plugin supplies that identity. At build time, in dev and production alike, it wraps every function return in your application modules with a branding call that stamps returned vnodes with the function's id (module path plus function name), set-if-absent. Identity therefore attaches at view-function boundaries, and any branching syntax behaves the same: if/else, ternaries, Effect Match, switch statements, and pattern-matching libraries are all equivalent, because identity belongs to the function that produced the subtree, not to the branch that selected it.
33
+ This plugin supplies that identity. At build time, in dev and production alike, it wraps every function return in your application modules with a branding call that stamps returned vnodes with the function's id (module path plus function name), set-if-absent. The identity is nothing else: it ships in the client bundle, so anything derived from the module's contents would be a published check against those contents. Identity therefore attaches at view-function boundaries, and any branching syntax behaves the same: if/else, ternaries, Effect Match, switch statements, and pattern-matching libraries are all equivalent, because identity belongs to the function that produced the subtree, not to the branch that selected it.
34
34
 
35
35
  Foldkit core modules are never instrumented, and functions that never return vnodes are wrapped inertly. Builds without this plugin fall back to positional matching plus keys, where branch points need hand-written keys.
36
36
 
@@ -53,6 +53,45 @@ The plugin uses Vite's WebSocket connection to communicate between the dev serve
53
53
 
54
54
  Model is preserved across hot reloads but cleared on manual browser refreshes, giving you control over when to reset your app.
55
55
 
56
+ ## Server rendering dev host
57
+
58
+ Pass `ssr` with the path to your server entry to render page requests through it during development:
59
+
60
+ ```typescript
61
+ plugins: [foldkit({ ssr: { serverEntry: '/src/entry.server.ts' } })]
62
+ ```
63
+
64
+ With this set, the dev server converts HTML page requests to Web `Request` values, passes them to the entry's `renderPage`, and serves the returned `Response`. The request URL retains Vite's configured `base` prefix and the browser's query string. Vite continues to serve the client entry, HMR, and assets, and the server entry runs through Vite's module graph, so edits to it apply without a restart. The client side of the handoff needs no plugin configuration: a server-rendered application's client entry calls `Runtime.hydrate` instead of `Runtime.run`. Hydration adopts matching DOM, rebuilds mismatched subtrees, and refuses an invalid or cross-deployment handoff. The option shapes only the dev server; production hosts import the built server entry themselves. See the [Server Rendering documentation](https://foldkit.dev/core/server-rendering) for the full contract.
65
+
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
+
68
+ ## Build id
69
+
70
+ A server-rendered build carries an id naming the deployment it came from. `renderToString` stamps it on the rendered root and `Runtime.hydrate` compares it before accessing the Flags payload text or adopting DOM, so a page served by an earlier deployment is refused rather than reconciled against a client that no longer means the same thing by it. Startup stops, and the page is contained: the document's body is marked `inert`, and a nondismissable modal shield covers its controls and existing top-layer content without closing author-owned dialogs. Nothing is moved, so no custom element reconnects and no frame reloads. This boundary blocks native page interaction; it is not a script or global-event sandbox. A client already running in an open tab is not rechecked when a deployment lands: the comparison happens when a client boots against a page.
71
+
72
+ The plugin compiles the id into application code as `import.meta.env.FOLDKIT_BUILD_ID`, from its `buildId` option or from the `FOLDKIT_BUILD_ID` environment variable:
73
+
74
+ ```typescript
75
+ plugins: [foldkit({ buildId: process.env.DEPLOYMENT_SHA })]
76
+ ```
77
+
78
+ The entries pass it explicitly, because Vite externalizes an installed dependency from a server build, where a compile-time define never reaches the framework itself:
79
+
80
+ ```typescript
81
+ // src/entry.server.ts
82
+ Server.renderToString(config, {
83
+ flags,
84
+ buildId: import.meta.env.FOLDKIT_BUILD_ID,
85
+ })
86
+
87
+ // src/entry.ts
88
+ Runtime.hydrate(application, { buildId: import.meta.env.FOLDKIT_BUILD_ID })
89
+ ```
90
+
91
+ Nothing is derived from the project. Use a value the deployment already has, such as a commit, a release tag, or a container digest. Three rules govern it: the id is published in the HTML every visitor receives, so it must never contain a secret; it must identify one deployment, so two deployments can never share one; and the same value must reach the client build and the server build, which run as separate commands. A hydratable render given no id fails with `MissingBuildId`. Only a build takes the id from the deployment. The dev server compiles a fixed one because one live source session supplies both transforms and has no deployment identity to derive.
92
+
93
+ The standalone `foldkitSsr({ serverEntry, buildId })` export compiles the same define for its server entry. When it runs in development without an explicit value, it uses the fixed development id too. The aggregate `foldkit({ buildId, ssr })` plugin passes its top-level value through automatically.
94
+
56
95
  ## DevTools overlay
57
96
 
58
97
  When `@foldkit/devtools` is installed as a development dependency, the plugin mounts its overlay automatically during development and leaves it out of production builds. No application import or `devTools.overlay` field is needed.
@@ -0,0 +1,26 @@
1
+ import type { Plugin } from 'vite';
2
+ /** The build id this build was given, from the plugin option or
3
+ * `FOLDKIT_BUILD_ID`, or `undefined` when the deployment supplied neither.
4
+ *
5
+ * @internal Exported for tests.
6
+ */
7
+ export declare const resolveBuildId: (configured?: string) => string | undefined;
8
+ /** The value `import.meta.env.FOLDKIT_BUILD_ID` compiles to, or `undefined`
9
+ * when a build was given no id and must refuse to render a hydratable page.
10
+ *
11
+ * @internal Exported for tests.
12
+ */
13
+ export declare const buildIdForCommand: (command: "build" | "serve", configured?: string) => string | undefined;
14
+ /**
15
+ * Compiles the deployment's build id into application code as
16
+ * `import.meta.env.FOLDKIT_BUILD_ID`, for the client entry and the server entry
17
+ * to hand to `Runtime.hydrate` and `renderToString`.
18
+ *
19
+ * A build takes the id from the `buildId` option or `FOLDKIT_BUILD_ID` and
20
+ * compiles nothing when it was given neither, so a hydratable render fails with
21
+ * `MissingBuildId` rather than serving a page hydration cannot place.
22
+ * Development serves a fixed id instead because one live source session
23
+ * supplies both transforms and has no deployment identity to derive.
24
+ */
25
+ export declare const foldkitBuildToken: (buildId?: string) => Plugin;
26
+ //# sourceMappingURL=buildToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildToken.d.ts","sourceRoot":"","sources":["../src/buildToken.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAqClC;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,aAAa,MAAM,KAAG,MAAM,GAAG,SAQ7D,CAAA;AAcD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,OAAO,GAAG,OAAO,EAC1B,aAAa,MAAM,KAClB,MAAM,GAAG,SAMX,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,KAAG,MAYnD,CAAA"}
@@ -0,0 +1,94 @@
1
+ // The build id names the deployment a page came from. The server stamps it on
2
+ // the rendered root, the client carries it, and hydration refuses a page whose
3
+ // id is not its own before it adopts any DOM.
4
+ //
5
+ // Per-view identities cannot answer that question. They move when the view they
6
+ // name changes, but what a view renders also depends on the constants it
7
+ // imports, the configuration it reads, the dependencies it calls, and the
8
+ // arguments its caller passes. A component whose own source is untouched renders
9
+ // something different when its caller changes, and its identity is the one that
10
+ // wins on the element, so a stale page's `<input name="email">` can otherwise be
11
+ // adopted for a new build's `<input name="ssn">`, carrying what a visitor typed
12
+ // into a field that submits under a different name.
13
+ //
14
+ // The id is supplied by the deployment rather than derived from the project.
15
+ // Deriving it was tried and does not hold: a digest of the files under the Vite
16
+ // root misses shared modules from elsewhere in a monorepo, untracked inputs, and
17
+ // environment-derived configuration, so two deployments that render differently
18
+ // can share an id; it moves when a build writes output the next build reads, so
19
+ // one deployment can produce two ids; and hashing whatever files happen to sit
20
+ // in the project turns a value published in HTML into an oracle for the secrets
21
+ // among them. A value the deployment already has (a commit, a release tag, a
22
+ // container digest) has none of those problems.
23
+ //
24
+ // This plugin only compiles the id into application code. Foldkit itself is an
25
+ // ordinary dependency that Vite externalizes from a server build, where a
26
+ // compile-time define never reaches it, so the id is handed to `renderToString`
27
+ // and `Runtime.hydrate` explicitly rather than read from inside the framework.
28
+ //
29
+ // Development is compiled an id too. The dev SSR host renders through
30
+ // `renderToString` like any other, and a hydratable render refuses to run
31
+ // without one, so leaving development unnamed would fail every dev page
32
+ // request.
33
+ const BUILD_ID_ENVIRONMENT_VARIABLE = 'FOLDKIT_BUILD_ID';
34
+ /** The build id this build was given, from the plugin option or
35
+ * `FOLDKIT_BUILD_ID`, or `undefined` when the deployment supplied neither.
36
+ *
37
+ * @internal Exported for tests.
38
+ */
39
+ export const resolveBuildId = (configured) => {
40
+ if (configured !== undefined && configured !== '') {
41
+ return configured;
42
+ }
43
+ const fromEnvironment = process.env[BUILD_ID_ENVIRONMENT_VARIABLE];
44
+ return fromEnvironment !== undefined && fromEnvironment !== ''
45
+ ? fromEnvironment
46
+ : undefined;
47
+ };
48
+ // The id development serves. Development is the one place a constant is right:
49
+ // one live source session supplies both the server and client transforms rather
50
+ // than producing independently deployable artifacts. A value that moved would
51
+ // only make the dev server disagree with the tab already open against it. A
52
+ // hydratable render still requires an id, so development has to be given one
53
+ // rather than left without.
54
+ //
55
+ // It is exactly wrong for a build, which is why a build with no id is refused
56
+ // rather than defaulted: two deployments sharing an id is the case the id
57
+ // exists to catch.
58
+ const DEVELOPMENT_BUILD_ID = 'development';
59
+ /** The value `import.meta.env.FOLDKIT_BUILD_ID` compiles to, or `undefined`
60
+ * when a build was given no id and must refuse to render a hydratable page.
61
+ *
62
+ * @internal Exported for tests.
63
+ */
64
+ export const buildIdForCommand = (command, configured) => {
65
+ const resolved = resolveBuildId(configured);
66
+ if (resolved !== undefined) {
67
+ return resolved;
68
+ }
69
+ return command === 'serve' ? DEVELOPMENT_BUILD_ID : undefined;
70
+ };
71
+ /**
72
+ * Compiles the deployment's build id into application code as
73
+ * `import.meta.env.FOLDKIT_BUILD_ID`, for the client entry and the server entry
74
+ * to hand to `Runtime.hydrate` and `renderToString`.
75
+ *
76
+ * A build takes the id from the `buildId` option or `FOLDKIT_BUILD_ID` and
77
+ * compiles nothing when it was given neither, so a hydratable render fails with
78
+ * `MissingBuildId` rather than serving a page hydration cannot place.
79
+ * Development serves a fixed id instead because one live source session
80
+ * supplies both transforms and has no deployment identity to derive.
81
+ */
82
+ export const foldkitBuildToken = (buildId) => ({
83
+ name: 'foldkit:build-token',
84
+ config: (_config, { command }) => {
85
+ const resolved = buildIdForCommand(command, buildId);
86
+ return resolved === undefined
87
+ ? {}
88
+ : {
89
+ define: {
90
+ 'import.meta.env.FOLDKIT_BUILD_ID': JSON.stringify(resolved),
91
+ },
92
+ };
93
+ },
94
+ });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Plugin } from 'vite';
2
+ import { type FoldkitSsrOptions } from './ssr.js';
2
3
  export { type BrandDistResult, brandDistDirectory } from './brandDist.js';
4
+ export { type FoldkitSsrOptions, foldkitSsr } from './ssr.js';
3
5
  export { type ViewIdentityTransformResult, foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
4
6
  /** Options for the `foldkit` Vite plugin. */
5
7
  export type FoldkitPluginOptions = Readonly<{
@@ -10,6 +12,27 @@ export type FoldkitPluginOptions = Readonly<{
10
12
  * the Foldkit DevTools MCP server.
11
13
  */
12
14
  devToolsMcpPort?: number;
15
+ /**
16
+ * Serve server-rendered pages from the Vite dev server. When set, `vite`
17
+ * passes HTML navigations that fall through Vite, plus non-GET requests, to
18
+ * `renderPage` from the module at `ssr.serverEntry`. When `undefined` (the
19
+ * default), the dev server serves the client entry only.
20
+ */
21
+ ssr?: Omit<FoldkitSsrOptions, 'buildId'>;
22
+ /**
23
+ * The deployment this build belongs to, compiled into application code as
24
+ * `import.meta.env.FOLDKIT_BUILD_ID` for the entries to pass to
25
+ * `renderToString` and `Runtime.hydrate`. Hydration compares it against the id
26
+ * the server stamped and refuses a page from another deployment rather than
27
+ * adopting it: startup stops and the page is contained, with the document's
28
+ * body marked `inert`.
29
+ *
30
+ * Defaults to the `FOLDKIT_BUILD_ID` environment variable. Use a value the
31
+ * deployment already has, such as a commit or a release tag, and give the
32
+ * client build and the server build the same one. It is published in the
33
+ * page, so it must not be a secret.
34
+ */
35
+ buildId?: string;
13
36
  }>;
14
37
  /**
15
38
  * Foldkit's Vite plugin set: the view-identity branding transform and
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EACV,MAAM,EAIP,MAAM,MAAM,CAAA;AAMb,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAC,CAAA;AAilBF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAAI,UAAS,oBAAyB,KAAG,KAAK,CAAC,MAAM,CA6DxE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EACV,MAAM,EAIP,MAAM,MAAM,CAAA;AAKb,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,UAAU,CAAA;AAG7D,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAA;IACxC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAC,CAAA;AAilBF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAAI,UAAS,oBAAyB,KAAG,KAAK,CAAC,MAAM,CA+ExE,CAAA"}
package/dist/index.js CHANGED
@@ -4,9 +4,12 @@ import { PreserveModelMessage, RequestModelMessage, RestoreModelMessage, } from
4
4
  import { createRequire } from 'node:module';
5
5
  import { resolve } from 'node:path';
6
6
  import { WebSocketServer } from 'ws';
7
+ import { foldkitBuildToken } from './buildToken.js';
7
8
  import { devToolsOverlayPlugin } from './devToolsOverlay.js';
9
+ import { foldkitSsr } from './ssr.js';
8
10
  import { foldkitViewIdentity } from './viewIdentity.js';
9
11
  export { brandDistDirectory } from './brandDist.js';
12
+ export { foldkitSsr } from './ssr.js';
10
13
  export { foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
11
14
  // NOTE: Vite's dep optimizer scans the consumer's source for `effect`
12
15
  // imports and pre-bundles only those exports into a single `effect.js`
@@ -374,5 +377,23 @@ export const foldkit = (options = {}) => {
374
377
  return [];
375
378
  },
376
379
  };
377
- return [foldkitViewIdentity(), devToolsOverlayPlugin(), hmrPlugin];
380
+ return options.ssr === undefined
381
+ ? [
382
+ foldkitBuildToken(options.buildId),
383
+ foldkitViewIdentity(),
384
+ devToolsOverlayPlugin(),
385
+ hmrPlugin,
386
+ ]
387
+ : [
388
+ foldkitBuildToken(options.buildId),
389
+ foldkitViewIdentity(),
390
+ devToolsOverlayPlugin(),
391
+ hmrPlugin,
392
+ foldkitSsr({
393
+ ...options.ssr,
394
+ ...(options.buildId === undefined
395
+ ? {}
396
+ : { buildId: options.buildId }),
397
+ }),
398
+ ];
378
399
  };
package/dist/ssr.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { Plugin } from 'vite';
2
+ /** Options for serving server-rendered pages from the Vite dev server. */
3
+ export type FoldkitSsrOptions = Readonly<{
4
+ /**
5
+ * Module path of the server entry, resolved by Vite (e.g.
6
+ * `'/src/entry.server.ts'`). The module must export a `renderPage`
7
+ * function taking a Web `Request` and returning a
8
+ * `Promise<EntryResult>`.
9
+ */
10
+ serverEntry: string;
11
+ /**
12
+ * The `id` of the empty container element in `index.html` the rendered
13
+ * markup replaces. Defaults to `'root'`.
14
+ */
15
+ containerId?: string;
16
+ /**
17
+ * The origin the entry sees as `Request.url`, such as
18
+ * `'https://app.example'`. Defaults to the origin the dev server itself
19
+ * resolved from its own configuration.
20
+ *
21
+ * The origin is deployment configuration rather than something a request
22
+ * carries. A client chooses its own `Host` header, and Vite accepts IP
23
+ * literals and (with `allowedHosts`) arbitrary names, so deriving the origin
24
+ * from the request would let the client pick the redirects, canonical URLs,
25
+ * and cookie domains an entry builds from `Request.url`. Set this when the
26
+ * dev server sits behind a proxy or TLS terminator that serves a different
27
+ * public origin.
28
+ */
29
+ origin?: string;
30
+ /**
31
+ * The deployment id compiled into a server entry when `foldkitSsr` is used
32
+ * as a standalone plugin. It defaults to `FOLDKIT_BUILD_ID`, or to the fixed
33
+ * development id in serve mode. The aggregate `foldkit` plugin supplies its
34
+ * top-level `buildId` here automatically.
35
+ *
36
+ * The value is public in rendered HTML, so it must not be a secret.
37
+ */
38
+ buildId?: string;
39
+ }>;
40
+ /**
41
+ * Serves server-rendered pages from the Vite dev server.
42
+ *
43
+ * Vite retains ownership of configured proxies, source modules, HMR, and
44
+ * assets. Requests that fall through load the server entry through Vite's SSR
45
+ * module loader, call its `renderPage` with a Web `Request`, and send the
46
+ * resulting Web `Response`. Vite's host validation applies before either
47
+ * owner. Foldkit then validates and normalizes the request target before Vite
48
+ * or the server entry can resolve it. Vite's CORS option applies only to
49
+ * Vite-owned responses, while the server entry owns CORS for application
50
+ * responses. Server entry edits take effect without a restart.
51
+ */
52
+ export declare const foldkitSsr: (options: FoldkitSsrOptions) => Plugin;
53
+ //# sourceMappingURL=ssr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAW,MAAM,EAA+B,MAAM,MAAM,CAAA;AAIxE,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;CACjB,CAAC,CAAA;AA4rBF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,iBAAiB,KAAG,MA4CvD,CAAA"}
package/dist/ssr.js ADDED
@@ -0,0 +1,490 @@
1
+ import { Array, Effect, Predicate } from 'effect';
2
+ import * as Server from 'foldkit/experimental/server';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { Readable } from 'node:stream';
6
+ import { buildIdForCommand } from './buildToken.js';
7
+ const isEntryModule = (loadedModule) => Predicate.isObject(loadedModule) &&
8
+ Predicate.hasProperty(loadedModule, 'renderPage') &&
9
+ Predicate.isFunction(loadedModule.renderPage);
10
+ // The origin the dev server serves, taken from the plugin option when the
11
+ // deployment sets one and otherwise from the server's own resolved
12
+ // configuration. The request never contributes: `Host` is a value the client
13
+ // writes, and Vite accepts IP literals by default and any name at all under
14
+ // `allowedHosts`, so a request could otherwise name the origin the entry builds
15
+ // redirects and canonical URLs from.
16
+ const DEV_SERVER_FALLBACK_ORIGIN = 'http://localhost';
17
+ const configuredOrigin = (server, options) => {
18
+ if (options.origin !== undefined) {
19
+ return options.origin;
20
+ }
21
+ const [resolvedUrl] = server.resolvedUrls?.local ?? [];
22
+ if (resolvedUrl !== undefined) {
23
+ // Vite prints these with a trailing slash; `new URL` normalizes either form
24
+ // and `origin` drops the path, so both are safe to read here.
25
+ try {
26
+ return new URL(resolvedUrl).origin;
27
+ }
28
+ catch {
29
+ return DEV_SERVER_FALLBACK_ORIGIN;
30
+ }
31
+ }
32
+ const { https, port } = server.config.server;
33
+ const scheme = https === undefined ? 'http' : 'https';
34
+ return port === undefined
35
+ ? DEV_SERVER_FALLBACK_ORIGIN
36
+ : `${scheme}://localhost:${port}`;
37
+ };
38
+ const toWebRequest = (requestUrl, nodeRequest) => {
39
+ const headers = new Headers();
40
+ for (const [name, value] of Object.entries(nodeRequest.headers)) {
41
+ if (Predicate.isString(value)) {
42
+ headers.set(name, value);
43
+ }
44
+ if (Array.isArray(value)) {
45
+ for (const item of value) {
46
+ headers.append(name, item);
47
+ }
48
+ }
49
+ }
50
+ const method = nodeRequest.method ?? 'GET';
51
+ const requestInit = { headers, method };
52
+ if (method !== 'GET' && method !== 'HEAD') {
53
+ // NOTE: Node and the DOM library declare structurally different
54
+ // ReadableStream interfaces even though Node's `Readable.toWeb` returns
55
+ // the Web stream implementation that `Request` consumes at runtime.
56
+ requestInit.body = Readable.toWeb(nodeRequest);
57
+ requestInit.duplex = 'half';
58
+ }
59
+ return new Request(requestUrl, requestInit);
60
+ };
61
+ const originalRequestTargetOf = (nodeRequest) => nodeRequest.originalUrl ?? nodeRequest.url ?? '/';
62
+ const requestTargetOf = (nodeRequest) => nodeRequest.url ?? nodeRequest.originalUrl ?? '/';
63
+ // The request target resolved against the configured origin, or `undefined`
64
+ // when it names a different one (an absolute-form target, or a network-path
65
+ // reference such as `//elsewhere.example/page`). The dev host refuses those
66
+ // rather than handing the entry an origin the client chose, which is what a
67
+ // generated production host does too.
68
+ const resolvedRequestUrl = (server, options, nodeRequest) => Server.resolveRequestUrl(originalRequestTargetOf(nodeRequest), configuredOrigin(server, options)) === undefined
69
+ ? undefined
70
+ : Server.resolveRequestUrl(requestTargetOf(nodeRequest), configuredOrigin(server, options));
71
+ const prepareRequestTarget = (server, options, requestUrls, nodeRequest) => {
72
+ const requestUrl = requestUrls.get(nodeRequest) ??
73
+ resolvedRequestUrl(server, options, nodeRequest);
74
+ if (requestUrl === undefined) {
75
+ return undefined;
76
+ }
77
+ requestUrls.set(nodeRequest, requestUrl);
78
+ const resolved = new URL(requestUrl);
79
+ nodeRequest.url = `${resolved.pathname}${resolved.search}`;
80
+ return requestUrl;
81
+ };
82
+ const requestTargetMiddleware = (server, options, requestUrls, render) => (nodeRequest, nodeResponse, next) => {
83
+ const requestUrl = prepareRequestTarget(server, options, requestUrls, nodeRequest);
84
+ if (requestUrl === undefined) {
85
+ nodeResponse.statusCode = 400;
86
+ nodeResponse.end();
87
+ return;
88
+ }
89
+ if (nodeRequest.method === 'OPTIONS') {
90
+ const proxy = server.config.server.proxy;
91
+ if (isProxyRequest(nodeRequest, proxy) ||
92
+ shouldViteCorsAnswerPreflight(nodeRequest, proxy, server.config.base)) {
93
+ next();
94
+ return;
95
+ }
96
+ render(nodeRequest, nodeResponse, next);
97
+ return;
98
+ }
99
+ next();
100
+ };
101
+ const renderDecision = (nodeRequest, requestUrl) => {
102
+ const method = nodeRequest.method ?? 'GET';
103
+ if (method === 'GET' || method === 'HEAD') {
104
+ if (Server.resolvesToIndexHtml(requestUrl)) {
105
+ return 'Render';
106
+ }
107
+ // A request Vite did not serve and that names an asset is a miss, not a
108
+ // navigation. Browsers fetch scripts and stylesheets with `Accept: */*`, so
109
+ // without this a stale hashed asset would be answered with the app shell at
110
+ // 200 and read as a blank page instead of the 404 it is.
111
+ const fetchDestination = nodeRequest.headers['sec-fetch-dest'];
112
+ const classification = Server.classifyRequest(requestUrl, Predicate.isString(fetchDestination) ? fetchDestination : undefined);
113
+ if (classification === 'PathAsset') {
114
+ return 'RefusedPathAsset';
115
+ }
116
+ if (classification === 'DestinationAsset') {
117
+ return 'RefusedDestinationAsset';
118
+ }
119
+ const accept = nodeRequest.headers.accept;
120
+ return Server.acceptsHtml(Predicate.isString(accept) ? accept : undefined)
121
+ ? 'RenderNegotiated'
122
+ : 'RefusedNegotiated';
123
+ }
124
+ return 'Render';
125
+ };
126
+ const renderRequest = (server, options, nodeRequest, requestUrl) => Effect.gen(function* () {
127
+ const { pathname, search } = new URL(requestUrl);
128
+ const route = `${pathname}${search}`;
129
+ const rawTemplate = yield* Effect.promise(() => readFile(resolve(server.config.root, 'index.html'), 'utf-8'));
130
+ // NOTE: the first argument tells Vite where the HTML lives, and Vite
131
+ // resolves the template's relative URLs (such as a `./src/entry.ts`
132
+ // script) against it. The template always lives at the site root, so
133
+ // that argument must stay `/index.html` no matter which route is being
134
+ // rendered. The third argument, named `originalUrl` in Vite's signature,
135
+ // carries the route actually being requested.
136
+ const template = yield* Effect.promise(() => server.transformIndexHtml('/index.html', rawTemplate, route));
137
+ const loadedModule = yield* Effect.promise(() => server.ssrLoadModule(options.serverEntry));
138
+ if (!isEntryModule(loadedModule)) {
139
+ return yield* Effect.die(new Error(`[foldkit] '${options.serverEntry}' does not export a renderPage function, so the dev server cannot render pages.`));
140
+ }
141
+ const result = yield* Effect.promise(() => loadedModule.renderPage(toWebRequest(requestUrl, nodeRequest)));
142
+ return Server.toResponse(template, result, options.containerId === undefined
143
+ ? {}
144
+ : { containerId: options.containerId });
145
+ });
146
+ const varyHeaderValue = (value) => {
147
+ if (Array.isArray(value)) {
148
+ return value.join(', ');
149
+ }
150
+ if (typeof value === 'string') {
151
+ return value;
152
+ }
153
+ return undefined;
154
+ };
155
+ // NOTE: every outcome a static miss negotiates declares both headers the
156
+ // negotiation read. Declaring only Accept would let a shared cache store the
157
+ // rendered page from a document request and serve it to a later script request
158
+ // carrying the same Accept, which never reaches the asset classification, and
159
+ // the reverse for the 404.
160
+ // Every field name in `incoming`, folded into `existing`. The application's own
161
+ // Vary is a list, and `Server.varyWith` merges one name at a time.
162
+ const mergeVary = (existing, incoming) => incoming
163
+ .split(',')
164
+ .map(token => token.trim())
165
+ .filter(token => token !== '')
166
+ .reduce((merged, fieldName) => Server.varyWith(merged, fieldName), existing ?? '');
167
+ const isHeaderList = (value) => typeof value === 'object';
168
+ const cloneHeaderValue = (value) => isHeaderList(value) ? [...value] : value;
169
+ const responseHeaders = (nodeResponse) => Object.fromEntries(Object.entries(nodeResponse.getHeaders()).map(([name, value]) => [
170
+ name,
171
+ cloneHeaderValue(value),
172
+ ]));
173
+ const isSameHeaderValue = (left, right) => {
174
+ if (isHeaderList(left) && isHeaderList(right)) {
175
+ return (left.length === right.length &&
176
+ left.every((value, index) => value === right.at(index)));
177
+ }
178
+ return left === right;
179
+ };
180
+ const headerMutations = (before, after) => Object.keys(after)
181
+ .filter(name => !isSameHeaderValue(before[name], after[name]))
182
+ .map(name => ({ name, before: before[name], after: after[name] }));
183
+ const varyFields = (value) => {
184
+ if (typeof value === 'string') {
185
+ return value.split(',').map(fieldName => fieldName.trim());
186
+ }
187
+ if (isHeaderList(value)) {
188
+ return value.flatMap(item => item.split(',').map(fieldName => fieldName.trim()));
189
+ }
190
+ return [];
191
+ };
192
+ const restoreVary = (nodeResponse, mutation) => {
193
+ const fieldsBeforeCors = new Set(varyFields(mutation.before).map(fieldName => fieldName.toLowerCase()));
194
+ const fieldsAddedByCors = new Set(varyFields(mutation.after)
195
+ .filter(fieldName => !fieldsBeforeCors.has(fieldName.toLowerCase()))
196
+ .map(fieldName => fieldName.toLowerCase()));
197
+ const remainingFields = varyFields(nodeResponse.getHeader(mutation.name)).filter(fieldName => !fieldsAddedByCors.has(fieldName.toLowerCase()));
198
+ if (Array.isArrayEmpty(remainingFields)) {
199
+ nodeResponse.removeHeader(mutation.name);
200
+ }
201
+ else {
202
+ nodeResponse.setHeader(mutation.name, remainingFields.join(', '));
203
+ }
204
+ };
205
+ const restoreResponseHeaders = (nodeResponse, mutations, headersWrittenAfterCors) => {
206
+ for (const mutation of mutations) {
207
+ if (headersWrittenAfterCors.has(mutation.name.toLowerCase())) {
208
+ continue;
209
+ }
210
+ const current = cloneHeaderValue(nodeResponse.getHeader(mutation.name));
211
+ if (mutation.name.toLowerCase() === 'vary') {
212
+ restoreVary(nodeResponse, mutation);
213
+ }
214
+ else if (isSameHeaderValue(current, mutation.after)) {
215
+ if (mutation.before === undefined) {
216
+ nodeResponse.removeHeader(mutation.name);
217
+ }
218
+ else {
219
+ nodeResponse.setHeader(mutation.name, mutation.before);
220
+ }
221
+ }
222
+ }
223
+ };
224
+ const trackHeaderWrites = (nodeResponse) => {
225
+ const names = new Set();
226
+ const setHeader = nodeResponse.setHeader;
227
+ const appendHeader = nodeResponse.appendHeader;
228
+ const removeHeader = nodeResponse.removeHeader;
229
+ nodeResponse.setHeader = function (name, value) {
230
+ names.add(String(name).toLowerCase());
231
+ return Reflect.apply(setHeader, this, [name, value]);
232
+ };
233
+ nodeResponse.appendHeader = function (name, value) {
234
+ names.add(String(name).toLowerCase());
235
+ return Reflect.apply(appendHeader, this, [name, value]);
236
+ };
237
+ nodeResponse.removeHeader = function (name) {
238
+ names.add(String(name).toLowerCase());
239
+ return Reflect.apply(removeHeader, this, [name]);
240
+ };
241
+ return {
242
+ names,
243
+ stop: () => {
244
+ nodeResponse.setHeader = setHeader;
245
+ nodeResponse.appendHeader = appendHeader;
246
+ nodeResponse.removeHeader = removeHeader;
247
+ },
248
+ };
249
+ };
250
+ const isProxyRequest = (nodeRequest, proxy) => {
251
+ const requestUrl = nodeRequest.url;
252
+ if (requestUrl === undefined || proxy === undefined) {
253
+ return false;
254
+ }
255
+ return Object.entries(proxy).some(([context, proxyOptions]) => {
256
+ if (proxyOptions === undefined) {
257
+ return false;
258
+ }
259
+ return context.startsWith('^')
260
+ ? new RegExp(context).test(requestUrl)
261
+ : requestUrl.startsWith(context);
262
+ });
263
+ };
264
+ const VITE_SOURCE_EXTENSIONS = new Set([
265
+ 'astro',
266
+ 'cts',
267
+ 'jsx',
268
+ 'mdx',
269
+ 'mts',
270
+ 'svelte',
271
+ 'ts',
272
+ 'tsx',
273
+ 'vue',
274
+ ]);
275
+ const hasViteSourceExtension = (path) => {
276
+ const lastSegment = path.slice(path.lastIndexOf('/') + 1);
277
+ const separator = lastSegment.lastIndexOf('.');
278
+ return (separator >= 0 &&
279
+ VITE_SOURCE_EXTENSIONS.has(lastSegment.slice(separator + 1).toLowerCase()));
280
+ };
281
+ const shouldViteCorsAnswerPreflight = (nodeRequest, proxy, base) => {
282
+ if (isProxyRequest(nodeRequest, proxy)) {
283
+ return false;
284
+ }
285
+ const requestedMethod = nodeRequest.headers['access-control-request-method'];
286
+ const requestOrigin = nodeRequest.headers.origin;
287
+ if (!Predicate.isString(requestedMethod) ||
288
+ !Predicate.isString(requestOrigin) ||
289
+ requestOrigin === '') {
290
+ return false;
291
+ }
292
+ const normalizedMethod = requestedMethod.toUpperCase();
293
+ if (normalizedMethod !== 'GET' && normalizedMethod !== 'HEAD') {
294
+ return false;
295
+ }
296
+ const requestTarget = requestTargetOf(nodeRequest);
297
+ const path = new URL(requestTarget, DEV_SERVER_FALLBACK_ORIGIN).pathname;
298
+ const vitePath = base !== '/' && path.startsWith(base) ? `/${path.slice(base.length)}` : path;
299
+ if (vitePath.startsWith('/@') ||
300
+ vitePath.startsWith('/__vite') ||
301
+ vitePath.startsWith('/node_modules/') ||
302
+ hasViteSourceExtension(vitePath)) {
303
+ return true;
304
+ }
305
+ const fetchDestination = nodeRequest.headers['sec-fetch-dest'];
306
+ return (Server.classifyRequest(requestTarget, Predicate.isString(fetchDestination) ? fetchDestination : undefined) !== 'Page');
307
+ };
308
+ const wrapViteCors = (server, options, requestUrls, stateByRequest) => {
309
+ if (server.config.server.cors === false) {
310
+ return;
311
+ }
312
+ const corsLayer = server.middlewares.stack.find(layer => Predicate.isFunction(layer.handle) &&
313
+ layer.handle.name.startsWith('corsMiddleware'));
314
+ if (corsLayer === undefined || !Predicate.isFunction(corsLayer.handle)) {
315
+ throw new Error('[foldkit] Could not find Vite\u2019s CORS middleware at the expected ' +
316
+ 'ownership boundary. This Vite version is not compatible with ' +
317
+ '@foldkit/vite-plugin.');
318
+ }
319
+ const viteCors = corsLayer.handle;
320
+ const wrappedCors = (nodeRequest, nodeResponse, next) => {
321
+ if (prepareRequestTarget(server, options, requestUrls, nodeRequest) ===
322
+ undefined) {
323
+ nodeResponse.statusCode = 400;
324
+ nodeResponse.end();
325
+ return;
326
+ }
327
+ if (nodeRequest.method === 'OPTIONS' &&
328
+ !shouldViteCorsAnswerPreflight(nodeRequest, server.config.server.proxy, server.config.base)) {
329
+ next();
330
+ return;
331
+ }
332
+ const before = responseHeaders(nodeResponse);
333
+ Reflect.apply(viteCors, undefined, [
334
+ nodeRequest,
335
+ nodeResponse,
336
+ (error) => {
337
+ stateByRequest.set(nodeRequest, {
338
+ mutations: headerMutations(before, responseHeaders(nodeResponse)),
339
+ tracking: trackHeaderWrites(nodeResponse),
340
+ });
341
+ next(error);
342
+ },
343
+ ]);
344
+ };
345
+ corsLayer.handle = wrappedCors;
346
+ };
347
+ const setNegotiatedVary = (nodeResponse) => {
348
+ const existing = varyHeaderValue(nodeResponse.getHeader('vary'));
349
+ nodeResponse.setHeader('vary', Server.varyWith(Server.varyWithAccept(existing), 'Sec-Fetch-Dest'));
350
+ };
351
+ const sendWebResponse = async (webResponse, nodeRequest, nodeResponse, isNegotiated) => {
352
+ nodeResponse.statusCode = webResponse.status;
353
+ if (webResponse.statusText !== '') {
354
+ nodeResponse.statusMessage = webResponse.statusText;
355
+ }
356
+ const getSetCookie = Predicate.hasProperty(webResponse.headers, 'getSetCookie')
357
+ ? webResponse.headers.getSetCookie
358
+ : undefined;
359
+ const setCookieHeaders = Predicate.isFunction(getSetCookie)
360
+ ? getSetCookie.call(webResponse.headers)
361
+ : [];
362
+ for (const [name, value] of webResponse.headers) {
363
+ if (name === 'vary') {
364
+ // NOTE: merged rather than set. Middleware that ran before the entry may
365
+ // already have declared a field, and a plain setHeader here would replace
366
+ // it when the application declares its own Vary.
367
+ nodeResponse.setHeader('vary', mergeVary(varyHeaderValue(nodeResponse.getHeader('vary')), value));
368
+ }
369
+ else if (name !== 'set-cookie' || Array.isArrayEmpty(setCookieHeaders)) {
370
+ nodeResponse.setHeader(name, value);
371
+ }
372
+ }
373
+ if (Array.isArrayNonEmpty(setCookieHeaders)) {
374
+ nodeResponse.setHeader('set-cookie', setCookieHeaders);
375
+ }
376
+ if (isNegotiated) {
377
+ // Merge into whatever Vary already sits on the node response, including
378
+ // the application's own fields, so declaring these does not drop it.
379
+ setNegotiatedVary(nodeResponse);
380
+ }
381
+ if (nodeRequest.method === 'HEAD' || webResponse.body === null) {
382
+ nodeResponse.end();
383
+ }
384
+ else {
385
+ const body = new Uint8Array(await webResponse.arrayBuffer());
386
+ nodeResponse.end(body);
387
+ }
388
+ };
389
+ // The handler that turns an application-owned request into a response. OPTIONS
390
+ // reaches it at the host boundary. Other methods reach it after Vite's modules
391
+ // and assets have had an opportunity to answer.
392
+ const renderMiddleware = (server, options, requestUrls, stateByRequest) => (nodeRequest, nodeResponse, next) => {
393
+ const corsState = stateByRequest.get(nodeRequest);
394
+ stateByRequest.delete(nodeRequest);
395
+ corsState?.tracking.stop();
396
+ restoreResponseHeaders(nodeResponse, corsState?.mutations ?? [], corsState?.tracking.names ?? new Set());
397
+ const requestUrl = requestUrls.get(nodeRequest) ??
398
+ resolvedRequestUrl(server, options, nodeRequest);
399
+ requestUrls.delete(nodeRequest);
400
+ if (requestUrl === undefined) {
401
+ // The target names an origin other than the one being served, so there is
402
+ // no request to render: answering it would hand the entry a client-chosen
403
+ // origin to build redirects and canonical URLs from.
404
+ nodeResponse.statusCode = 400;
405
+ nodeResponse.end();
406
+ return;
407
+ }
408
+ const method = nodeRequest.method ?? 'GET';
409
+ if (Server.isHostSettledMethod(method)) {
410
+ nodeResponse.statusCode = Server.HOST_METHOD_ANSWERS.refusedStatus;
411
+ nodeResponse.setHeader('allow', Server.HOST_METHOD_ANSWERS.allow);
412
+ nodeResponse.end();
413
+ return;
414
+ }
415
+ const decision = renderDecision(nodeRequest, requestUrl);
416
+ if (decision === 'RefusedPathAsset') {
417
+ // The path names an asset whatever the request headers say, so this
418
+ // refusal is the same for every client and needs no Vary.
419
+ nodeResponse.statusCode = 404;
420
+ nodeResponse.end();
421
+ return;
422
+ }
423
+ if (decision === 'RefusedDestinationAsset') {
424
+ nodeResponse.statusCode = 404;
425
+ setNegotiatedVary(nodeResponse);
426
+ nodeResponse.end();
427
+ return;
428
+ }
429
+ if (decision === 'RefusedNegotiated') {
430
+ nodeResponse.statusCode = 404;
431
+ setNegotiatedVary(nodeResponse);
432
+ nodeResponse.end();
433
+ return;
434
+ }
435
+ const isNegotiated = decision === 'RenderNegotiated';
436
+ void Effect.runPromise(renderRequest(server, options, nodeRequest, requestUrl))
437
+ .then(response => sendWebResponse(response, nodeRequest, nodeResponse, isNegotiated))
438
+ .catch((error) => {
439
+ if (error instanceof Error) {
440
+ server.ssrFixStacktrace(error);
441
+ }
442
+ next(error);
443
+ });
444
+ };
445
+ /**
446
+ * Serves server-rendered pages from the Vite dev server.
447
+ *
448
+ * Vite retains ownership of configured proxies, source modules, HMR, and
449
+ * assets. Requests that fall through load the server entry through Vite's SSR
450
+ * module loader, call its `renderPage` with a Web `Request`, and send the
451
+ * resulting Web `Response`. Vite's host validation applies before either
452
+ * owner. Foldkit then validates and normalizes the request target before Vite
453
+ * or the server entry can resolve it. Vite's CORS option applies only to
454
+ * Vite-owned responses, while the server entry owns CORS for application
455
+ * responses. Server entry edits take effect without a restart.
456
+ */
457
+ export const foldkitSsr = (options) => {
458
+ return {
459
+ name: 'foldkit-ssr',
460
+ config: (_config, { command, isPreview }) => {
461
+ const buildId = buildIdForCommand(command, options.buildId);
462
+ return {
463
+ // NOTE: `vite preview` also resolves with command 'serve', but it
464
+ // serves built output and never runs configureServer. Setting custom
465
+ // there would strip preview's HTML middleware. A build still needs the
466
+ // define below even though only a dev server needs this app type.
467
+ ...(command === 'serve' && isPreview !== true
468
+ ? { appType: 'custom' }
469
+ : {}),
470
+ ...(buildId === undefined
471
+ ? {}
472
+ : {
473
+ define: {
474
+ 'import.meta.env.FOLDKIT_BUILD_ID': JSON.stringify(buildId),
475
+ },
476
+ }),
477
+ };
478
+ },
479
+ configureServer: server => {
480
+ const requestUrls = new WeakMap();
481
+ const stateByRequest = new WeakMap();
482
+ const render = renderMiddleware(server, options, requestUrls, stateByRequest);
483
+ server.middlewares.use(requestTargetMiddleware(server, options, requestUrls, render));
484
+ wrapViteCors(server, options, requestUrls, stateByRequest);
485
+ return () => {
486
+ server.middlewares.use(render);
487
+ };
488
+ },
489
+ };
490
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"viewIdentity.d.ts","sourceRoot":"","sources":["../src/viewIdentity.ts"],"names":[],"mappings":"AAAA,OAAoB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAI1D,OAAO,EAAE,KAAK,MAAM,EAAY,MAAM,MAAM,CAAA;AAwZ5C,iFAAiF;AACjF,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,SAAS,CAAA;CACf,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,IAAI,MAAM,EACV,MAAM,MAAM,EACZ,UAAU,QAAQ,CAAC;IAAE,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,KACtD,2BAA2B,GAAG,IAyChC,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,QAAO,MA0CtC,CAAA"}
1
+ {"version":3,"file":"viewIdentity.d.ts","sourceRoot":"","sources":["../src/viewIdentity.ts"],"names":[],"mappings":"AAAA,OAAoB,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAI1D,OAAO,EAAE,KAAK,MAAM,EAAY,MAAM,MAAM,CAAA;AA6a5C,iFAAiF;AACjF,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,SAAS,CAAA;CACf,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,IAAI,MAAM,EACV,MAAM,MAAM,EACZ,UAAU,QAAQ,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAC,KACD,2BAA2B,GAAG,IAyChC,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,QAAO,MA0CtC,CAAA"}
@@ -176,6 +176,27 @@ const rawFunctionName = (functionNode, parentByNode) => {
176
176
  }
177
177
  return ANONYMOUS_FUNCTION_NAME;
178
178
  };
179
+ // NOTE: a view identity names a source position, and a source position outlives
180
+ // the code that occupied it. Hydration compares identities to decide whether the
181
+ // server DOM and the client's first render describe the same logical element, so
182
+ // an identity that stays the same while the function's body changes tells it two
183
+ // different views are the same one: a stale page's `<input name="email">` is
184
+ // adopted by a new build's `<input name="ssn">`, and the value the visitor typed
185
+ // before hydration is carried into a field that means something else and submits
186
+ // under a new name. That is what the deployment's build id answers: it is
187
+ // compared before hydration reads anything the page carries, so a page from a
188
+ // build whose views mean something else is refused as a whole.
189
+ //
190
+ // An identity carries no digest of the module's source, and must not. Mixing
191
+ // one in would make a changed view rebuild its own subtree, which is the reason
192
+ // to want it, but the identity is emitted into the client bundle every visitor
193
+ // downloads, and a truncated hash of a whole source file is a check against that
194
+ // file's contents. A build that tree-shakes a low-entropy server-only value out
195
+ // of the client (a PIN behind `import.meta.env.SSR`) would still ship a digest
196
+ // of the source that contained it, and the value could be recovered by hashing
197
+ // candidates until one matched. The build id costs nothing to compare and
198
+ // reveals nothing about the source. `check:packed-ssr-consumer` asserts the
199
+ // absence against a real built bundle.
179
200
  const assignFunctionIds = (functionNodes, parentByNode, modulePath) => {
180
201
  const functionIds = new Map();
181
202
  const occurrenceCountsByName = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/vite-plugin",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Vite plugin for Foldkit hot module reloading with state preservation",
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-rc.109",
20
- "foldkit": "^0",
20
+ "foldkit": ">=0.148.0",
21
21
  "vite": "^7.0.0 || ^8.0.0"
22
22
  },
23
23
  "dependencies": {
@@ -32,8 +32,9 @@
32
32
  "rimraf": "^6.1.3",
33
33
  "typescript": "^6.0.3",
34
34
  "vite": "^8.0.16",
35
+ "vite7": "npm:vite@^7.0.0",
35
36
  "vitest": "^4.1.9",
36
- "foldkit": "0.146.0"
37
+ "foldkit": "0.148.0"
37
38
  },
38
39
  "keywords": [
39
40
  "vite",
@@ -53,7 +54,7 @@
53
54
  "access": "public"
54
55
  },
55
56
  "engines": {
56
- "node": ">=18.0.0"
57
+ "node": ">=20.19.0"
57
58
  },
58
59
  "scripts": {
59
60
  "clean": "rimraf dist *.tsbuildinfo",