Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
eefd708
fix(doctor): stop --archive truncating the path at the first dot
zkochan Aug 4, 2026
b96ee6d
feat(dependency-resolver): opt-in support for pnpm's global virtual s…
zkochan Aug 4, 2026
749fdb1
test(e2e): make the layout-asserting tests virtual-store agnostic
zkochan Aug 4, 2026
9ba0cee
docs(dependency-resolver): document the global virtual store and friends
zkochan Aug 4, 2026
3e2fc66
fix(e2e): let the tsconfig-issue env declare what it imports
zkochan Aug 5, 2026
8a2065d
feat(dependency-resolver): reach core aspects via NODE_PATH under the…
zkochan Aug 5, 2026
5b622ab
feat(bit): resolve hoisted phantom deps from the global virtual store…
zkochan Aug 5, 2026
071fc89
chore(deps): bump @pnpm/napi to 12.0.0-rc.0
zkochan Aug 5, 2026
eb388c9
feat(dependency-resolver): make the global virtual store safe for sel…
zkochan Aug 5, 2026
83e380c
chore(workspace): keep the repo type-check green under the global vir…
zkochan Aug 5, 2026
52a84c6
chore(deps): bump @pnpm/napi to 12.0.0-rc.1, drop the local-engine sc…
zkochan Aug 7, 2026
85f6c41
fix(api-reference): suppress the SchemaNode version skew with @ts-ignore
zkochan Aug 7, 2026
6f6a6a6
test(e2e): assert the shared links root; TEMP disable eb388c91a logic…
zkochan Aug 7, 2026
1eeeed2
fix(deps): re-enable the guard and extensions; keep the lockfile mini…
zkochan Aug 7, 2026
3c389fc
TEMP: disable the transition-guard call to isolate the capsule MochaT…
zkochan Aug 7, 2026
da0a9ca
Revert "TEMP: disable the transition-guard call to isolate the capsul…
zkochan Aug 7, 2026
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
6 changes: 6 additions & 0 deletions components/legacy/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ export const CFG_CAPSULES_ROOT_BASE_DIR = 'capsules_root_base_dir';

export const CFG_ISOLATED_SCOPE_CAPSULES = 'isolated_scope_capsules';

/**
* Opt in to pnpm's global virtual store for every workspace and capsule on this machine.
* A workspace can still override it through `teambit.dependencies/dependency-resolver`.
*/
export const CFG_ENABLE_GLOBAL_VIRTUAL_STORE = 'enable_global_virtual_store';

/**
* Name of the directory where the capsules for building components are stored
* This is used for the components capsules for bit build / tag / snap / sign
Expand Down
46 changes: 46 additions & 0 deletions components/legacy/e2e-helper/e2e-fs-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import fs from 'fs-extra';
import { use, expect } from 'chai';
import { globSync } from 'glob';
import * as path from 'path';
import * as yaml from 'yaml';
import resolveFrom from 'resolve-from';
import { generateRandomStr } from '@teambit/toolbox.string.random';
import { depPathToDirName } from '@teambit/dependencies.pnpm.dep-path';
import * as fixtures from './fixtures';
import { ensureAndWriteJson } from './e2e-helper';
import type ScopesData from './e2e-scopes';
Expand All @@ -26,6 +29,49 @@ export default class FsHelper {

return globSync(path.normalize(`**/${ext}`), params).map((x) => path.normalize(x));
}
/**
* Walk a chain of package names the way Node resolves them - each name is resolved from the real
* directory of the one before it - and return the last one's directory.
*
* Use this instead of hand-writing a path into `node_modules/.pnpm/<depPath>/node_modules/<dep>`:
* that spelling only exists in the project-local virtual store. Under the global virtual store the
* package lives in a hash-named directory in the shared store, and under a hoisted `nodeLinker` it
* is nested in `node_modules` - resolution finds the right copy in all three.
*/
resolvePackageDir(chain: string[], workspacePath: string = this.scopes.localPath): string {
let dir = workspacePath;
for (const packageName of chain) {
dir = path.dirname(fs.realpathSync(resolveFrom(dir, `${packageName}/package.json`)));
}
return dir;
}

/** The `package.json` of the package at the end of a `resolvePackageDir` chain. */
readPackageJsonOfChain(chain: string[], workspacePath?: string): Record<string, any> {
return fs.readJsonSync(path.join(this.resolvePackageDir(chain, workspacePath), 'package.json'));
}

/**
* The dependency directories the last install materialized, named the way `node_modules/.pnpm`
* names them (`@scope+name@version`).
*
* With the global virtual store enabled those directories live in the shared store instead of
* `node_modules/.pnpm`, and the shared store holds every workspace's packages - so the equivalent
* per-workspace list comes from the current lockfile the install writes next to them.
*/
getVirtualStoreDirNames(workspacePath: string = this.scopes.localPath): string[] {
const virtualStoreDir = path.join(workspacePath, 'node_modules/.pnpm');
if (!fs.existsSync(virtualStoreDir)) return [];
const dirs = fs
.readdirSync(virtualStoreDir)
.filter((dirName) => dirName !== 'lock.yaml' && dirName !== 'node_modules');
if (dirs.length) return dirs;
const currentLockfile = path.join(virtualStoreDir, 'lock.yaml');
if (!fs.existsSync(currentLockfile)) return dirs;
const lockfile = yaml.parse(fs.readFileSync(currentLockfile, 'utf8'));
return Object.keys(lockfile?.packages ?? {}).map((depPath: string) => depPathToDirName(depPath));
}

getObjectFiles() {
return globSync(path.normalize('*/*'), { cwd: path.join(this.scopes.localPath, '.bit/objects') });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @bit-no-check
// @ts-nocheck

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export function SchemaNodesSummary({
headings={_headings}
/>
{groupedMembersByType.map((member) => {
// @ts-expect-error - version skew: local SchemaNode has diff() but npm-published version doesn't yet
// @ts-ignore - version skew: local SchemaNode has diff() but the npm-published one doesn't yet.
// Only errors in the capsule build, where two semantic-schema versions genuinely coexist; the
// workspace type-check resolves a single copy (tsconfig paths), so @ts-expect-error would be unused there.
return renderTable(type ?? '', member, _headings);
})}
</div>
Expand All @@ -146,7 +148,8 @@ export function SchemaNodesSummary({
{groupedMembersByType.map((member) => (
<SchemaMethodMember
key={`${member.__schema}-${member.name}`}
// @ts-expect-error - version skew: local SchemaNode has diff() but npm-published version doesn't yet
// @ts-ignore - version skew: local SchemaNode has diff() but the npm-published one doesn't yet.
// Only errors in the capsule build (two semantic-schema versions); unused in the workspace type-check.
member={member}
apiNodeRendererProps={apiNodeRendererProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,12 +375,17 @@ export function ComponentCompare(props: ComponentCompareProps) {
const skipComponentCompareQuery =
hidden || (base?.id.version?.toString() === compare?.id.version?.toString() && !compareIsLocalChanges);

const { loading: compCompareLoading, componentCompareData } = useComponentCompareQuery(
const { loading: compCompareLoading, componentCompareData: componentCompareDataRaw } = useComponentCompareQuery(
base?.id.toString(),
compare?.id.toString(),
undefined,
skipComponentCompareQuery
);
// version skew: the GraphQL response carries `tests`, the npm-published
// ComponentCompareQueryResponse doesn't declare it yet
const componentCompareData = componentCompareDataRaw as
| (typeof componentCompareDataRaw & { tests?: Array<FileCompareResult & { fileName: string }> })
| undefined;

const { loading: apiDiffLoading, result: apiDiffResult } = useApiDiff(base?.id.toString(), compare?.id.toString(), {
skip: hidden,
Expand Down
14 changes: 12 additions & 2 deletions components/ui/component-meta/component-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ export function ComponentOverview({
}
}, [selectedPkgManager]);

const tabsComponentId: ContentTab[] = [
// version skew: the published Tab type gained `content?: string`, which intersects
// ContentTab's `content: ReactNode` into `ReactNode & string` - impossible to satisfy with an
// element, though elements are the intended usage. Build with the corrected member and cast
// once where the array is handed over.
type ContentTabFixed = Omit<ContentTab, 'content'> & { content: React.ReactNode };
const tabsComponentId: ContentTabFixed[] = [
{
component: function TabPackageName() {
return <span>{packageNameTabTitle}</span>;
Expand Down Expand Up @@ -262,7 +267,12 @@ export function ComponentOverview({
</Row>
<Row>
<div className={styles.contentTabs}>
<ContentTabs priority="folder" tabs={tabsComponentId} navClassName={styles.nav} tabClassName={styles.tab} />
<ContentTabs
priority="folder"
tabs={tabsComponentId as ContentTab[]}
navClassName={styles.nav}
tabClassName={styles.tab}
/>
</div>
<BadgeSection
position={BadgePosition.Package}
Expand Down
16 changes: 4 additions & 12 deletions e2e/harmony/dependency-resolver.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,24 +277,16 @@ describe('dependency-resolver extension', function () {
});
it('should force a newer version of a subdependency using just the dependency name', function () {
// Without the override, is-odd would be 0.1.2
expect(
helper.fixtures.fs.readJsonFile('node_modules/.pnpm/is-odd@1.0.0/node_modules/is-odd/package.json').version
).to.eq('1.0.0');
expect(helper.fs.readPackageJsonOfChain(['is-even', 'is-odd']).version).to.eq('1.0.0');
});
it('should force a newer version of a subdependency using the dependency name and version', function () {
expect(
helper.fixtures.fs.readJsonFile('node_modules/.pnpm/glob@6.0.4/node_modules/glob/package.json').version
).to.eq('6.0.4');
expect(helper.fs.readPackageJsonOfChain(['rimraf', 'glob']).version).to.eq('6.0.4');
});
it('should not change the version of the package if the parent package does not match the pattern', function () {
expect(
helper.fixtures.fs.readJsonFile('node_modules/.pnpm/glob@6.0.4/node_modules/once/package.json').version
).to.eq('1.4.0');
expect(helper.fs.readPackageJsonOfChain(['rimraf', 'glob', 'once']).version).to.eq('1.4.0');
});
it('should change the version of the package if the parent package matches the pattern', function () {
expect(
helper.fixtures.fs.readJsonFile('node_modules/.pnpm/inflight@1.0.6/node_modules/once/package.json').version
).to.eq('1.3.0');
expect(helper.fs.readPackageJsonOfChain(['rimraf', 'glob', 'inflight', 'once']).version).to.eq('1.3.0');
});
});
});
Expand Down
85 changes: 85 additions & 0 deletions e2e/harmony/global-virtual-store.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fs from 'fs-extra';
import { expect } from 'chai';
import path from 'path';
import { Helper } from '@teambit/legacy.e2e-helper';

/**
* `enableGlobalVirtualStore` moves the dependency directories out of the workspace's
* `node_modules/.pnpm` and into a directory shared by every workspace on the machine. Everything a
* workspace does has to keep working from there - the deps still have to resolve, and the envs still
* have to find the core aspects they require without declaring them.
*/
describe('installing with the global virtual store', function () {
let helper: Helper;
this.timeout(0);
before(() => {
helper = new Helper();
});
after(() => {
helper.scopeHelper.destroy();
});
describe('a workspace with a component', () => {
before(() => {
helper.scopeHelper.reInitWorkspace();
helper.extensions.workspaceJsonc.addKeyValToDependencyResolver('enableGlobalVirtualStore', true);
helper.fixtures.populateComponents(1);
helper.command.install('is-positive@1.0.0');
});
it('should not create the dependency directories inside the workspace', () => {
const virtualStoreDir = path.join(helper.scopes.localPath, 'node_modules/.pnpm');
const dirs = fs.readdirSync(virtualStoreDir).filter((dir) => dir !== 'lock.yaml' && dir !== 'node_modules');
expect(dirs).to.deep.eq([]);
});
it('should link the dependency from the global virtual store', () => {
const depPath = path.join(helper.scopes.localPath, 'node_modules/is-positive');
// the shared store lives outside the workspace: pnpm's own `<storeDir>/links` root
const realPath = fs.realpathSync(depPath);
expect(realPath).to.match(/[\\/]links[\\/]/);
expect(realPath).to.not.have.string(helper.scopes.localPath);
});
it('should still record the installed dependencies in the current lockfile', () => {
expect(helper.fs.getVirtualStoreDirNames()).to.include('is-positive@1.0.0');
});
it('should load the component without issues', () => {
const status = helper.command.statusJson();
expect(status.componentsWithIssues).to.have.lengthOf(0);
});
it('should compile the component into its package directory', () => {
helper.command.compile();
expect(
path.join(helper.scopes.localPath, 'node_modules', `@${helper.scopes.remote}/comp1/dist/index.js`)
).to.be.a.path();
});
});
describe('patched dependencies', () => {
before(() => {
helper.scopeHelper.reInitWorkspace();
helper.extensions.workspaceJsonc.addKeyValToDependencyResolver('enableGlobalVirtualStore', true);
helper.command.install('is-positive@1.0.0');
// a patch's context lines are byte-exact, so build it from what was actually installed
const indexPath = path.join(helper.scopes.localPath, 'node_modules/is-positive/index.js');
const context = fs.readFileSync(indexPath, 'utf8').split('\n');
helper.fs.outputFile(
'patches/is-positive.patch',
[
'diff --git a/index.js b/index.js',
'--- a/index.js',
'+++ b/index.js',
`@@ -1,${context.length - 1} +1,${context.length} @@`,
` ${context[0]}`,
'+// patched',
...context.slice(1, -1).map((line) => ` ${line}`),
'',
].join('\n')
);
helper.extensions.workspaceJsonc.addKeyValToDependencyResolver('patchedDependencies', {
'is-positive@1.0.0': 'patches/is-positive.patch',
});
helper.command.install();
});
it('should apply the patch to the copy in the global virtual store', () => {
const indexPath = path.join(helper.scopes.localPath, 'node_modules/is-positive/index.js');
expect(fs.readFileSync(indexPath, 'utf8')).to.include('// patched');
});
});
});
2 changes: 1 addition & 1 deletion e2e/harmony/install.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('install generator configured envs', function () {
helper.scopeHelper.destroy();
});
it('should not install optional dependencies', async () => {
const dirs = fs.readdirSync(path.join(helper.fixtures.scopes.localPath, 'node_modules/.pnpm'));
const dirs = helper.fs.getVirtualStoreDirNames();
expect(dirs).to.not.include('is-positive@1.0.0');
expect(dirs).to.include('@pnpm.e2e+pkg-with-good-optional@1.0.0');
});
Expand Down
4 changes: 3 additions & 1 deletion e2e/harmony/node-linker.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ describe('installing with non-default nodeLinker', function () {
});
it('should create a hoisted node_modules', function () {
const depPath = path.join(helper.fixtures.scopes.localPath, 'node_modules/is-positive');
expect(fs.realpathSync(depPath)).to.contain('.pnpm');
// the isolated linker symlinks into a virtual store: `node_modules/.pnpm` project-locally,
// or `<storeDir>/bit-links/<installation>` when the global virtual store is enabled.
expect(fs.realpathSync(depPath)).to.match(/[\\/](\.pnpm|bit-links)[\\/]/);
});
});
});
Expand Down
Loading