@multiplatform.one/config 7.11.0 → 7.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { afterAll, describe, expect, it } from "vitest";
8
+
9
+ // MPO-23 ruling 2A (Clay, 2026-09-26): a consumer turns the convention rules
10
+ // on with one `extends` line and no rules block of its own.
11
+ const packageDir = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const pkg = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")) as {
13
+ exports: Record<string, unknown>;
14
+ files: string[];
15
+ };
16
+ const oxlintBin = join(
17
+ dirname(createRequire(import.meta.url).resolve("oxlint/package.json")),
18
+ "bin",
19
+ "oxlint",
20
+ );
21
+
22
+ const project = mkdtempSync(join(tmpdir(), "mpo-oxlint-preset-"));
23
+ afterAll(() => rmSync(project, { recursive: true, force: true }));
24
+
25
+ function lint(files: Record<string, string>): string {
26
+ mkdirSync(join(project, "node_modules", "@multiplatform.one"), { recursive: true });
27
+ try {
28
+ symlinkSync(packageDir, join(project, "node_modules", "@multiplatform.one", "config"));
29
+ } catch {
30
+ // already linked by an earlier case
31
+ }
32
+ writeFileSync(
33
+ join(project, ".oxlintrc.json"),
34
+ JSON.stringify({ extends: ["./node_modules/@multiplatform.one/config/oxlint.json"] }),
35
+ );
36
+ for (const [path, source] of Object.entries(files)) {
37
+ mkdirSync(dirname(join(project, path)), { recursive: true });
38
+ writeFileSync(join(project, path), source);
39
+ }
40
+ try {
41
+ return execFileSync(oxlintBin, ["-c", ".oxlintrc.json", ...Object.keys(files)], {
42
+ cwd: project,
43
+ encoding: "utf8",
44
+ });
45
+ } catch (error) {
46
+ return String((error as { stdout?: string }).stdout ?? "");
47
+ }
48
+ }
49
+
50
+ describe("oxlint preset (MPO-23 2A)", () => {
51
+ it("ships as ./oxlint.json and is in the published files", () => {
52
+ expect(pkg.exports["./oxlint.json"]).toBe("./oxlint.json");
53
+ expect(pkg.files).toContain("oxlint.json");
54
+ });
55
+
56
+ it("an extends line alone turns the hex rule on, and specs stay exempt", () => {
57
+ const out = lint({
58
+ "features/demo/Swatch.tsx": 'export const swatch = "#ff0000";\n',
59
+ "features/demo/Swatch.spec.tsx": 'export const fixture = "#00ff00";\n',
60
+ });
61
+ expect(out).toContain("mpo-conventions(no-hex-literals)");
62
+ expect(out).toContain("Swatch.tsx");
63
+ expect(out).not.toContain("Swatch.spec.tsx");
64
+ });
65
+ });
@@ -0,0 +1,272 @@
1
+ /**
2
+ * publicConfigFile (MPO-247): config.json is read, not imported, so an edit
3
+ * never restarts the dev server; the plugin inlines the bake, watches the file
4
+ * and reloads clients with the new keys instead.
5
+ */
6
+
7
+ import { EventEmitter } from "node:events";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
+ import { loadConfigFromFile } from "vite";
13
+ import type { Plugin, ViteDevServer } from "vite";
14
+ import { publicConfigPlugin, readPublicConfigKeys } from "./publicConfig.js";
15
+
16
+ const bakedKeys = "VITE_MP_PUBLIC_CONFIG_KEYS";
17
+ const bakedValues = "VITE_MP_CONFIG";
18
+
19
+ let dir: string;
20
+ let file: string;
21
+
22
+ beforeEach(() => {
23
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "mpo-public-config-"));
24
+ file = path.join(dir, "config.json");
25
+ delete process.env[bakedKeys];
26
+ delete process.env[bakedValues];
27
+ });
28
+
29
+ afterEach(() => {
30
+ fs.rmSync(dir, { recursive: true, force: true });
31
+ delete process.env.MPO_SPEC_FOO;
32
+ delete process.env.MPO_SPEC_BAR;
33
+ });
34
+
35
+ function writeConfig(publicKeys: unknown, extra: Record<string, unknown> = {}) {
36
+ fs.writeFileSync(file, JSON.stringify({ private: ["SECRET"], public: publicKeys, ...extra }));
37
+ }
38
+
39
+ type FakeEnvironment = {
40
+ modules: Map<string, { id: string }>;
41
+ moduleGraph: {
42
+ getModuleById: (id: string) => unknown;
43
+ invalidateModule: ReturnType<typeof vi.fn>;
44
+ };
45
+ hot: { send: ReturnType<typeof vi.fn> };
46
+ };
47
+
48
+ function fakeServer() {
49
+ const watcher = Object.assign(new EventEmitter(), { add: vi.fn() });
50
+ const environment = (): FakeEnvironment => {
51
+ const modules = new Map<string, { id: string }>();
52
+ return {
53
+ modules,
54
+ moduleGraph: { getModuleById: (id: string) => modules.get(id), invalidateModule: vi.fn() },
55
+ hot: { send: vi.fn() },
56
+ };
57
+ };
58
+ const environments = { client: environment(), ssr: environment() };
59
+ const logger = { info: vi.fn(), warn: vi.fn() };
60
+ const server = { watcher, environments, config: { logger } };
61
+ return { server: server as unknown as ViteDevServer, watcher, environments, logger };
62
+ }
63
+
64
+ type TransformEnvironment = {
65
+ name: string;
66
+ config: { consumer: "client" | "server" };
67
+ depsOptimizer?: { isOptimizedDepFile: (id: string) => boolean };
68
+ };
69
+
70
+ async function transform(
71
+ plugin: Plugin,
72
+ environment: string | TransformEnvironment,
73
+ code: string,
74
+ id: string,
75
+ ) {
76
+ const hook = plugin.transform as (
77
+ this: { environment: TransformEnvironment },
78
+ code: string,
79
+ id: string,
80
+ ) => Promise<{ code: string } | undefined>;
81
+ const context =
82
+ typeof environment === "string"
83
+ ? {
84
+ name: environment,
85
+ config: { consumer: environment === "ssr" ? ("server" as const) : ("client" as const) },
86
+ }
87
+ : environment;
88
+ return await hook.call({ environment: context }, code, id);
89
+ }
90
+
91
+ function configureServer(plugin: Plugin, server: ViteDevServer) {
92
+ (plugin.configureServer as (server: ViteDevServer) => void)(server);
93
+ }
94
+
95
+ describe("readPublicConfigKeys", () => {
96
+ it("returns the public key names", () => {
97
+ writeConfig(["FRAPPE_ENABLED", "FRAPPE_URL"]);
98
+ expect(readPublicConfigKeys(file)).toEqual(["FRAPPE_ENABLED", "FRAPPE_URL"]);
99
+ });
100
+
101
+ it("drops anything that is not a key name and tolerates a missing list", () => {
102
+ writeConfig(["A", 1, "", null, "B"]);
103
+ expect(readPublicConfigKeys(file)).toEqual(["A", "B"]);
104
+ fs.writeFileSync(file, JSON.stringify({ private: ["SECRET"] }));
105
+ expect(readPublicConfigKeys(file)).toEqual([]);
106
+ });
107
+ });
108
+
109
+ describe("config.json as a vite config dependency", () => {
110
+ it("an imported config.json restarts the server on edit; a read one is not tracked at all", async () => {
111
+ writeConfig(["A"]);
112
+ const imported = path.join(dir, "imported.config.mjs");
113
+ const read = path.join(dir, "read.config.mjs");
114
+ fs.writeFileSync(
115
+ imported,
116
+ 'import config from "./config.json" with { type: "json" };\n' +
117
+ "export default { define: { keys: config.public } };\n",
118
+ );
119
+ fs.writeFileSync(
120
+ read,
121
+ 'import fs from "node:fs";\n' +
122
+ 'const config = JSON.parse(fs.readFileSync(new URL("./config.json", import.meta.url), "utf-8"));\n' +
123
+ "export default { define: { keys: config.public } };\n",
124
+ );
125
+ const env = { command: "serve", mode: "development" } as const;
126
+ const viaImport = await loadConfigFromFile(env, imported, dir);
127
+ const viaRead = await loadConfigFromFile(env, read, dir);
128
+ expect(viaImport?.dependencies.some((dep) => dep.endsWith("config.json"))).toBe(true);
129
+ expect(viaRead?.dependencies.some((dep) => dep.endsWith("config.json"))).toBe(false);
130
+ expect(viaRead?.config.define).toEqual({ keys: ["A"] });
131
+ });
132
+ });
133
+
134
+ describe("publicConfigPlugin", () => {
135
+ it("bakes on construction and inlines exactly the two baked expressions", async () => {
136
+ writeConfig(["MPO_SPEC_FOO"]);
137
+ process.env.MPO_SPEC_FOO = "bar";
138
+ const plugin = publicConfigPlugin({ file, projectRoot: dir, keys: ["INLINE"] });
139
+ expect(plugin.apply).toBe("serve");
140
+ expect(JSON.parse(process.env[bakedKeys]!)).toEqual(["INLINE", "MPO_SPEC_FOO"]);
141
+ const out = await transform(
142
+ plugin,
143
+ "client",
144
+ "const a = process.env.VITE_MP_CONFIG;\n" +
145
+ "const b = process.env.VITE_MP_PUBLIC_CONFIG_KEYS;\n" +
146
+ "const c = process.env.VITE_MP_CONFIGX + process.env.OTHER;\n",
147
+ "/app/runtimeConfig.ts",
148
+ );
149
+ expect(out?.code).toBe(
150
+ `const a = ${JSON.stringify(JSON.stringify({ MPO_SPEC_FOO: "bar" }))};\n` +
151
+ `const b = ${JSON.stringify(JSON.stringify(["INLINE", "MPO_SPEC_FOO"]))};\n` +
152
+ "const c = process.env.VITE_MP_CONFIGX + process.env.OTHER;\n",
153
+ );
154
+ expect(await transform(plugin, "client", "const d = process.env.OTHER;", "/app/other.ts")).toBe(
155
+ undefined,
156
+ );
157
+ expect(
158
+ await transform(plugin, "client", '{"x":"process.env.VITE_MP_CONFIG"}', "/app/data.json"),
159
+ ).toBe(undefined);
160
+ });
161
+
162
+ it("replaces only real reads: strings, assignment targets and globalThis reads stay as written", async () => {
163
+ writeConfig(["MPO_SPEC_FOO"]);
164
+ process.env.MPO_SPEC_FOO = "bar";
165
+ const plugin = publicConfigPlugin({ file, projectRoot: dir });
166
+ const out = await transform(
167
+ plugin,
168
+ "ssr",
169
+ 'const hint = "set process.env.VITE_MP_CONFIG first";\n' +
170
+ 'globalThis.process.env.VITE_MP_CONFIG ??= "{}";\n' +
171
+ 'process.env.VITE_MP_PUBLIC_CONFIG_KEYS = "[]";\n' +
172
+ "export const a = process?.env?.VITE_MP_CONFIG;\n",
173
+ "/app/runtimeConfig.ts",
174
+ );
175
+ expect(out?.code).toContain('"set process.env.VITE_MP_CONFIG first"');
176
+ expect(out?.code).toContain("globalThis.process.env.VITE_MP_CONFIG ??=");
177
+ expect(out?.code).toContain("process.env.VITE_MP_PUBLIC_CONFIG_KEYS =");
178
+ expect(out?.code).toContain(
179
+ `export const a = ${JSON.stringify(JSON.stringify({ MPO_SPEC_FOO: "bar" }))};`,
180
+ );
181
+ });
182
+
183
+ it("leaves browser-cached client files to vite's injected define and still inlines them for ssr", async () => {
184
+ writeConfig(["MPO_SPEC_FOO"]);
185
+ const plugin = publicConfigPlugin({ file, projectRoot: dir });
186
+ const code = "export const keys = process.env.VITE_MP_PUBLIC_CONFIG_KEYS;";
187
+ const inlined = `export const keys = ${JSON.stringify(JSON.stringify(["MPO_SPEC_FOO"]))};\n`;
188
+ const installed = "/proj/node_modules/@multiplatform.one/platform/lib/config/runtimeConfig.js";
189
+ const optimized = "/proj/.cache/vite/deps/@multiplatform__one_platform.js";
190
+ expect(await transform(plugin, "client", code, installed)).toBeUndefined();
191
+ expect((await transform(plugin, "ssr", code, installed))?.code).toBe(inlined);
192
+ const optimizer = { isOptimizedDepFile: (id: string) => id === optimized };
193
+ expect(
194
+ await transform(
195
+ plugin,
196
+ { name: "client", config: { consumer: "client" }, depsOptimizer: optimizer },
197
+ code,
198
+ optimized,
199
+ ),
200
+ ).toBeUndefined();
201
+ const source = await transform(
202
+ plugin,
203
+ { name: "client", config: { consumer: "client" }, depsOptimizer: optimizer },
204
+ code,
205
+ "/proj/apps/web/runtimeConfig.ts",
206
+ );
207
+ expect(source?.code).toBe(inlined);
208
+ });
209
+
210
+ it("watches the file and, when the keys change, rebakes, invalidates the inlined modules and reloads", async () => {
211
+ writeConfig(["MPO_SPEC_FOO"]);
212
+ process.env.MPO_SPEC_FOO = "bar";
213
+ const plugin = publicConfigPlugin({ file, projectRoot: dir });
214
+ const { server, watcher, environments, logger } = fakeServer();
215
+ configureServer(plugin, server);
216
+ expect(watcher.add).toHaveBeenCalledWith(file);
217
+
218
+ const code = "export const keys = process.env.VITE_MP_PUBLIC_CONFIG_KEYS;";
219
+ await transform(plugin, "client", code, "/app/runtimeConfig.ts");
220
+ await transform(plugin, "ssr", code, "/app/runtimeConfig.ts");
221
+ const clientModule = { id: "/app/runtimeConfig.ts" };
222
+ const ssrModule = { id: "/app/runtimeConfig.ts" };
223
+ environments.client.modules.set(clientModule.id, clientModule);
224
+ environments.ssr.modules.set(ssrModule.id, ssrModule);
225
+
226
+ fs.writeFileSync(path.join(dir, ".env"), "MPO_SPEC_BAR=from-dotenv\n");
227
+ writeConfig(["MPO_SPEC_FOO", "MPO_SPEC_BAR"]);
228
+ watcher.emit("change", file);
229
+
230
+ expect(JSON.parse(process.env[bakedKeys]!)).toEqual(["MPO_SPEC_FOO", "MPO_SPEC_BAR"]);
231
+ expect(JSON.parse(process.env[bakedValues]!)).toEqual({
232
+ MPO_SPEC_FOO: "bar",
233
+ MPO_SPEC_BAR: "from-dotenv",
234
+ });
235
+ expect(environments.client.moduleGraph.invalidateModule).toHaveBeenCalledWith(clientModule);
236
+ expect(environments.ssr.moduleGraph.invalidateModule).toHaveBeenCalledWith(ssrModule);
237
+ expect(environments.client.hot.send).toHaveBeenCalledWith({ type: "full-reload", path: "*" });
238
+ expect(environments.ssr.hot.send).toHaveBeenCalledWith({ type: "full-reload", path: "*" });
239
+ expect(logger.info).toHaveBeenCalledTimes(1);
240
+ expect((await transform(plugin, "client", code, "/app/runtimeConfig.ts"))?.code).toBe(
241
+ `export const keys = ${JSON.stringify(JSON.stringify(["MPO_SPEC_FOO", "MPO_SPEC_BAR"]))};\n`,
242
+ );
243
+ });
244
+
245
+ it("stays quiet when an edit leaves the public keys as they were", () => {
246
+ writeConfig(["MPO_SPEC_FOO"]);
247
+ const plugin = publicConfigPlugin({ file, projectRoot: dir });
248
+ const { server, watcher, environments } = fakeServer();
249
+ configureServer(plugin, server);
250
+ writeConfig(["MPO_SPEC_FOO"], { private: ["SECRET", "OTHER_SECRET"] });
251
+ watcher.emit("change", file);
252
+ watcher.emit("change", path.join(dir, "unrelated.json"));
253
+ expect(environments.client.hot.send).not.toHaveBeenCalled();
254
+ expect(environments.ssr.hot.send).not.toHaveBeenCalled();
255
+ });
256
+
257
+ it("keeps the previous keys through a half-written file and picks up the finished one", () => {
258
+ writeConfig(["MPO_SPEC_FOO"]);
259
+ const plugin = publicConfigPlugin({ file, projectRoot: dir });
260
+ const { server, watcher, environments, logger } = fakeServer();
261
+ configureServer(plugin, server);
262
+ fs.writeFileSync(file, '{ "public": ["MPO_SPEC_FOO", ');
263
+ watcher.emit("change", file);
264
+ expect(logger.warn).toHaveBeenCalledTimes(1);
265
+ expect(JSON.parse(process.env[bakedKeys]!)).toEqual(["MPO_SPEC_FOO"]);
266
+ expect(environments.client.hot.send).not.toHaveBeenCalled();
267
+ writeConfig(["MPO_SPEC_FOO", "MPO_SPEC_BAR"]);
268
+ watcher.emit("add", file);
269
+ expect(JSON.parse(process.env[bakedKeys]!)).toEqual(["MPO_SPEC_FOO", "MPO_SPEC_BAR"]);
270
+ expect(environments.client.hot.send).toHaveBeenCalledTimes(1);
271
+ });
272
+ });
@@ -0,0 +1,170 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveConfig } from "@multiplatform.one/utils/dev";
4
+ import dotenv from "dotenv";
5
+ import { transformWithOxc } from "vite";
6
+ import type { Environment, Plugin, ViteDevServer } from "vite";
7
+
8
+ export const bakedConfigEnvKey = "VITE_MP_CONFIG";
9
+ export const publicConfigKeysEnvKey = "VITE_MP_PUBLIC_CONFIG_KEYS";
10
+
11
+ /**
12
+ * The `public` key names of a config.json, read rather than imported: a
13
+ * static import puts the file on Vite's configFileDependencies, and every
14
+ * edit then restarts the dev server, re-optimizes deps and 504s each open tab
15
+ * (MPO-247). A read keeps it off that list.
16
+ */
17
+ export function readPublicConfigKeys(file: string): string[] {
18
+ const parsed: unknown = JSON.parse(fs.readFileSync(file, "utf-8"));
19
+ const keys = parsed && typeof parsed === "object" ? (parsed as { public?: unknown }).public : [];
20
+ if (!Array.isArray(keys)) return [];
21
+ return keys.filter((key): key is string => typeof key === "string" && key.length > 0);
22
+ }
23
+
24
+ export function mergePublicConfigKeys(...lists: string[][]): string[] {
25
+ return [...new Set(lists.flat())];
26
+ }
27
+
28
+ /**
29
+ * Put the bake on process.env: the values resolved from the environment under
30
+ * VITE_MP_CONFIG and the key NAMES under VITE_MP_PUBLIC_CONFIG_KEYS. Names,
31
+ * never a value, is what lets the SSR node process resolve them from its own
32
+ * environment at request time (@multiplatform.one/platform runtimeConfig.ts).
33
+ */
34
+ export function bakePublicConfig(keys: string[]): void {
35
+ process.env[bakedConfigEnvKey] = JSON.stringify(resolveConfig(keys));
36
+ process.env[publicConfigKeysEnvKey] = JSON.stringify(keys);
37
+ }
38
+
39
+ export interface PublicConfigPluginOptions {
40
+ /** Absolute path of the config.json whose `public` array names the keys. */
41
+ file: string;
42
+ /** Directory holding the `.env` the values are resolved from. */
43
+ projectRoot: string;
44
+ /** Keys the config declares inline, kept alongside the file's. */
45
+ keys?: string[];
46
+ }
47
+
48
+ const bakedExpression = /\bprocess\??\.env\??\.(?:VITE_MP_CONFIG|VITE_MP_PUBLIC_CONFIG_KEYS)\b/;
49
+ const nonScriptRequest = /\.(?:json|css|html)(?:$|\?)/;
50
+ const nodeModulesPath = /[\\/]node_modules[\\/]/;
51
+
52
+ /**
53
+ * The same replacement vite:define makes, done through oxc so only real
54
+ * `process.env.<KEY>` reads change: a string, an assignment target or a
55
+ * `globalThis.process.env.<KEY>` is left as written.
56
+ */
57
+ function bakedDefine(): Record<string, string> {
58
+ return Object.fromEntries(
59
+ [bakedConfigEnvKey, publicConfigKeysEnvKey].map((key) => [
60
+ `process.env.${key}`,
61
+ JSON.stringify(process.env[key] ?? ""),
62
+ ]),
63
+ );
64
+ }
65
+
66
+ /**
67
+ * The browser caches node_modules files and optimized deps as immutable under
68
+ * a ?v= hash that no define value feeds, so a value inlined there would
69
+ * outlive every reload and restart. Those keep reading the defines vite
70
+ * injects through /@vite/env instead.
71
+ */
72
+ function browserCached(environment: Environment | undefined, id: string): boolean {
73
+ if (!environment || environment.config.consumer === "server") return false;
74
+ if (nodeModulesPath.test(id)) return true;
75
+ return "depsOptimizer" in environment && !!environment.depsOptimizer?.isOptimizedDepFile(id);
76
+ }
77
+
78
+ /**
79
+ * Dev-server half of `publicConfigFile`. vite:define freezes its replacement
80
+ * table per environment when the server starts, so this plugin inlines the
81
+ * two baked expressions itself (it runs ahead of vite:define), remembers the
82
+ * modules that carried them, and when config.json changes rebakes,
83
+ * invalidates those modules and reloads every environment. The server never
84
+ * restarts, so the dep optimizer never runs again.
85
+ */
86
+ export function publicConfigPlugin(options: PublicConfigPluginOptions): Plugin {
87
+ const file = path.resolve(options.file);
88
+ const envFile = path.resolve(options.projectRoot, ".env");
89
+ const inlineKeys = options.keys ?? [];
90
+ const bakedModules = new Map<string, Set<string>>();
91
+
92
+ function readKeys(): string[] {
93
+ return mergePublicConfigKeys(inlineKeys, readPublicConfigKeys(file));
94
+ }
95
+
96
+ function bake(next: string[]) {
97
+ dotenv.config({ path: envFile, quiet: true });
98
+ bakePublicConfig(next);
99
+ }
100
+
101
+ let keys = readKeys();
102
+ bake(keys);
103
+
104
+ function refresh(server: ViteDevServer) {
105
+ const label = path.relative(process.cwd(), file);
106
+ let next: string[];
107
+ try {
108
+ next = readKeys();
109
+ } catch (err) {
110
+ server.config.logger.warn(
111
+ `${label}: ${(err as Error).message}; keeping the previous public keys`,
112
+ { timestamp: true },
113
+ );
114
+ return;
115
+ }
116
+ if (next.length === keys.length && next.every((key, i) => key === keys[i])) return;
117
+ keys = next;
118
+ bake(keys);
119
+ for (const [name, ids] of bakedModules) {
120
+ const environment = server.environments[name];
121
+ if (!environment) continue;
122
+ for (const id of ids) {
123
+ const mod = environment.moduleGraph.getModuleById(id);
124
+ if (mod) environment.moduleGraph.invalidateModule(mod);
125
+ }
126
+ }
127
+ for (const environment of Object.values(server.environments)) {
128
+ environment.hot.send({ type: "full-reload", path: "*" });
129
+ }
130
+ server.config.logger.info(`${label} changed, reloading with public keys ${keys.join(", ")}`, {
131
+ timestamp: true,
132
+ });
133
+ }
134
+
135
+ return {
136
+ name: "multiplatform-public-config",
137
+ apply: "serve",
138
+ async transform(code, id) {
139
+ if (nonScriptRequest.test(id) || !bakedExpression.test(code)) return;
140
+ if (browserCached(this.environment, id)) return;
141
+ let result: Awaited<ReturnType<typeof transformWithOxc>>;
142
+ try {
143
+ result = await transformWithOxc(code, id, {
144
+ lang: "js",
145
+ sourceType: "module",
146
+ define: bakedDefine(),
147
+ tsconfig: false,
148
+ });
149
+ } catch {
150
+ return;
151
+ }
152
+ const name = this.environment?.name ?? "client";
153
+ let ids = bakedModules.get(name);
154
+ if (!ids) {
155
+ ids = new Set();
156
+ bakedModules.set(name, ids);
157
+ }
158
+ ids.add(id);
159
+ return { code: result.code, map: result.map };
160
+ },
161
+ configureServer(server) {
162
+ server.watcher.add(file);
163
+ const onChange = (changed: string) => {
164
+ if (path.resolve(changed) === file) refresh(server);
165
+ };
166
+ server.watcher.on("change", onChange);
167
+ server.watcher.on("add", onChange);
168
+ },
169
+ };
170
+ }
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import { describe, expect, it } from "vitest";
4
5
  import { createStorybookViteConfig } from "./storybook";
@@ -180,8 +181,59 @@ describe("CONTEXT_SINGLETONS membership, measured rather than recited (MPO-288)"
180
181
  expect(aliases["@tanstack/db"]).toBeTruthy();
181
182
  expect(subscribesExternalStore("@tanstack/store")).toBe(false);
182
183
  });
184
+
185
+ it("leaves react-query off while it subscribes the hook (MPO-290)", () => {
186
+ expect(subscribesExternalStore("@tanstack/react-query")).toBe(true);
187
+ expect(aliases["@tanstack/react-query"]).toBeUndefined();
188
+ });
189
+
190
+ it("resolves react-query to one directory from every package that declares it (MPO-290)", () => {
191
+ const declarers = workspacePackageDirs().filter((dir) =>
192
+ declares(dir, "@tanstack/react-query"),
193
+ );
194
+ expect(declarers.length).toBeGreaterThan(1);
195
+ const records = new Set(
196
+ declarers.map((dir) =>
197
+ fs.realpathSync(
198
+ path.dirname(
199
+ createRequire(path.join(dir, "package.json")).resolve(
200
+ "@tanstack/react-query/package.json",
201
+ ),
202
+ ),
203
+ ),
204
+ ),
205
+ );
206
+ expect([...records]).toHaveLength(1);
207
+ });
208
+
209
+ it("locks react-query to one snapshot, so the isolated layout has one copy too (MPO-290)", () => {
210
+ const lockfile = fs.readFileSync(path.join(workspaceRoot, "pnpm-lock.yaml"), "utf8");
211
+ const snapshots = lockfile.split(/^snapshots:$/m)[1] ?? "";
212
+ const keys = [...snapshots.matchAll(/^ {2}'(@tanstack\/react-query@[^']+)':$/gm)].map(
213
+ (match) => match[1],
214
+ );
215
+ expect(keys).toHaveLength(1);
216
+ });
183
217
  });
184
218
 
219
+ function workspacePackageDirs(): string[] {
220
+ const dirs = [path.join(workspaceRoot, "features")];
221
+ for (const group of ["apps", "packages", "public"]) {
222
+ const groupDir = path.join(workspaceRoot, group);
223
+ for (const entry of fs.readdirSync(groupDir, { withFileTypes: true })) {
224
+ if (entry.isDirectory()) dirs.push(path.join(groupDir, entry.name));
225
+ }
226
+ }
227
+ return dirs.filter((dir) => fs.existsSync(path.join(dir, "package.json")));
228
+ }
229
+
230
+ function declares(dir: string, pkgName: string): boolean {
231
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
232
+ return ["dependencies", "devDependencies", "peerDependencies"].some(
233
+ (field) => manifest[field]?.[pkgName] !== undefined,
234
+ );
235
+ }
236
+
185
237
  /**
186
238
  * Whether an installed package references `useSyncExternalStore` in anything
187
239
  * it ships. The cheap half of the membership rule, re-run against the tree so
@@ -74,9 +74,16 @@ import path from "node:path";
74
74
  * clause and not the whole rule: `@tanstack/db` references the hook nowhere
75
75
  * and is pinned anyway, for the collection registry it holds at module scope.
76
76
  *
77
- * `@tanstack/react-query` references the hook in 25 shipped files and is not
78
- * listed. That is recorded rather than settled — no split has been observed
79
- * there, and pinning it reaches further than this measurement does.
77
+ * MPO-290. `@tanstack/react-query` references the hook in 25 shipped files
78
+ * and is deliberately NOT listed. It passes the grep, but a pin only earns its
79
+ * place where a second record can exist, and here none does. Measured at
80
+ * 5.100.7 on 0af30157c: seven workspace packages declare it and the lockfile
81
+ * resolves all seven to one snapshot, `5.100.7(react@19.2.5)`. The hoisted
82
+ * install holds one physical copy. So does the isolated layout an unset
83
+ * NPM_AUTH_TOKEN produced while .npmrc held the linker keys, rebuilt by
84
+ * stripping them from pnpm-workspace.yaml: fourteen links, one realpath. The
85
+ * spec re-checks both halves, so a second version or peer set fails there
86
+ * before it splits an app.
80
87
  */
81
88
  const CONTEXT_SINGLETONS: readonly string[] = [
82
89
  "react-cookie",