@foldkit/vite-plugin 0.24.0-canary.eb11872b6977 → 0.24.0-canary.ff73466cfc55

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
@@ -119,34 +119,31 @@ Server-rendered HTML carries the deployment id, and the client bundle carries it
119
119
 
120
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.
121
121
 
122
- 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:
123
123
 
124
124
  ```typescript
125
- 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)
126
130
  ```
127
131
 
128
- 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:
129
133
 
130
134
  ```typescript
131
- // src/entry.server.ts
132
- Server.renderToString(config, {
133
- flags,
134
- buildId: import.meta.env.FOLDKIT_BUILD_ID,
135
- })
136
-
137
- // src/entry.ts
138
- Runtime.hydrate(application, { buildId: import.meta.env.FOLDKIT_BUILD_ID })
135
+ plugins: [foldkit({ buildId: process.env.DEPLOYMENT_ID })]
139
136
  ```
140
137
 
141
- 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:
142
139
 
143
140
  - The id appears in the HTML every visitor receives, so it must never contain a secret.
144
141
  - Two deployments must never share an id.
145
- - 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.
146
143
 
147
- 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.
148
145
 
149
- 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.
150
147
 
151
148
  ## DevTools overlay
152
149
 
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAG/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,CAmPxB,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,4 +1,5 @@
1
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';
@@ -231,7 +232,9 @@ export const foldkitBuild = (serverEntry, options = {}) => {
231
232
  if (!entryFile.startsWith(`${contained}${nodePath.sep}`)) {
232
233
  throw new Error(`[foldkit] the server entry "${entryFileName}" resolves outside the server build at "${contained}".`);
233
234
  }
234
- 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);
235
238
  if (typeof entry.renderPage !== 'function') {
236
239
  throw new Error(`[foldkit] "${entryFileName}" exports no renderPage function, so there is nothing to generate pages with.`);
237
240
  }
@@ -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"}
@@ -1,38 +1,28 @@
1
+ import MagicString from 'magic-string';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { isFoldkitSingletonPackageSpecifier } from './foldkitPackages.js';
1
4
  // The build id names the deployment a page came from. The server stamps it on
2
5
  // the rendered root, the client carries it, and hydration refuses a page whose
3
6
  // id is not its own before it adopts any DOM.
4
7
  //
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.
8
+ // The aggregate plugin generates one opaque id when a Vite app build contains
9
+ // both the client and ssr environments. The id lives on the ViteBuilder's
10
+ // ResolvedConfig object, so concurrent builders cannot overwrite one another
11
+ // and a later builder in the same process gets a fresh value. Separately
12
+ // invoked builds have no shared session and must receive an explicit value.
13
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.
14
+ // Foldkit's own build-token module contains a placeholder call. This transform
15
+ // replaces that call in both artifacts, which lets Runtime.hydrate and
16
+ // renderToString consume the value without application forwarding. Installed
17
+ // Foldkit must therefore participate in the server build; the aggregate plugin
18
+ // configures that boundary and the post-build check refuses an artifact that
19
+ // still imports Foldkit externally.
33
20
  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.
21
+ const FRAMEWORK_BUILD_ID_PLACEHOLDER = 'foldkitBuildIdPlaceholder()';
22
+ const buildIdentitySessions = new WeakMap();
23
+ const developmentBuildIds = new WeakMap();
24
+ /** The build id explicitly supplied through plugin configuration or the
25
+ * environment, or `undefined` when neither supplied a nonempty value.
36
26
  *
37
27
  * @internal Exported for tests.
38
28
  */
@@ -45,19 +35,7 @@ export const resolveBuildId = (configured) => {
45
35
  ? fromEnvironment
46
36
  : undefined;
47
37
  };
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.
38
+ /** The id a standalone plugin compiles for one Vite command.
61
39
  *
62
40
  * @internal Exported for tests.
63
41
  */
@@ -66,29 +44,140 @@ export const buildIdForCommand = (command, configured) => {
66
44
  if (resolved !== undefined) {
67
45
  return resolved;
68
46
  }
69
- return command === 'serve' ? DEVELOPMENT_BUILD_ID : undefined;
47
+ return command === 'serve' ? 'development' : undefined;
48
+ };
49
+ const hasCoordinatedArtifacts = (builder) => builder.environments['client'] !== undefined &&
50
+ builder.environments['ssr'] !== undefined;
51
+ const beginBuildIdentity = (builder, configuredBuildId, verifyFrameworkIdentity) => {
52
+ const isCoordinated = hasCoordinatedArtifacts(builder);
53
+ const buildId = configuredBuildId ?? (isCoordinated ? randomUUID() : undefined);
54
+ const session = {
55
+ buildId,
56
+ externalizedEnvironments: new Set(),
57
+ verifyFrameworkIdentity,
58
+ };
59
+ buildIdentitySessions.set(builder.config, session);
60
+ for (const environment of Object.values(builder.environments)) {
61
+ buildIdentitySessions.set(environment.config, session);
62
+ buildIdentitySessions.set(environment.getTopLevelConfig(), session);
63
+ }
64
+ };
65
+ const buildIdForConfig = (config, configuredBuildId) => {
66
+ if (config.command === 'build') {
67
+ return buildIdentitySessions.get(config)?.buildId ?? configuredBuildId;
68
+ }
69
+ const existing = developmentBuildIds.get(config);
70
+ if (existing !== undefined) {
71
+ return existing;
72
+ }
73
+ const fresh = configuredBuildId ?? randomUUID();
74
+ developmentBuildIds.set(config, fresh);
75
+ return fresh;
76
+ };
77
+ const replaceAll = (source, code, search, replacement) => {
78
+ let didReplace = false;
79
+ let fromIndex = 0;
80
+ while (fromIndex < code.length) {
81
+ const index = code.indexOf(search, fromIndex);
82
+ if (index === -1) {
83
+ return didReplace;
84
+ }
85
+ source.overwrite(index, index + search.length, replacement);
86
+ didReplace = true;
87
+ fromIndex = index + search.length;
88
+ }
89
+ return didReplace;
70
90
  };
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`.
91
+ const isFoldkitBuildTokenModule = (id) => {
92
+ const fileName = (id.split('?', 1)[0] ?? '').replaceAll('\\', '/');
93
+ return (fileName.endsWith('/foldkit/src/buildToken.ts') ||
94
+ fileName.endsWith('/foldkit/dist/buildToken.js'));
95
+ };
96
+ const hasExternalFoldkitSingletonImport = (imports, dynamicImports) => [...imports, ...dynamicImports].some(isFoldkitSingletonPackageSpecifier);
97
+ const transformBuildIdentity = (code, id, config, configuredBuildId) => {
98
+ if (!isFoldkitBuildTokenModule(id) ||
99
+ !code.includes(FRAMEWORK_BUILD_ID_PLACEHOLDER)) {
100
+ return undefined;
101
+ }
102
+ const buildId = buildIdForConfig(config, configuredBuildId);
103
+ const replacement = buildId === undefined ? 'undefined' : JSON.stringify(buildId);
104
+ const transformed = new MagicString(code);
105
+ const transformedFramework = replaceAll(transformed, code, FRAMEWORK_BUILD_ID_PLACEHOLDER, replacement);
106
+ if (!transformedFramework) {
107
+ return undefined;
108
+ }
109
+ return {
110
+ code: transformed.toString(),
111
+ map: transformed.generateMap({ hires: 'boundary', source: id }),
112
+ };
113
+ };
114
+ const verifyBuildIdentity = (builder) => {
115
+ const session = buildIdentitySessions.get(builder.config);
116
+ if (session === undefined || !session.verifyFrameworkIdentity) {
117
+ return;
118
+ }
119
+ const externalized = [...session.externalizedEnvironments];
120
+ if (externalized.length === 0) {
121
+ return;
122
+ }
123
+ throw new Error('[foldkit] A Foldkit singleton package was externalized from the ' +
124
+ `${externalized.join(' and ')} ` +
125
+ `${externalized.length === 1 ? 'artifact' : 'artifacts'}, so it can ` +
126
+ 'load a framework copy whose hydration build identity was not compiled. ' +
127
+ 'Remove Foldkit packages from explicit SSR or Rolldown externalization ' +
128
+ 'settings and let @foldkit/vite-plugin bundle them.');
129
+ };
130
+ /** Compiles one build identity into application entries and Foldkit itself.
75
131
  *
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.
132
+ * @internal
81
133
  */
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),
134
+ export const foldkitBuildToken = (buildId, verifyFrameworkIdentity = true) => {
135
+ const configuredBuildId = resolveBuildId(buildId);
136
+ return [
137
+ {
138
+ name: 'foldkit:build-token',
139
+ enforce: 'pre',
140
+ sharedDuringBuild: true,
141
+ config: (_config, { command }) => {
142
+ const legacyBuildId = configuredBuildId ?? (command === 'serve' ? 'development' : undefined);
143
+ return legacyBuildId === undefined
144
+ ? {}
145
+ : {
146
+ define: {
147
+ 'import.meta.env.FOLDKIT_BUILD_ID': JSON.stringify(legacyBuildId),
148
+ },
149
+ };
150
+ },
151
+ buildApp: {
152
+ order: 'pre',
153
+ async handler(builder) {
154
+ beginBuildIdentity(builder, configuredBuildId, verifyFrameworkIdentity);
91
155
  },
92
- };
93
- },
94
- });
156
+ },
157
+ transform(code, id) {
158
+ const config = this.environment.getTopLevelConfig();
159
+ return transformBuildIdentity(code, id, config, configuredBuildId);
160
+ },
161
+ generateBundle(_options, bundle) {
162
+ const isExternalized = Object.values(bundle).some(output => output.type === 'chunk' &&
163
+ hasExternalFoldkitSingletonImport(output.imports, output.dynamicImports));
164
+ if (!isExternalized) {
165
+ return;
166
+ }
167
+ buildIdentitySessions
168
+ .get(this.environment.getTopLevelConfig())
169
+ ?.externalizedEnvironments.add(this.environment.name);
170
+ },
171
+ },
172
+ {
173
+ name: 'foldkit:verify-build-token',
174
+ sharedDuringBuild: true,
175
+ buildApp: {
176
+ order: 'post',
177
+ async handler(builder) {
178
+ verifyBuildIdentity(builder);
179
+ },
180
+ },
181
+ },
182
+ ];
183
+ };
@@ -0,0 +1,12 @@
1
+ /** Tests whether an import refers to a package that must share Foldkit's
2
+ * runtime instance.
3
+ *
4
+ * @internal
5
+ */
6
+ export declare const isFoldkitSingletonPackageSpecifier: (specifier: string) => boolean;
7
+ /** Resolves the installed packages that must share Foldkit's runtime instance.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare const resolveInstalledFoldkitPackages: (root: string) => Array<string>;
12
+ //# sourceMappingURL=foldkitPackages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foldkitPackages.d.ts","sourceRoot":"","sources":["../src/foldkitPackages.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,cAClC,MAAM,KAChB,OAKA,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,+BAA+B,SACpC,MAAM,KACX,KAAK,CAAC,MAAM,CAmBd,CAAA"}
@@ -0,0 +1,37 @@
1
+ import { Array, Predicate } from 'effect';
2
+ import { createRequire } from 'node:module';
3
+ import { resolve } from 'node:path';
4
+ const FOLDKIT_SINGLETON_PACKAGES = [
5
+ 'foldkit',
6
+ '@foldkit/ui',
7
+ '@foldkit/devtools',
8
+ ];
9
+ /** Tests whether an import refers to a package that must share Foldkit's
10
+ * runtime instance.
11
+ *
12
+ * @internal
13
+ */
14
+ export const isFoldkitSingletonPackageSpecifier = (specifier) => Array.some(FOLDKIT_SINGLETON_PACKAGES, packageName => specifier === packageName || specifier.startsWith(`${packageName}/`));
15
+ /** Resolves the installed packages that must share Foldkit's runtime instance.
16
+ *
17
+ * @internal
18
+ */
19
+ export const resolveInstalledFoldkitPackages = (root) => {
20
+ // NOTE: Vite can supply a relative root before it resolves the config, while
21
+ // createRequire requires an absolute path.
22
+ const requireFromRoot = createRequire(resolve(root, 'noop.js'));
23
+ return Array.filter(FOLDKIT_SINGLETON_PACKAGES, packageName => {
24
+ try {
25
+ requireFromRoot.resolve(packageName);
26
+ return true;
27
+ }
28
+ catch (error) {
29
+ // NOTE: optional ESM-only packages can throw
30
+ // ERR_PACKAGE_PATH_NOT_EXPORTED even when installed. Only
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
+ }
36
+ });
37
+ };
package/dist/index.d.ts CHANGED
@@ -38,25 +38,21 @@ export type FoldkitPluginOptions = Readonly<{
38
38
  build?: boolean | FoldkitBuildOptions;
39
39
  }>;
40
40
  /**
41
- * The deployment this build belongs to, compiled into application code as
42
- * `import.meta.env.FOLDKIT_BUILD_ID` for the entries to pass to
43
- * `renderToString` and `Runtime.hydrate`. Hydration compares it against the id
44
- * the server stamped and refuses a page from another deployment rather than
45
- * adopting it: startup stops and the page is contained, with the document's
46
- * body marked `inert`.
41
+ * An explicit identity for the deployment this build belongs to. Foldkit
42
+ * normally generates an opaque identity when one Vite app build coordinates
43
+ * the client and server artifacts, then compiles it into the framework in
44
+ * both. Hydration compares that value against the id the server stamped and
45
+ * refuses a page from another deployment before adopting its DOM.
47
46
  *
48
- * Defaults to the `FOLDKIT_BUILD_ID` environment variable. Use a value the
49
- * deployment already has, such as a commit or a release tag, and give the
50
- * client build and the server build the same one. It is published in the
51
- * page, so it must not be a secret.
47
+ * Set this when the client and server are built separately, or when the id
48
+ * should name a deployment in another system. The `FOLDKIT_BUILD_ID`
49
+ * environment variable supplies the same override when this option is
50
+ * absent. Give every artifact the same value. It is published in the page,
51
+ * so it must not be a secret.
52
52
  *
53
- * Whatever supplies it has to answer with the same value every time it is
54
- * asked, because Vite reads a config file once per environment it builds. A
55
- * config that computes a fresh value on each read — `randomUUID()`, a
56
- * timestamp — gives the browser bundle and the server bundle different ids
57
- * within one build, and every page of that deployment is then refused at
58
- * hydration. Read it from the environment, or store a generated fallback
59
- * back into the environment so later reads resolve the same id.
53
+ * Reusing an override across deployments makes stale pages appear current.
54
+ * Use a value that changes whenever the deployment's rendering inputs can
55
+ * change.
60
56
  */
61
57
  buildId?: string;
62
58
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAEV,MAAM,EAIP,MAAM,MAAM,CAAA;AAOb,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,YAAY,CAAA;AAInE,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,UAAU,CAAA;AAG7D,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAChC;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,SAAS,GAAG,gBAAgB,CAAC,GACzD,QAAQ,CAAC;QACP;;;;;;;;WAQG;QACH,KAAK,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAA;KACtC,CAAC,CAAA;IACJ;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAC,CAAA;AAw8BF,eAAO,MAAM,OAAO,aAAa,oBAAoB,KAAQ,KAAK,CAAC,MAAM,CA+FxE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAEV,MAAM,EAIP,MAAM,MAAM,CAAA;AAOb,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,YAAY,CAAA;AAKnE,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,UAAU,CAAA;AAG7D,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,KAAK,iBAAiB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAChC;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,SAAS,GAAG,gBAAgB,CAAC,GACzD,QAAQ,CAAC;QACP;;;;;;;;WAQG;QACH,KAAK,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAA;KACtC,CAAC,CAAA;IACJ;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAC,CAAA;AAs6BF,eAAO,MAAM,OAAO,aAAa,oBAAoB,KAAQ,KAAK,CAAC,MAAM,CAuHxE,CAAA"}
package/dist/index.js CHANGED
@@ -3,8 +3,6 @@ import { Event as DevToolsEvent, EventFrame, RELAY_RECORD_VERSION, RequestFrame,
3
3
  import { PreserveModelMessage, RequestModelMessage, RestoreModelMessage, } from 'foldkit/model-preservation';
4
4
  import { timingSafeEqual } from 'node:crypto';
5
5
  import { createServer as createHttpServer, } from 'node:http';
6
- import { createRequire } from 'node:module';
7
- import { resolve } from 'node:path';
8
6
  import { WebSocketServer } from 'ws';
9
7
  import * as NodeCrypto from '@effect/platform-node/NodeCrypto';
10
8
  import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem';
@@ -12,6 +10,7 @@ import * as NodePath from '@effect/platform-node/NodePath';
12
10
  import { foldkitBuild } from './build.js';
13
11
  import { foldkitBuildToken } from './buildToken.js';
14
12
  import { devToolsOverlayPlugin } from './devToolsOverlay.js';
13
+ import { resolveInstalledFoldkitPackages } from './foldkitPackages.js';
15
14
  import { publishRelayRecord, retireRelayRecord } from './relayRegistry.js';
16
15
  import { foldkitSsr } from './ssr.js';
17
16
  import { foldkitViewIdentity } from './viewIdentity.js';
@@ -71,37 +70,6 @@ const FORCE_INCLUDED_EFFECT_NAMESPACES = [
71
70
  'effect/SubscriptionRef',
72
71
  'effect/Types',
73
72
  ];
74
- // NOTE: a duplicate `foldkit` instance is its own hazard. If a bundler
75
- // resolves `foldkit` (or a foldkit-consuming package like `@foldkit/ui`) to
76
- // more than one copy, the copies get distinct Schema and tagged-message
77
- // identities (decode and tag matching fail across the boundary) and separate
78
- // module-level singleton state. `resolve.dedupe` (below) collapses every
79
- // installed Foldkit package to one resolved copy.
80
- const FOLDKIT_SINGLETON_PACKAGES = [
81
- 'foldkit',
82
- '@foldkit/ui',
83
- '@foldkit/devtools',
84
- ];
85
- // NOTE: `@foldkit/ui` and `@foldkit/devtools` are optional, so dedupe only
86
- // the ones the consumer installed. An installed ESM package resolves to
87
- // ERR_PACKAGE_PATH_NOT_EXPORTED rather than succeeding, so a missing package
88
- // is signalled only by MODULE_NOT_FOUND.
89
- const resolveInstalledFoldkitPackages = (root) => {
90
- // NOTE: `root` (Vite's `config.root`) can be relative at config-hook time,
91
- // and createRequire requires an absolute path; `resolve` normalizes it.
92
- const requireFromRoot = createRequire(resolve(root, 'noop.js'));
93
- return Array.filter(FOLDKIT_SINGLETON_PACKAGES, packageName => {
94
- try {
95
- requireFromRoot.resolve(packageName);
96
- return true;
97
- }
98
- catch (error) {
99
- return !(error instanceof Error &&
100
- Predicate.hasProperty(error, 'code') &&
101
- error.code === 'MODULE_NOT_FOUND');
102
- }
103
- });
104
- };
105
73
  const Event = Data.taggedEnum();
106
74
  const makeState = Effect.gen(function* () {
107
75
  const preservedModels = yield* Ref.make(HashMap.empty());
@@ -545,13 +513,10 @@ export const foldkit = (options = {}) => {
545
513
  const reloadPlugin = {
546
514
  name: 'foldkit',
547
515
  apply: 'serve',
548
- config: userConfig => ({
516
+ config: () => ({
549
517
  optimizeDeps: {
550
518
  include: [...FORCE_INCLUDED_EFFECT_NAMESPACES],
551
519
  },
552
- resolve: {
553
- dedupe: resolveInstalledFoldkitPackages(userConfig.root ?? process.cwd()),
554
- },
555
520
  }),
556
521
  configureServer: server => {
557
522
  const events = Effect.runSync(Queue.unbounded());
@@ -577,8 +542,33 @@ export const foldkit = (options = {}) => {
577
542
  return [];
578
543
  },
579
544
  };
545
+ const resolutionPlugin = {
546
+ name: 'foldkit:resolution',
547
+ config: userConfig => {
548
+ const singletonPackages = resolveInstalledFoldkitPackages(userConfig.root ?? process.cwd());
549
+ return {
550
+ optimizeDeps: {
551
+ exclude: ['foldkit'],
552
+ },
553
+ resolve: {
554
+ dedupe: singletonPackages,
555
+ },
556
+ ssr: {
557
+ noExternal: singletonPackages,
558
+ },
559
+ environments: {
560
+ ssr: {
561
+ resolve: {
562
+ noExternal: singletonPackages,
563
+ },
564
+ },
565
+ },
566
+ };
567
+ },
568
+ };
580
569
  const shared = [
581
- foldkitBuildToken(options.buildId),
570
+ resolutionPlugin,
571
+ ...foldkitBuildToken(options.buildId),
582
572
  foldkitViewIdentity(),
583
573
  devToolsOverlayPlugin(),
584
574
  reloadPlugin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/vite-plugin",
3
- "version": "0.24.0-canary.eb11872b6977",
3
+ "version": "0.24.0-canary.ff73466cfc55",
4
4
  "description": "Vite plugin for Foldkit with state-preserving live reload",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,27 +16,28 @@
16
16
  "dist"
17
17
  ],
18
18
  "peerDependencies": {
19
- "effect": "4.0.0-rc.116",
20
- "foldkit": "0.163.0-canary.eb11872b6977",
19
+ "effect": "4.0.0-rc.117",
20
+ "foldkit": "0.163.0-canary.ff73466cfc55",
21
21
  "vite": "^8.0.0"
22
22
  },
23
23
  "dependencies": {
24
- "@effect/platform-node": "4.0.0-rc.116",
25
- "magic-string": "^1.2.3",
24
+ "@effect/platform-node": "4.0.0-rc.117",
25
+ "@effect/platform-node-shared": "4.0.0-rc.117",
26
+ "magic-string": "^1.4.2",
26
27
  "ws": "^8.21.3"
27
28
  },
28
29
  "devDependencies": {
29
- "@types/node": "^26.4.0",
30
+ "@types/node": "26.4.0",
30
31
  "@types/ws": "^8.18.1",
31
32
  "@vitejs/plugin-basic-ssl": "^2.3.0",
32
- "effect": "4.0.0-rc.116",
33
- "happy-dom": "^20.11.13",
33
+ "effect": "4.0.0-rc.117",
34
+ "happy-dom": "^20.14.5",
34
35
  "rimraf": "^6.1.3",
35
36
  "typescript": "^7.0.2",
36
- "vite": "^8.2.2",
37
+ "vite": "^8.3.1",
37
38
  "vite-host": "npm:vite@8.2.1",
38
39
  "vitest": "^4.1.11",
39
- "foldkit": "0.163.0-canary.eb11872b6977"
40
+ "foldkit": "0.163.0-canary.ff73466cfc55"
40
41
  },
41
42
  "keywords": [
42
43
  "vite",