Skip to content

Commit dd3f7e1

Browse files
objectstack-fleet[bot]hotlongclaude
authored
fix(console): an injected @objectstack/client joins the vendor-objectstack chunk group (objectui#10920) (#10921)
Fixes #10920 ## What broke objectstack's `build-console.sh` always sets `OBJECTSTACK_CLIENT_DIST` to the framework's own `packages/client`. That path is outside `node_modules`, so the `vendor-objectstack` group test never matched the injected client. Before #9488 the recursive `data-adapter` group absorbed the client anyway, which hid the gap. Once #9488 made `data-adapter` non-recursive, the client landed in `framework`. `framework` imports `data-adapter`, through `packages/react`'s `error-message.ts`. `data-adapter` then `require`s the CJS client from `framework` at its top level, before `framework` has defined it. On `9f0c84a44` the console built this way stays on the splash screen with `TypeError: u is not a function`. ## The fix `widenVendorChunkTestForClient` widens the group's test with the injected client's package directory. The spec hook already does the same for an injected spec. When nothing is injected, the test is returned unchanged, so production and CI builds do not move. ## Evidence - Reverse verification: the three new cases pass. With `vendorObjectstackTest` wired back to the bare expression, only the config-level case fails (1 red, 21 green). That case is the one that pins the wiring. - `pnpm exec vitest run scripts/__tests__/vite-objectstack-client-dist.test.ts`: 22 passed. - `pnpm type-check:scripts` and `pnpm type-check:vitest-config`: clean. - `node scripts/check-changeset-presence.mjs`: no changeset is owed, because no published source moved. - End to end, with the same change applied to objectstack's build tree at `9f0c84a44`: `chunk-membership.json` reads `client: { "vendor-objectstack": 1 }`, the console boots to `/login`, and a 95-surface sweep of the showcase and Setup apps shows no page errors. --- _Generated by [Claude Code](https://claude.ai/code/session_local_f929252f-7e71-4327-b98b-bd46424b5e05)_ Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1dae95a commit dd3f7e1

3 files changed

Lines changed: 122 additions & 6 deletions

File tree

‎apps/console/vite.config.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import zlib from 'node:zlib';
1616
// `native` becomes the default loader (objectui#3384).
1717
import { viteCryptoStub } from '../../scripts/vite-crypto-stub.ts';
1818
import { viteMaplibreWorker } from '../../scripts/vite-maplibre-worker.ts';
19-
import { resolveClientDistInjection } from '../../scripts/vite-objectstack-client-dist.ts';
19+
import { resolveClientDistInjection, widenVendorChunkTestForClient } from '../../scripts/vite-objectstack-client-dist.ts';
2020
import { formatConditionReport, resolveSpecDistInjection } from '../../scripts/vite-objectstack-spec-dist.ts';
2121
import { viteIneffectiveDynamicImports } from '../../scripts/vite-ineffective-dynamic-imports.ts';
2222
import { viteDeclaredLazyViews } from '../../scripts/vite-declared-lazy-views.ts';
@@ -767,10 +767,14 @@ const optimizeDepsInclude = specDistInjection
767767
// stops matching it and the biggest vendor surface in the bundle (spec is
768768
// imported by 29 packages here) would scatter into its importers' chunks. The
769769
// injected build should differ from a released one in spec CONTENT, not in
770-
// chunk layout, so the override's location joins the group's test.
771-
const vendorObjectstackTest = specDistInjection
772-
? specDistInjection.vendorChunkTest
773-
: VENDOR_OBJECTSTACK_TEST;
770+
// chunk layout, so the override's location joins the group's test. The same
771+
// holds for an injected CLIENT, and there the layout is not cosmetic: left out
772+
// of this group it lands in `framework`, closing a framework ↔ data-adapter
773+
// import cycle that kills the console at boot (see the helper's docblock).
774+
const vendorObjectstackTest = widenVendorChunkTestForClient(
775+
specDistInjection ? specDistInjection.vendorChunkTest : VENDOR_OBJECTSTACK_TEST,
776+
clientDistInjection,
777+
);
774778

775779
// The chunk grouping is not the only consumer of "where does the spec live".
776780
// `assertLazyLinterStaysLazy`'s counter-probe asks the same question about the

‎scripts/__tests__/vite-objectstack-client-dist.test.ts‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import os from 'node:os';
44
import path from 'node:path';
55
import { fileURLToPath } from 'node:url';
66

7-
import { CLIENT_PACKAGE_NAME, resolveClientDistInjection } from '../vite-objectstack-client-dist';
7+
import {
8+
CLIENT_PACKAGE_NAME,
9+
resolveClientDistInjection,
10+
widenVendorChunkTestForClient,
11+
} from '../vite-objectstack-client-dist';
812

913
/**
1014
* objectui#6094 — `OBJECTSTACK_CLIENT_DIST` validates the override it aliases.
@@ -347,3 +351,74 @@ describe('objectui#6094: a BROKEN override is refused, naming the variable', ()
347351
}
348352
});
349353
});
354+
355+
/**
356+
* An injected client stays in `vendor-objectstack`.
357+
*
358+
* objectstack's `build-console.sh` always injects its own client, from a
359+
* workspace path (`<framework>/packages/client`) that the baseline group test —
360+
* written against `node_modules/@objectstack/` and pnpm's `@objectstack+` store
361+
* paths — does not match. Unmatched, the client landed in `framework` while
362+
* `data-adapter` required it at top level, and the console died at boot with
363+
* `TypeError: u is not a function` (measured on `9f0c84a44`). The fixture below
364+
* reproduces that layout: a client package outside any `node_modules`, its
365+
* dependencies reachable from a workspace-root `node_modules`.
366+
*
367+
* Reverse verification: with `vendorObjectstackTest` back on the bare
368+
* `specDistInjection ? … : VENDOR_OBJECTSTACK_TEST` expression, the config case
369+
* below goes red (the injected module id is not claimed) while the helper cases
370+
* stay green — the config case is the one that pins the wiring.
371+
*/
372+
describe('an injected client joins the vendor-objectstack group', () => {
373+
function workspaceClient(): string {
374+
const root = makeFixtureRoot();
375+
const clientDir = path.join(root, 'packages', 'client');
376+
fs.cpSync(fs.realpathSync(installedClientDir), clientDir, { recursive: true, dereference: true });
377+
for (const dep of ['core', 'spec']) {
378+
fs.mkdirSync(path.join(root, 'node_modules', '@objectstack', dep), { recursive: true });
379+
}
380+
return clientDir;
381+
}
382+
383+
const BASELINE = /([\\/]node_modules[\\/]@objectstack[\\/](?!lint[\\/])|[\\/]@objectstack\+(?!lint@))/;
384+
385+
it('returns the baseline test itself when nothing is injected', () => {
386+
expect(widenVendorChunkTestForClient(BASELINE, null)).toBe(BASELINE);
387+
});
388+
389+
it('claims a module under the injected package, and still everything the baseline claimed', () => {
390+
const clientDir = workspaceClient();
391+
const injection = resolveClientDistInjection(clientDir);
392+
expect(injection).not.toBeNull();
393+
const moduleId = path.join(fs.realpathSync(clientDir), 'dist', 'index.js').split(path.sep).join('/');
394+
395+
expect(BASELINE.test(moduleId)).toBe(false);
396+
const widened = widenVendorChunkTestForClient(BASELINE, injection);
397+
expect(widened.test(moduleId)).toBe(true);
398+
expect(widened.test('/r/node_modules/@objectstack/spec/dist/index.mjs')).toBe(true);
399+
expect(widened.test('/r/node_modules/.pnpm/@objectstack+core@1.0.0/x.js')).toBe(true);
400+
// A sibling directory that merely shares the prefix is not claimed.
401+
expect(widened.test(`${fs.realpathSync(clientDir).split(path.sep).join('/')}-other/dist/index.js`)).toBe(false);
402+
});
403+
404+
it('widens the vendor-objectstack group of the real console config', async () => {
405+
const clientDir = workspaceClient();
406+
const moduleId = path.join(fs.realpathSync(clientDir), 'dist', 'index.js').split(path.sep).join('/');
407+
const vendorTest = (config: any): RegExp =>
408+
config.build.rollupOptions.output.advancedChunks.groups.find(
409+
(g: { name: string }) => g.name === 'vendor-objectstack'
410+
).test;
411+
412+
const baseline = await loadConsoleConfig();
413+
process.env.OBJECTSTACK_CLIENT_DIST = clientDir;
414+
let injected: any;
415+
try {
416+
injected = await loadConsoleConfig('?objectstack-client-dist=vendor-chunk');
417+
} finally {
418+
delete process.env.OBJECTSTACK_CLIENT_DIST;
419+
}
420+
421+
expect(vendorTest(baseline).test(moduleId)).toBe(false);
422+
expect(vendorTest(injected).test(moduleId)).toBe(true);
423+
});
424+
});

‎scripts/vite-objectstack-client-dist.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,43 @@ function a(shape: OverrideShape): string {
228228
return shape === 'entry file' ? `an ${shape}` : `a ${shape}`;
229229
}
230230

231+
/**
232+
* The `vendor-objectstack` group test, widened so an injected client stays in
233+
* that group — or `vendorChunkTest` itself, unchanged, when nothing is injected.
234+
*
235+
* An injected client resolves OUTSIDE `node_modules`, so the baseline test —
236+
* written against `node_modules/@objectstack/` and pnpm's `@objectstack+` store
237+
* paths — stops matching it, exactly as it stops matching an injected spec (the
238+
* spec hook widens the same test for the same reason). Left unmatched, the
239+
* client lands wherever its importers pull it. Measured on objectui `9f0c84a44`
240+
* built by objectstack's `build-console.sh` (which always injects its own
241+
* client): `chunk-membership.json` put the client's one module in `framework`,
242+
* while `data-adapter` — no longer recursive since #9488 — imports it, and
243+
* `framework` imports `data-adapter` back (`packages/react`'s
244+
* `error-message.ts` → `isApiAccessDeniedError`). The client ships as CJS, so
245+
* `data-adapter`'s top-level `require` runs before `framework` has defined it
246+
* and the console dies at boot with `TypeError: u is not a function`. Before
247+
* #9488 the recursive `data-adapter` group swallowed the client and hid the gap.
248+
* With the client in `vendor-objectstack` the cycle is gone and the console
249+
* boots; `chunk-membership.json` then reads `client: { 'vendor-objectstack': 1 }`.
250+
*
251+
* Widened, never replaced — the baseline stays whole and the override's
252+
* directory joins it as one more alternative, matched on a path-separator
253+
* boundary so a sibling directory sharing the prefix is not claimed.
254+
*
255+
* @param vendorChunkTest the config's `vendor-objectstack` group test so far
256+
* @param injection the resolved override, or `null` when unset
257+
*/
258+
export function widenVendorChunkTestForClient(
259+
vendorChunkTest: RegExp,
260+
injection: ClientDistInjection | null,
261+
): RegExp {
262+
if (!injection) return vendorChunkTest;
263+
const posixDir = injection.packageDir.split(path.sep).join('/');
264+
const escaped = posixDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
265+
return new RegExp(`${vendorChunkTest.source}|${escaped}[\\\\/]`);
266+
}
267+
231268
/**
232269
* Resolve the override, or `null` when it is unset.
233270
*

0 commit comments

Comments
 (0)