diff --git a/.changeset/26131-console-chunk-membership-attribution.md b/.changeset/26131-console-chunk-membership-attribution.md new file mode 100644 index 00000000000..7e9f59be5b5 --- /dev/null +++ b/.changeset/26131-console-chunk-membership-attribution.md @@ -0,0 +1,10 @@ +--- +'@object-ui/console': patch +--- + +fix(console): the build's chunk-membership report attributes only objectui's own packages + +Clause-②: no +Metadata-protocol: none + +The Console build writes `dist/chunk-membership.json`, which the eager-closure gate reads to check that each budgeted chunk holds the packages it declares. Inside the objectstack monorepo the framework's own packages resolve to a second `packages/` directory, and four of their names are objectui package names too (`cli`, `core`, `sdui-parser`, `types`). The report matched any `packages//` segment, so `@objectstack/core`'s bundle file, which belongs in `vendor-objectstack`, was counted as a module of objectui's `packages/core`, and the gate reported `framework` as having lost one module to a vendor chunk. The report now counts only modules under objectui's own `packages/` directory. No chunk's contents change. diff --git a/ui/apps/console/vite.config.ts b/ui/apps/console/vite.config.ts index de0b876d06b..3422ecc4f0f 100644 --- a/ui/apps/console/vite.config.ts +++ b/ui/apps/console/vite.config.ts @@ -19,7 +19,12 @@ import { viteCryptoStub } from '../../scripts/vite-crypto-stub.ts'; import { viteMaplibreWorker } from '../../scripts/vite-maplibre-worker.ts'; import { assertSingleZodInstance } from '../../scripts/vite-single-zod.ts'; import { viteIneffectiveDynamicImports } from '../../scripts/vite-ineffective-dynamic-imports.ts'; -import { frameworkPackagesDir, workspaceChunkTests } from '../../scripts/vite-objectstack-workspace.ts'; +import { + frameworkPackagesDir, + objectuiPackagesDir, + workspaceChunkTests, + workspaceModuleMatcher, +} from '../../scripts/vite-objectstack-workspace.ts'; import { viteDeclaredLazyViews } from '../../scripts/vite-declared-lazy-views.ts'; import { viteTypesZodLazy } from '../../scripts/vite-types-zod-lazy.ts'; import { compression } from 'vite-plugin-compression2'; @@ -400,8 +405,9 @@ function emitEagerClosureReport(reportFileName = 'eager-closure.json'): Plugin { * * ## What it counts, said in words * - * The population is every module in every emitted chunk whose id contains a - * `packages//` segment — workspace source, not `node_modules`, and not + * The population is every module in every emitted chunk whose id lies under + * objectui's own `packages//` — workspace source, not `node_modules`, not + * the objectstack framework's `packages/` beside it in the monorepo, and not * limited to the eager closure: a budgeted package's module hiding in a LAZY * chunk is exactly as much a membership breach as one hiding in an eager * neighbour, and the eager-closure walk cannot see it. @@ -429,10 +435,15 @@ function emitChunkMembershipReport(reportFileName = 'chunk-membership.json'): Pl // The module ids rolldown records are realpaths, so a workspace module reads // as `/packages//src/...` no matter which symlink resolved it — // the same assumption the `advancedChunks` group tests above are written on. + // Anchored on objectui's OWN `packages/` directory: inside the objectstack + // monorepo the framework's packages resolve to a second `packages/` + // directory whose names overlap objectui's (`core`, `types`, ...), and an + // unanchored segment attributed `@objectstack/core` to objectui's + // `packages/core`. `workspaceModuleMatcher` says why in full. // The second capture is the rest of the id — the package-relative path the // `directories` table is keyed by (its directory, `/`-separated, any query // suffix dropped: `?inline` and friends are the same file). - const WORKSPACE_MODULE = /[\\/]packages[\\/]([^\\/]+)[\\/](.*)$/; + const WORKSPACE_MODULE = workspaceModuleMatcher(objectuiPackagesDir(import.meta.dirname)); const directoryOf = (rest: string) => { const file = rest.split('?')[0].replace(/\\/g, '/'); const slash = file.lastIndexOf('/'); diff --git a/ui/scripts/__tests__/vite-objectstack-workspace.test.ts b/ui/scripts/__tests__/vite-objectstack-workspace.test.ts index 198edec1f6a..5ce3cbe0cc5 100644 --- a/ui/scripts/__tests__/vite-objectstack-workspace.test.ts +++ b/ui/scripts/__tests__/vite-objectstack-workspace.test.ts @@ -26,7 +26,9 @@ import { candidateFrameworkPackagesDir, escapeRegExp, frameworkPackagesDir, + objectuiPackagesDir, workspaceChunkTests, + workspaceModuleMatcher, } from '../vite-objectstack-workspace.ts'; import { isNestedWorkspace } from '../workspace-root.mjs'; @@ -171,3 +173,39 @@ describe('workspaceChunkTests (objectstack ruling D1)', () => { expect(config).toContain('const LINT = WORKSPACE_CHUNK_TESTS.lint;'); }); }); + +describe('workspaceModuleMatcher (the chunk-membership attribution)', () => { + const matcher = workspaceModuleMatcher(objectuiPackagesDir(path.join(MONOREPO, 'ui/apps/console'))); + + it("locates objectui's own packages dir two levels above the console", () => { + expect(objectuiPackagesDir(path.join(MONOREPO, 'ui/apps/console'))).toBe(path.join(MONOREPO, 'ui/packages')); + expect(objectuiPackagesDir(consoleDir)).toBe(path.join(repoRoot, 'packages')); + }); + + it('attributes an objectui module to its package, with the package-relative rest', () => { + const hit = matcher.exec(`${MONOREPO}/ui/packages/core/src/actions/ActionRunner.ts`); + expect(hit?.[1]).toBe('core'); + expect(hit?.[2]).toBe('src/actions/ActionRunner.ts'); + expect(matcher.exec(`${MONOREPO}/ui/packages/types/src/zod/index.ts?inline`)?.[1]).toBe('types'); + }); + + it('does not attribute a framework package that shares an objectui package name', () => { + // `@objectstack/core` resolves here in the monorepo; objectui has a `packages/core` too. + for (const name of ['core', 'types', 'cli', 'sdui-parser']) { + expect(matcher.test(`${MONOREPO}/packages/${name}/dist/index.js`)).toBe(false); + } + expect(matcher.test(`/elsewhere${MONOREPO}/ui/packages/core/src/index.ts`)).toBe(false); + expect(matcher.test(`${MONOREPO}/ui/packages-extra/core/src/index.ts`)).toBe(false); + }); + + it('escapes a directory with regex metacharacters so the prefix is matched literally', () => { + const odd = workspaceModuleMatcher('/w/ui (v1.0)/packages'); + expect(odd.exec('/w/ui (v1.0)/packages/core/src/index.ts')?.[1]).toBe('core'); + expect(odd.test('/w/uiX(v1A0)/packages/core/src/index.ts')).toBe(false); + }); + + it('is what the console config attributes membership with', () => { + const config = readFileSync(path.join(consoleDir, 'vite.config.ts'), 'utf8'); + expect(config).toContain('const WORKSPACE_MODULE = workspaceModuleMatcher(objectuiPackagesDir(import.meta.dirname));'); + }); +}); diff --git a/ui/scripts/vite-objectstack-workspace.ts b/ui/scripts/vite-objectstack-workspace.ts index f2b53fe34af..5d0855074ef 100644 --- a/ui/scripts/vite-objectstack-workspace.ts +++ b/ui/scripts/vite-objectstack-workspace.ts @@ -94,3 +94,33 @@ export function workspaceChunkTests(frameworkPackagesDir: string | null): Worksp lint: new RegExp('(@objectstack[\\\\/+]lint|^' + prefix + 'lint[\\\\/])'), }; } + +/** + * objectui's own `packages/` directory, seen from a console directory + * (`apps/console` → `../../packages`) — the same in both layouts. Pure. + */ +export function objectuiPackagesDir(consoleDir: string): string { + return path.resolve(consoleDir, '../../packages'); +} + +/** + * The matcher `emitChunkMembershipReport` in `apps/console/vite.config.ts` + * attributes a module id with: capture 1 is the objectui package name, capture + * 2 the package-relative rest of the id. + * + * It is anchored on objectui's OWN `packages/` directory, and the monorepo is + * why. Inside it the framework's packages are workspace links that Vite + * resolves to `/packages//...` (see the top of this file), + * and four of those names are objectui package names too: `cli`, `core`, + * `sdui-parser` and `types`. An unanchored `/packages//` segment reads + * `@objectstack/core`'s `dist/index.js` — reached through `@objectstack/client` + * and rightly grouped into `vendor-objectstack` — as a module of objectui's + * `packages/core`, and the membership verdict then reports `framework`'s + * declared package as having strayed into a vendor chunk. That verdict was + * about attribution, not about where any objectui module landed. Standalone + * the anchor changes nothing: no other `packages/` directory is in the graph. + */ +export function workspaceModuleMatcher(packagesDir: string): RegExp { + const prefix = escapeRegExp(packagesDir.split(path.sep).join('/')); + return new RegExp('^' + prefix + '[\\\\/]([^\\\\/]+)[\\\\/](.*)$'); +}