@foldkit/vite-plugin 0.23.0 → 0.24.0-canary.4187988d5940

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,6 +65,52 @@ 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
+ ## Completed build metadata
69
+
70
+ 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:
71
+
72
+ ```typescript
73
+ import type { FoldkitBuildApi } from '@foldkit/vite-plugin'
74
+ import { createBuilder } from 'vite'
75
+
76
+ const builder = await createBuilder()
77
+ await builder.buildApp()
78
+
79
+ const plugin = builder.config.plugins.find(
80
+ plugin => plugin.name === 'foldkit:build',
81
+ )
82
+
83
+ if (plugin !== undefined) {
84
+ const api: FoldkitBuildApi | undefined = plugin.api
85
+
86
+ if (typeof api?.getBuildMetadata !== 'function') {
87
+ throw new Error('This Foldkit version does not expose build metadata')
88
+ }
89
+
90
+ const metadata = api.getBuildMetadata()
91
+ console.log(metadata.serverEntry)
92
+ console.log(metadata.manifest.prerendered)
93
+ }
94
+ ```
95
+
96
+ `FoldkitBuildApi` preserves `serverEntry` (the configured source entry) and `fetchModuleId` (the virtual fetch module). Its `getBuildMetadata()` method returns `FoldkitBuildMetadata`, exported as a Schema and inferred type:
97
+
98
+ | Field | Meaning |
99
+ | ----------------- | ------------------------------------------------------- |
100
+ | `root` | Absolute resolved application root |
101
+ | `clientDirectory` | Absolute resolved client output directory |
102
+ | `serverDirectory` | Absolute resolved server output directory |
103
+ | `serverEntry` | Absolute path to the emitted fetch handler |
104
+ | `manifest` | The version-1 data also written to `foldkit.build.json` |
105
+
106
+ The snapshot, manifest, and prerendered route array are frozen. The data can be serialized to another process. The manifest keeps its portable relative POSIX paths; the outer path fields describe the local build machine. Output paths reflect the resolved Vite environments, including overrides made by a host plugin.
107
+
108
+ A client-only build has no `foldkit:build` plugin. A present plugin without the accessor needs a Foldkit upgrade. The accessor throws before Foldkit finalizes, while another client or server build is running, or after its build fails. Always await `builder.buildApp()` successfully: another plugin can fail after Foldkit has finalized. An environment's `writeBundle` and another plugin's post-order `buildApp` hook do not establish this completion boundary.
109
+
110
+ Create a fresh Foldkit plugin set for each independent builder. The build plugin uses Vite's `sharedDuringBuild` to share captures across its environments. Concurrent builders must not reuse the same plugin object. This API does not add watch-mode support.
111
+
112
+ Prerendered pages and `foldkit.build.json` are finalized after the environment bundles. A separate deployment process that consumes an existing build can continue reading the disk manifest. An integration that runs the build in a child process can read the API there and transfer the serialized metadata in its child result.
113
+
68
114
  ## Build id
69
115
 
70
116
  The build id does not make hydration correct. It makes hydration refuse when it would otherwise be incorrect.
@@ -73,34 +119,31 @@ Server-rendered HTML carries the deployment id, and the client bundle carries it
73
119
 
74
120
  Nothing moves, so no custom element reconnects and no frame reloads. The containment 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 because the comparison happens only when a client boots against a page.
75
121
 
76
- 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:
122
+ When one Vite app build produces the client and server artifacts, the plugin generates an opaque id and compiles it into Foldkit in both. The entries need no build-id wiring:
77
123
 
78
124
  ```typescript
79
- plugins: [foldkit({ buildId: process.env.DEPLOYMENT_SHA })]
125
+ // src/entry.server.ts
126
+ Server.renderToString(config, { flags })
127
+
128
+ // src/entry.ts
129
+ Runtime.hydrate(application)
80
130
  ```
81
131
 
82
- 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:
132
+ Use the `buildId` option or `FOLDKIT_BUILD_ID` as an explicit override when client and server build in separate jobs, or when the id should name a deployment in another system:
83
133
 
84
134
  ```typescript
85
- // src/entry.server.ts
86
- Server.renderToString(config, {
87
- flags,
88
- buildId: import.meta.env.FOLDKIT_BUILD_ID,
89
- })
90
-
91
- // src/entry.ts
92
- Runtime.hydrate(application, { buildId: import.meta.env.FOLDKIT_BUILD_ID })
135
+ plugins: [foldkit({ buildId: process.env.DEPLOYMENT_ID })]
93
136
  ```
94
137
 
95
- Use a public value the deployment already has, such as a commit, release tag, or container digest. Three things have to be true:
138
+ Three things have to be true:
96
139
 
97
140
  - The id appears in the HTML every visitor receives, so it must never contain a secret.
98
141
  - Two deployments must never share an id.
99
- - The same value must reach the client and server builds, which run as separate commands.
142
+ - Separate build jobs must receive the same explicit override.
100
143
 
101
- 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.
144
+ A hydratable render with neither a compiled nor explicit id fails with `MissingBuildId`. The dev server generates an opaque id for its own client and server transforms.
102
145
 
103
- 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.
146
+ The standalone `foldkitSsr({ serverEntry, buildId })` export retains explicit build-id support for separately orchestrated integrations. The aggregate `foldkit({ ssr })` plugin owns the automatic path.
104
147
 
105
148
  ## DevTools overlay
106
149
 
@@ -110,13 +153,23 @@ To include the overlay in production, list `@foldkit/devtools` in regular `depen
110
153
 
111
154
  ## DevTools MCP relay
112
155
 
113
- Pass `devToolsMcpPort` to enable the relay that exposes your running Foldkit app to AI agents via the [`@foldkit/devtools-mcp`](https://www.npmjs.com/package/@foldkit/devtools-mcp) MCP server:
156
+ During development, the plugin starts a WebSocket relay for the [`@foldkit/devtools-mcp`](https://www.npmjs.com/package/@foldkit/devtools-mcp) server. Through the relay, an AI agent can inspect a running Foldkit app and dispatch Messages.
157
+
158
+ By default, the relay uses the dev server's listener at `/__foldkit/devtools-mcp`. The plugin publishes its address to a registry private to your user, and the MCP server finds it by project. You do not need to coordinate a port between them. The registry lives under `XDG_RUNTIME_DIR` when that is set, or under the operating system's temporary directory. `FOLDKIT_DEVTOOLS_RELAY_DIRECTORY` selects another directory.
159
+
160
+ The relay follows Vite's `server.host` setting. If you expose the dev server with `--host`, a client still needs the random token in the published address to inspect a Model or dispatch a Message. The plugin will not publish that token into a registry directory owned by another user or readable by other users. It reports the problem in the console.
161
+
162
+ In middleware mode, the relay uses a free loopback port because there is no HTTP server to share. It also uses a free loopback port for HTTPS dev servers, whose self-signed certificates the MCP server cannot verify. The plugin publishes these addresses for discovery in the same way.
163
+
164
+ To use a fixed port, set `devToolsMcpPort` in your Vite config:
114
165
 
115
166
  ```typescript
116
167
  plugins: [foldkit({ devToolsMcpPort: 9988 })]
117
168
  ```
118
169
 
119
- When set, the plugin opens a separate WebSocket server on the given port. The MCP server connects to it and forwards typed `Request` and `Response` frames between AI agents and your Runtime. Without `devToolsMcpPort` (the default), the relay is not started and the plugin behaves exactly as before.
170
+ Set `FOLDKIT_DEVTOOLS_MCP_PORT` to the same value for the MCP server. A fixed port opens a separate socket on every interface and does not require a token. Use this setting on platforms where directory ownership cannot be verified, including Windows, because the plugin cannot publish a relay address there.
171
+
172
+ `devToolsMcpPort: false` disables the relay. The relay does not start during Vitest runs or in production builds.
120
173
 
121
174
  See the [DevTools MCP documentation](https://foldkit.dev/ai/mcp) for setup, the available tools, and how dispatch validation works.
122
175
 
package/dist/build.d.ts CHANGED
@@ -9,9 +9,10 @@ export type FoldkitPrerenderOptions = Readonly<{
9
9
  */
10
10
  paths?: ReadonlyArray<string>;
11
11
  /**
12
- * The origin the entry sees as `Request.url` while generating, such as
13
- * `'https://app.example'`. It reaches canonical URLs and Open Graph URLs, so
14
- * a deployment that publishes those should set the origin it publishes.
12
+ * The origin used for `Request.url` while generating, such as
13
+ * `'https://app.example'`. This option does not set canonical or Open Graph
14
+ * metadata. It affects those fields only when the server entry derives them
15
+ * from `Request.url`, in which case it should match the published origin.
15
16
  */
16
17
  origin?: string;
17
18
  /**
@@ -42,15 +43,11 @@ export type FoldkitBuildOptions = Readonly<{
42
43
  /** Vite module id of the fetch handler Foldkit emits as the server entry. */
43
44
  export declare const FOLDKIT_FETCH_MODULE_ID = "virtual:foldkit/fetch";
44
45
  /**
45
- * What the build produced, written beside the server bundle for whatever
46
- * deploys it.
46
+ * What an `ssr.build` build produced, written beside the server bundle.
47
47
  *
48
- * A host has to decide what the asset layer does with a request that matches no
49
- * file, and that answer follows from the build rather than from taste: an
50
- * application with generated pages and no others wants a miss to stay a miss,
51
- * one with a server wants a miss to reach it, and one with neither wants the
52
- * template. Reading it here is how a deployment target gets that right without
53
- * asking its user to configure it twice.
48
+ * An SSR host serves generated paths as files and sends requests without a
49
+ * matching file to the server entry. A static-only SSG host serves the files
50
+ * and leaves other paths as misses.
54
51
  */
55
52
  export declare const FoldkitBuildManifest: Schema.Struct<{
56
53
  /**
@@ -71,28 +68,61 @@ export declare const FoldkitBuildManifest: Schema.Struct<{
71
68
  readonly serverEntry: Schema.String;
72
69
  /** Every path this build generated a page for, in the order it generated. */
73
70
  readonly prerendered: Schema.$Array<Schema.String>;
74
- /**
75
- * How a request-time host should run the server entry. Always `'fetch'`:
76
- * the entry is a Web `fetch` handler, not a Node process.
77
- */
78
- readonly host: Schema.optional<Schema.Literals<readonly ["fetch"]>>;
79
71
  }>;
80
72
  /**
81
- * What the build produced, written beside the server bundle for whatever
82
- * deploys it.
73
+ * The decoded shape of `foldkit.build.json`.
83
74
  *
84
- * A host has to decide what the asset layer does with a request that matches no
85
- * file, and that answer follows from the build rather than from taste: an
86
- * application with generated pages and no others wants a miss to stay a miss,
87
- * one with a server wants a miss to reach it, and one with neither wants the
88
- * template. Reading it here is how a deployment target gets that right without
89
- * asking its user to configure it twice.
90
- *
91
- * It is a file on disk that something else writes the next time it builds, so a
92
- * consumer decodes it with this Schema and fails closed rather than trusting
93
- * the shape it happens to find.
75
+ * A deployment host decodes the manifest before using it. The Schema rejects
76
+ * unknown versions rather than letting the host read missing fields.
94
77
  */
95
78
  export type FoldkitBuildManifest = typeof FoldkitBuildManifest.Type;
79
+ /** Completed application build data for an in-process deployment integration. */
80
+ export declare const FoldkitBuildMetadata: Schema.Struct<{
81
+ /** Absolute resolved Vite application root. */
82
+ readonly root: Schema.String;
83
+ /** Absolute resolved browser output directory. */
84
+ readonly clientDirectory: Schema.String;
85
+ /** Absolute resolved server output directory. */
86
+ readonly serverDirectory: Schema.String;
87
+ /** Absolute path to the emitted fetch handler. */
88
+ readonly serverEntry: Schema.String;
89
+ /** Portable data also written to `foldkit.build.json`. */
90
+ readonly manifest: Schema.Struct<{
91
+ /**
92
+ * The shape of this document. A consumer decodes before reading and refuses
93
+ * a version it does not know, so a manifest written by a newer Foldkit is a
94
+ * clear refusal rather than a field silently read as undefined.
95
+ */
96
+ readonly schemaVersion: Schema.Literals<readonly [1]>;
97
+ /**
98
+ * Where the browser build was written, as a POSIX path relative to the Vite
99
+ * root. Relative and normalized so a manifest survives being moved with the
100
+ * build it describes.
101
+ */
102
+ readonly client: Schema.String;
103
+ /** Where the server build was written, on the same terms as {@link client}. */
104
+ readonly server: Schema.String;
105
+ /** The server build's entry file, relative to `server`. */
106
+ readonly serverEntry: Schema.String;
107
+ /** Every path this build generated a page for, in the order it generated. */
108
+ readonly prerendered: Schema.$Array<Schema.String>;
109
+ }>;
110
+ }>;
111
+ /** The serializable, frozen snapshot of a completed Foldkit build. */
112
+ export type FoldkitBuildMetadata = typeof FoldkitBuildMetadata.Type;
113
+ /** The `foldkit:build` plugin's public integration API. */
114
+ export type FoldkitBuildApi = Readonly<{
115
+ /** Configured application source entry. */
116
+ serverEntry: string;
117
+ /** Virtual module used to bundle the fetch handler. */
118
+ fetchModuleId: typeof FOLDKIT_FETCH_MODULE_ID;
119
+ /**
120
+ * Read after a successful `await builder.buildApp()`. Throws before Foldkit
121
+ * finalizes or after another client or server build starts. Later plugin failures
122
+ * still require callers to await the full build successfully.
123
+ */
124
+ getBuildMetadata: () => FoldkitBuildMetadata;
125
+ }>;
96
126
  export declare const manifestPath: (root: string, directory: string, pathApi?: typeof nodePath) => string;
97
127
  export declare const renderTargetFor: (clientDirectory: string, path: string, origin: string, pathApi?: typeof nodePath) => Readonly<{
98
128
  url: URL;
@@ -102,11 +132,10 @@ export declare const renderTargetFor: (clientDirectory: string, path: string, or
102
132
  * Builds a Web `fetch` handler alongside the browser build, and generates
103
133
  * static HTML from the server entry, inside one `vite build`.
104
134
  *
105
- * Vite drives both environments and every host plugin composes with them, so a
106
- * deployment target that runs `vite build` gets the whole application rather
107
- * than the browser half. The generated pages take their template from the
108
- * browser build's own output, so generating twice over one build produces the
109
- * same pages. The server bundle's default export is `{ fetch }`.
135
+ * Vite builds both environments, so a deployment target that runs `vite build`
136
+ * gets the browser and server bundles. The `fetch` handler and generated pages
137
+ * use the HTML emitted by the browser build, but the unrendered template is not
138
+ * published with the assets. The server bundle's default export is `{ fetch }`.
110
139
  */
111
- export declare const foldkitBuild: (serverEntry: string, options?: FoldkitBuildOptions) => Plugin;
140
+ export declare const foldkitBuild: (serverEntry: string, options?: FoldkitBuildOptions) => Plugin<FoldkitBuildApi>;
112
141
  //# sourceMappingURL=build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAA;AAGtC,OAAO,QAA8B,MAAM,WAAW,CAAA;AAEtD,OAAO,KAAK,EAGV,MAAM,EAEP,MAAM,MAAM,CAAA;AAEb,mEAAmE;AACnE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAA;CAC9C,CAAC,CAAA;AAEF,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,0BAA0B,CAAA;AAE9D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB;IAC/B;;;;OAIG;;IAEH;;;;OAIG;;IAEH,+EAA+E;;IAE/E,2DAA2D;;IAE3D,6EAA6E;;IAE7E;;;OAGG;;EAEH,CAAA;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAcnE,eAAO,MAAM,YAAY,SACjB,MAAM,aACD,MAAM,YACR,OAAO,QAAQ,KACvB,MAQF,CAAA;AAwFD,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;AA4GD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,gBACV,MAAM,YACV,mBAAmB,KAC3B,MAoOF,CAAA"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAI/B,OAAO,QAA8B,MAAM,WAAW,CAAA;AAEtD,OAAO,KAAK,EAGV,MAAM,EAEP,MAAM,MAAM,CAAA;AAEb,mEAAmE;AACnE,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C;;;OAGG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAA;CAC9C,CAAC,CAAA;AAEF,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,0BAA0B,CAAA;AAE9D;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;IAC/B;;;;OAIG;;IAEH;;;;OAIG;;IAEH,+EAA+E;;IAE/E,2DAA2D;;IAE3D,6EAA6E;;EAE7E,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB;IAC/B,+CAA+C;;IAE/C,kDAAkD;;IAElD,iDAAiD;;IAEjD,kDAAkD;;IAElD,0DAA0D;;QAtC1D;;;;WAIG;;QAEH;;;;WAIG;;QAEH,+EAA+E;;QAE/E,2DAA2D;;QAE3D,6EAA6E;;;EAwB7E,CAAA;AAEF,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AAEnE,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,aAAa,EAAE,OAAO,uBAAuB,CAAA;IAC7C;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;CAC7C,CAAC,CAAA;AAcF,eAAO,MAAM,YAAY,SACjB,MAAM,aACD,MAAM,YACR,OAAO,QAAQ,KACvB,MAQF,CAAA;AAyFD,eAAO,MAAM,eAAe,oBACT,MAAM,QACjB,MAAM,UACJ,MAAM,YACL,OAAO,QAAQ,KACvB,QAAQ,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA0CrC,CAAA;AAsFD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,gBACV,MAAM,YACV,mBAAmB,KAC3B,MAAM,CAAC,eAAe,CAqPxB,CAAA"}
package/dist/build.js CHANGED
@@ -1,19 +1,16 @@
1
- import { Array, Schema } from 'effect';
1
+ import { Schema } from 'effect';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import nodePath, { dirname, resolve } from 'node:path';
4
5
  import { pathToFileURL } from 'node:url';
5
6
  /** Vite module id of the fetch handler Foldkit emits as the server entry. */
6
7
  export const FOLDKIT_FETCH_MODULE_ID = 'virtual:foldkit/fetch';
7
8
  /**
8
- * What the build produced, written beside the server bundle for whatever
9
- * deploys it.
9
+ * What an `ssr.build` build produced, written beside the server bundle.
10
10
  *
11
- * A host has to decide what the asset layer does with a request that matches no
12
- * file, and that answer follows from the build rather than from taste: an
13
- * application with generated pages and no others wants a miss to stay a miss,
14
- * one with a server wants a miss to reach it, and one with neither wants the
15
- * template. Reading it here is how a deployment target gets that right without
16
- * asking its user to configure it twice.
11
+ * An SSR host serves generated paths as files and sends requests without a
12
+ * matching file to the server entry. A static-only SSG host serves the files
13
+ * and leaves other paths as misses.
17
14
  */
18
15
  export const FoldkitBuildManifest = Schema.Struct({
19
16
  /**
@@ -34,11 +31,19 @@ export const FoldkitBuildManifest = Schema.Struct({
34
31
  serverEntry: Schema.String,
35
32
  /** Every path this build generated a page for, in the order it generated. */
36
33
  prerendered: Schema.Array(Schema.String),
37
- /**
38
- * How a request-time host should run the server entry. Always `'fetch'`:
39
- * the entry is a Web `fetch` handler, not a Node process.
40
- */
41
- host: Schema.optional(Schema.Literals(['fetch'])),
34
+ });
35
+ /** Completed application build data for an in-process deployment integration. */
36
+ export const FoldkitBuildMetadata = Schema.Struct({
37
+ /** Absolute resolved Vite application root. */
38
+ root: Schema.String,
39
+ /** Absolute resolved browser output directory. */
40
+ clientDirectory: Schema.String,
41
+ /** Absolute resolved server output directory. */
42
+ serverDirectory: Schema.String,
43
+ /** Absolute path to the emitted fetch handler. */
44
+ serverEntry: Schema.String,
45
+ /** Portable data also written to `foldkit.build.json`. */
46
+ manifest: FoldkitBuildManifest,
42
47
  });
43
48
  const MANIFEST_SCHEMA_VERSION = 1;
44
49
  // Relative and POSIX so the manifest describes a layout rather than this
@@ -59,6 +64,7 @@ export const manifestPath = (root, directory, pathApi = nodePath) => {
59
64
  return related.split(pathApi.sep).join('/');
60
65
  };
61
66
  const MANIFEST_FILE_NAME = 'foldkit.build.json';
67
+ const TEMPLATE_FILE_NAME = 'index.html';
62
68
  const DEFAULT_CLIENT_OUT_DIR = 'dist/client';
63
69
  const DEFAULT_SERVER_OUT_DIR = 'dist/server';
64
70
  const DEFAULT_PRERENDER_ORIGIN = 'http://localhost';
@@ -162,26 +168,6 @@ const prerenderOptionsFrom = (prerender) => {
162
168
  }
163
169
  return prerender === true ? {} : prerender;
164
170
  };
165
- // Keyed by Vite root plus the output layout, so concurrent builds of different
166
- // projects in one process never read each other's output.
167
- //
168
- // NOTE: the registry hangs off a global symbol rather than module scope. Vite
169
- // re-bundles a config file for each environment it resolves, and every bundle
170
- // is a fresh copy of this module with its own module scope, so what the client
171
- // build recorded would be invisible to the instance that finalizes. The symbol
172
- // is one registry for the process no matter how many copies of this module it
173
- // loads.
174
- const CAPTURES = Symbol.for('foldkit/vite-plugin:build-captures');
175
- const captures = (() => {
176
- const registry = globalThis;
177
- const existing = registry[CAPTURES];
178
- if (existing instanceof Map) {
179
- return existing;
180
- }
181
- const fresh = new Map();
182
- registry[CAPTURES] = fresh;
183
- return fresh;
184
- })();
185
171
  const fetchModuleSource = (serverEntry, template, containerId) => {
186
172
  const containerLiteral = containerId === undefined ? 'undefined' : JSON.stringify(containerId);
187
173
  // NOTE: `export *` re-exports whatever the application entry actually names,
@@ -212,7 +198,7 @@ const fetchModuleSource = (serverEntry, template, containerId) => {
212
198
  // page which cannot hydrate, from a build that reported success.
213
199
  const templateForFetchModule = (capturedTemplate) => {
214
200
  if (capturedTemplate === undefined) {
215
- throw new Error('[foldkit] the browser build has not emitted index.html, so the fetch handler has no template to render into. Build the "client" environment before "ssr", and give the client an HTML entry.');
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.`);
216
202
  }
217
203
  return capturedTemplate;
218
204
  };
@@ -220,13 +206,14 @@ const templateForFetchModule = (capturedTemplate) => {
220
206
  * Builds a Web `fetch` handler alongside the browser build, and generates
221
207
  * static HTML from the server entry, inside one `vite build`.
222
208
  *
223
- * Vite drives both environments and every host plugin composes with them, so a
224
- * deployment target that runs `vite build` gets the whole application rather
225
- * than the browser half. The generated pages take their template from the
226
- * browser build's own output, so generating twice over one build produces the
227
- * same pages. The server bundle's default export is `{ fetch }`.
209
+ * Vite builds both environments, so a deployment target that runs `vite build`
210
+ * gets the browser and server bundles. The `fetch` handler and generated pages
211
+ * use the HTML emitted by the browser build, but the unrendered template is not
212
+ * published with the assets. The server bundle's default export is `{ fetch }`.
228
213
  */
229
214
  export const foldkitBuild = (serverEntry, options = {}) => {
215
+ const state = {};
216
+ let metadata;
230
217
  const clientOutDir = options.clientOutDir ?? DEFAULT_CLIENT_OUT_DIR;
231
218
  const serverOutDir = options.serverOutDir ?? DEFAULT_SERVER_OUT_DIR;
232
219
  const prerender = prerenderOptionsFrom(options.prerender ?? false);
@@ -235,18 +222,19 @@ export const foldkitBuild = (serverEntry, options = {}) => {
235
222
  // with the build's own privileges. That module is the application's own code
236
223
  // and its dependencies, built from the configured entry, and is trusted on
237
224
  // exactly those terms. Nothing here is imported when prerendering is off.
238
- const generatePages = async (builder, template, serverDirectory, entryFileName) => {
225
+ const generatePages = async (builder, template, clientDirectory, serverDirectory, entryFileName) => {
239
226
  if (prerender === undefined) {
240
227
  return [];
241
228
  }
242
229
  const origin = prerender.origin ?? DEFAULT_PRERENDER_ORIGIN;
243
- const clientDirectory = resolve(builder.config.root, clientOutDir);
244
230
  const entryFile = resolve(serverDirectory, entryFileName);
245
231
  const contained = resolve(serverDirectory);
246
232
  if (!entryFile.startsWith(`${contained}${nodePath.sep}`)) {
247
233
  throw new Error(`[foldkit] the server entry "${entryFileName}" resolves outside the server build at "${contained}".`);
248
234
  }
249
- const entry = await import(pathToFileURL(entryFile).href);
235
+ const entryUrl = pathToFileURL(entryFile);
236
+ entryUrl.searchParams.set('foldkit-build', randomUUID());
237
+ const entry = await import(entryUrl.href);
250
238
  if (typeof entry.renderPage !== 'function') {
251
239
  throw new Error(`[foldkit] "${entryFileName}" exports no renderPage function, so there is nothing to generate pages with.`);
252
240
  }
@@ -267,39 +255,12 @@ export const foldkitBuild = (serverEntry, options = {}) => {
267
255
  }
268
256
  return paths;
269
257
  };
270
- const writeManifest = async (builder, serverDirectory, entryFileName, prerendered) => {
271
- const manifest = Schema.encodeSync(FoldkitBuildManifest)({
272
- schemaVersion: MANIFEST_SCHEMA_VERSION,
273
- client: manifestPath(builder.config.root, clientOutDir),
274
- server: manifestPath(builder.config.root, serverOutDir),
275
- serverEntry: entryFileName,
276
- prerendered,
277
- host: 'fetch',
278
- });
279
- await writeFile(resolve(serverDirectory, MANIFEST_FILE_NAME), `${JSON.stringify(manifest, undefined, 2)}\n`);
280
- builder.config.logger.info(` wrote ${MANIFEST_FILE_NAME}`);
281
- };
282
- // What each environment emitted, recorded as it is emitted.
283
- //
284
- // Vite resolves the config once per environment unless `sharedConfigBuild` is
285
- // on, so the plugin that finalizes is not necessarily the instance that saw a
286
- // given environment build. Keying the record by the output layout it
287
- // describes is what lets the finalizing instance read what the others
288
- // emitted, and what lets finalization work whether this plugin orchestrates
289
- // the environments or a host does.
290
- const key = [clientOutDir, serverOutDir, serverEntry].join('\u0000');
291
- const captured = (root) => {
292
- const existing = captures.get(`${root}\u0000${key}`);
293
- if (existing !== undefined) {
294
- return existing;
295
- }
296
- const fresh = {};
297
- captures.set(`${root}\u0000${key}`, fresh);
298
- return fresh;
299
- };
300
258
  const finalize = async (builder) => {
301
- const state = captured(builder.config.root);
302
- const serverDirectory = resolve(builder.config.root, serverOutDir);
259
+ metadata = undefined;
260
+ const client = environmentNamed(builder, 'client');
261
+ const server = environmentNamed(builder, 'ssr');
262
+ const clientDirectory = resolve(client.config.root, client.config.build.outDir);
263
+ const serverDirectory = resolve(server.config.root, server.config.build.outDir);
303
264
  if (state.serverEntryFile === undefined) {
304
265
  throw new Error('[foldkit] the server environment produced no entry chunk, so there is nothing to deploy or generate from.');
305
266
  }
@@ -307,20 +268,58 @@ export const foldkitBuild = (serverEntry, options = {}) => {
307
268
  // nothing has no use for an HTML entry and must not require one.
308
269
  const template = () => {
309
270
  if (state.template === undefined) {
310
- throw new Error('[foldkit] the browser build emitted no index.html to generate pages from. Prerendering needs an HTML entry.');
271
+ throw new Error(`[foldkit] the browser build emitted no ${TEMPLATE_FILE_NAME} to generate pages from. Prerendering needs an HTML entry.`);
311
272
  }
312
273
  return state.template;
313
274
  };
314
- const prerendered = await generatePages(builder, template, serverDirectory, state.serverEntryFile);
315
- await writeManifest(builder, serverDirectory, state.serverEntryFile, prerendered);
275
+ const prerendered = await generatePages(builder, template, clientDirectory, serverDirectory, state.serverEntryFile);
276
+ const manifest = FoldkitBuildManifest.make({
277
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
278
+ client: manifestPath(builder.config.root, clientDirectory),
279
+ server: manifestPath(builder.config.root, serverDirectory),
280
+ serverEntry: state.serverEntryFile,
281
+ prerendered: [...prerendered],
282
+ });
283
+ await writeFile(resolve(serverDirectory, MANIFEST_FILE_NAME), `${JSON.stringify(Schema.encodeSync(FoldkitBuildManifest)(manifest), undefined, 2)}\n`);
284
+ builder.config.logger.info(` wrote ${MANIFEST_FILE_NAME}`);
285
+ const completedMetadata = FoldkitBuildMetadata.make({
286
+ root: builder.config.root,
287
+ clientDirectory,
288
+ serverDirectory,
289
+ serverEntry: resolve(serverDirectory, state.serverEntryFile),
290
+ manifest,
291
+ });
292
+ Object.freeze(completedMetadata.manifest.prerendered);
293
+ Object.freeze(completedMetadata.manifest);
294
+ metadata = Object.freeze(completedMetadata);
316
295
  };
317
296
  return {
318
297
  name: 'foldkit:build',
319
298
  apply: 'build',
299
+ sharedDuringBuild: true,
320
300
  api: {
321
- host: 'fetch',
322
301
  serverEntry,
323
302
  fetchModuleId: FOLDKIT_FETCH_MODULE_ID,
303
+ getBuildMetadata: () => {
304
+ if (metadata === undefined) {
305
+ throw new Error('[foldkit] build metadata is not available. Read it after a successful builder.buildApp().');
306
+ }
307
+ return metadata;
308
+ },
309
+ },
310
+ buildStart: {
311
+ order: 'pre',
312
+ handler() {
313
+ if (this.environment.name === 'client') {
314
+ delete state.template;
315
+ delete state.serverEntryFile;
316
+ metadata = undefined;
317
+ }
318
+ else if (this.environment.name === 'ssr') {
319
+ delete state.serverEntryFile;
320
+ metadata = undefined;
321
+ }
322
+ },
324
323
  },
325
324
  resolveId(id) {
326
325
  if (id === FOLDKIT_FETCH_MODULE_ID) {
@@ -332,27 +331,28 @@ export const foldkitBuild = (serverEntry, options = {}) => {
332
331
  if (id !== RESOLVED_FETCH_MODULE_ID) {
333
332
  return;
334
333
  }
335
- const state = captured(this.environment.config.root);
336
334
  const template = templateForFetchModule(state.template);
337
335
  return fetchModuleSource(serverEntry, template, containerId);
338
336
  },
339
- // NOTE: `writeBundle` rather than `generateBundle`: Vite's own HTML plugin
340
- // emits `index.html` from a `generateBundle` hook of its own, and hook
341
- // order between plugins decides whether that asset exists yet. By
342
- // `writeBundle` the bundle is whatever the environment actually produced.
343
- writeBundle(_options, bundle) {
344
- const state = captured(this.environment.config.root);
345
- const outputs = Object.values(bundle);
346
- if (this.environment.name === 'client') {
347
- const html = Array.findFirst(outputs, file => file.type === 'asset' && file.fileName === 'index.html');
348
- if (html._tag === 'Some' && html.value.type === 'asset') {
349
- state.template = String(html.value.source);
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
+ generateBundle: {
340
+ order: 'post',
341
+ handler(_options, bundle) {
342
+ if (this.environment.name === 'ssr') {
343
+ state.serverEntryFile = serverEntryFile(Object.values(bundle), FETCH_CHUNK_NAME);
344
+ return;
350
345
  }
351
- return;
352
- }
353
- if (this.environment.name === 'ssr') {
354
- state.serverEntryFile = serverEntryFile(outputs, FETCH_CHUNK_NAME);
355
- }
346
+ if (this.environment.name !== 'client') {
347
+ return;
348
+ }
349
+ const html = bundle[TEMPLATE_FILE_NAME];
350
+ if (html === undefined || html.type !== 'asset') {
351
+ return;
352
+ }
353
+ state.template = String(html.source);
354
+ delete bundle[TEMPLATE_FILE_NAME];
355
+ },
356
356
  },
357
357
  config: userConfig => {
358
358
  const client = {
@@ -1,26 +1,18 @@
1
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.
2
+ /** The build id explicitly supplied through plugin configuration or the
3
+ * environment, or `undefined` when neither supplied a nonempty value.
4
4
  *
5
5
  * @internal Exported for tests.
6
6
  */
7
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.
8
+ /** The id a standalone plugin compiles for one Vite command.
10
9
  *
11
10
  * @internal Exported for tests.
12
11
  */
13
12
  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`.
13
+ /** Compiles one build identity into application entries and Foldkit itself.
18
14
  *
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.
15
+ * @internal
24
16
  */
25
- export declare const foldkitBuildToken: (buildId?: string) => Plugin;
17
+ export declare const foldkitBuildToken: (buildId?: string, verifyFrameworkIdentity?: boolean) => Array<Plugin>;
26
18
  //# sourceMappingURL=buildToken.d.ts.map
@@ -1 +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,gBAAiB,MAAM,KAAG,MAAM,GAAG,SAQ7D,CAAA;AAcD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,YACnB,OAAO,GAAG,OAAO,eACb,MAAM,KAClB,MAAM,GAAG,SAMX,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,aAAc,MAAM,KAAG,MAYnD,CAAA"}
1
+ {"version":3,"file":"buildToken.d.ts","sourceRoot":"","sources":["../src/buildToken.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAA+B,MAAM,MAAM,CAAA;AAoC/D;;;;GAIG;AACH,eAAO,MAAM,cAAc,gBAAiB,MAAM,KAAG,MAAM,GAAG,SAQ7D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,YACnB,OAAO,GAAG,OAAO,eACb,MAAM,KAClB,MAAM,GAAG,SAMX,CAAA;AA0ID;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAClB,MAAM,wCAEf,KAAK,CAAC,MAAM,CA+Dd,CAAA"}