Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions apps/console/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import zlib from 'node:zlib';
// `native` becomes the default loader (objectui#3384).
import { viteCryptoStub } from '../../scripts/vite-crypto-stub.ts';
import { viteMaplibreWorker } from '../../scripts/vite-maplibre-worker.ts';
import { resolveClientDistInjection } from '../../scripts/vite-objectstack-client-dist.ts';
import { resolveClientDistInjection, widenVendorChunkTestForClient } from '../../scripts/vite-objectstack-client-dist.ts';
import { formatConditionReport, resolveSpecDistInjection } from '../../scripts/vite-objectstack-spec-dist.ts';
import { viteIneffectiveDynamicImports } from '../../scripts/vite-ineffective-dynamic-imports.ts';
import { viteDeclaredLazyViews } from '../../scripts/vite-declared-lazy-views.ts';
Expand Down Expand Up @@ -767,10 +767,14 @@ const optimizeDepsInclude = specDistInjection
// stops matching it and the biggest vendor surface in the bundle (spec is
// imported by 29 packages here) would scatter into its importers' chunks. The
// injected build should differ from a released one in spec CONTENT, not in
// chunk layout, so the override's location joins the group's test.
const vendorObjectstackTest = specDistInjection
? specDistInjection.vendorChunkTest
: VENDOR_OBJECTSTACK_TEST;
// chunk layout, so the override's location joins the group's test. The same
// holds for an injected CLIENT, and there the layout is not cosmetic: left out
// of this group it lands in `framework`, closing a framework ↔ data-adapter
// import cycle that kills the console at boot (see the helper's docblock).
const vendorObjectstackTest = widenVendorChunkTestForClient(
specDistInjection ? specDistInjection.vendorChunkTest : VENDOR_OBJECTSTACK_TEST,
clientDistInjection,
);

// The chunk grouping is not the only consumer of "where does the spec live".
// `assertLazyLinterStaysLazy`'s counter-probe asks the same question about the
Expand Down
77 changes: 76 additions & 1 deletion scripts/__tests__/vite-objectstack-client-dist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { CLIENT_PACKAGE_NAME, resolveClientDistInjection } from '../vite-objectstack-client-dist';
import {
CLIENT_PACKAGE_NAME,
resolveClientDistInjection,
widenVendorChunkTestForClient,
} from '../vite-objectstack-client-dist';

/**
* objectui#6094 — `OBJECTSTACK_CLIENT_DIST` validates the override it aliases.
Expand Down Expand Up @@ -347,3 +351,74 @@ describe('objectui#6094: a BROKEN override is refused, naming the variable', ()
}
});
});

/**
* An injected client stays in `vendor-objectstack`.
*
* objectstack's `build-console.sh` always injects its own client, from a
* workspace path (`<framework>/packages/client`) that the baseline group test —
* written against `node_modules/@objectstack/` and pnpm's `@objectstack+` store
* paths — does not match. Unmatched, the client landed in `framework` while
* `data-adapter` required it at top level, and the console died at boot with
* `TypeError: u is not a function` (measured on `9f0c84a44`). The fixture below
* reproduces that layout: a client package outside any `node_modules`, its
* dependencies reachable from a workspace-root `node_modules`.
*
* Reverse verification: with `vendorObjectstackTest` back on the bare
* `specDistInjection ? … : VENDOR_OBJECTSTACK_TEST` expression, the config case
* below goes red (the injected module id is not claimed) while the helper cases
* stay green — the config case is the one that pins the wiring.
*/
describe('an injected client joins the vendor-objectstack group', () => {
function workspaceClient(): string {
const root = makeFixtureRoot();
const clientDir = path.join(root, 'packages', 'client');
fs.cpSync(fs.realpathSync(installedClientDir), clientDir, { recursive: true, dereference: true });
for (const dep of ['core', 'spec']) {
fs.mkdirSync(path.join(root, 'node_modules', '@objectstack', dep), { recursive: true });
}
return clientDir;
}

const BASELINE = /([\\/]node_modules[\\/]@objectstack[\\/](?!lint[\\/])|[\\/]@objectstack\+(?!lint@))/;

it('returns the baseline test itself when nothing is injected', () => {
expect(widenVendorChunkTestForClient(BASELINE, null)).toBe(BASELINE);
});

it('claims a module under the injected package, and still everything the baseline claimed', () => {
const clientDir = workspaceClient();
const injection = resolveClientDistInjection(clientDir);
expect(injection).not.toBeNull();
const moduleId = path.join(fs.realpathSync(clientDir), 'dist', 'index.js').split(path.sep).join('/');

expect(BASELINE.test(moduleId)).toBe(false);
const widened = widenVendorChunkTestForClient(BASELINE, injection);
expect(widened.test(moduleId)).toBe(true);
expect(widened.test('/r/node_modules/@objectstack/spec/dist/index.mjs')).toBe(true);
expect(widened.test('/r/node_modules/.pnpm/@objectstack+core@1.0.0/x.js')).toBe(true);
// A sibling directory that merely shares the prefix is not claimed.
expect(widened.test(`${fs.realpathSync(clientDir).split(path.sep).join('/')}-other/dist/index.js`)).toBe(false);
});

it('widens the vendor-objectstack group of the real console config', async () => {
const clientDir = workspaceClient();
const moduleId = path.join(fs.realpathSync(clientDir), 'dist', 'index.js').split(path.sep).join('/');
const vendorTest = (config: any): RegExp =>
config.build.rollupOptions.output.advancedChunks.groups.find(
(g: { name: string }) => g.name === 'vendor-objectstack'
).test;

const baseline = await loadConsoleConfig();
process.env.OBJECTSTACK_CLIENT_DIST = clientDir;
let injected: any;
try {
injected = await loadConsoleConfig('?objectstack-client-dist=vendor-chunk');
} finally {
delete process.env.OBJECTSTACK_CLIENT_DIST;
}

expect(vendorTest(baseline).test(moduleId)).toBe(false);
expect(vendorTest(injected).test(moduleId)).toBe(true);
});
});
37 changes: 37 additions & 0 deletions scripts/vite-objectstack-client-dist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,43 @@ function a(shape: OverrideShape): string {
return shape === 'entry file' ? `an ${shape}` : `a ${shape}`;
}

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

/**
* Resolve the override, or `null` when it is unset.
*
Expand Down
Loading