@foldkit/vite-plugin 0.24.0 → 0.25.0-canary.24f1c43eaaf7

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.
@@ -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
@@ -2,18 +2,21 @@ import type { Plugin } from 'vite';
2
2
  import { type FoldkitBuildOptions } from './build.js';
3
3
  import { type FoldkitSsrOptions } from './ssr.js';
4
4
  export { type BrandDistResult, brandDistDirectory } from './brandDist.js';
5
- export { FOLDKIT_FETCH_MODULE_ID, FoldkitBuildManifest, type FoldkitBuildOptions, type FoldkitPrerenderOptions, foldkitBuild, } from './build.js';
5
+ export { FOLDKIT_FETCH_MODULE_ID, FoldkitBuildManifest, FoldkitBuildMetadata, type FoldkitBuildApi, type FoldkitBuildOptions, type FoldkitPrerenderOptions, foldkitBuild, } from './build.js';
6
6
  export { type FoldkitSsrOptions, foldkitSsr } from './ssr.js';
7
7
  export { type ViewIdentityTransformResult, foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
8
8
  /** Options for the `foldkit` Vite plugin. */
9
9
  export type FoldkitPluginOptions = Readonly<{
10
10
  /**
11
- * Port for the WebSocket server that exposes the DevTools relay to an
12
- * external MCP server. When `undefined` (the default), no MCP relay is
13
- * started. When set, the plugin listens on this port for connections from
14
- * the Foldkit DevTools MCP server.
11
+ * By default, the dev server hosts the DevTools MCP relay and publishes its
12
+ * address for the MCP server to find. Middleware and HTTPS servers use a
13
+ * separate loopback listener. Published addresses carry an access token.
14
+ *
15
+ * A number starts an unauthenticated listener on that port on every
16
+ * interface; set `FOLDKIT_DEVTOOLS_MCP_PORT` in the MCP server to match.
17
+ * `false` disables the relay. Vitest never starts it.
15
18
  */
16
- devToolsMcpPort?: number;
19
+ devToolsMcpPort?: number | false;
17
20
  /**
18
21
  * Serve server-rendered pages from the Vite dev server, and, with
19
22
  * `ssr.build`, emit a Web `fetch` handler as the server bundle. When
@@ -35,25 +38,21 @@ export type FoldkitPluginOptions = Readonly<{
35
38
  build?: boolean | FoldkitBuildOptions;
36
39
  }>;
37
40
  /**
38
- * The deployment this build belongs to, compiled into application code as
39
- * `import.meta.env.FOLDKIT_BUILD_ID` for the entries to pass to
40
- * `renderToString` and `Runtime.hydrate`. Hydration compares it against the id
41
- * the server stamped and refuses a page from another deployment rather than
42
- * adopting it: startup stops and the page is contained, with the document's
43
- * 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.
44
46
  *
45
- * Defaults to the `FOLDKIT_BUILD_ID` environment variable. Use a value the
46
- * deployment already has, such as a commit or a release tag, and give the
47
- * client build and the server build the same one. It is published in the
48
- * 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.
49
52
  *
50
- * Whatever supplies it has to answer with the same value every time it is
51
- * asked, because Vite reads a config file once per environment it builds. A
52
- * config that computes a fresh value on each read — `randomUUID()`, a
53
- * timestamp — gives the browser bundle and the server bundle different ids
54
- * within one build, and every page of that deployment is then refused at
55
- * hydration. Read it from the environment, or store a generated fallback
56
- * 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.
57
56
  */
58
57
  buildId?: string;
59
58
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EACV,MAAM,EAIP,MAAM,MAAM,CAAA;AAGb,OAAO,EAAE,KAAK,mBAAmB,EAAgB,MAAM,YAAY,CAAA;AAGnE,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,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;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;;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;AAonBF,eAAO,MAAM,OAAO,aAAa,oBAAoB,KAAQ,KAAK,CAAC,MAAM,CAuFxE,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"}