Skip to content
Draft
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
9 changes: 9 additions & 0 deletions components/legacy/bit-map/bit-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ describe('BitMap', function () {
it('should remove the "id" property', () => {
expect(componentMap).to.not.have.property('id');
});
it('should persist files when an external inventory owns the file mapping', () => {
const explicitBitMap = new BitMap(__dirname, '', '17.0.0');
const explicitComponent = explicitBitMap.addComponent(addComponentParamsFixture);
explicitComponent.useExplicitFiles = true;

const serialized = explicitBitMap.toObjects()['is-string'];
expect(serialized.useExplicitFiles).to.equal(true);
expect(serialized.files).to.deep.equal(addComponentParamsFixture.files);
});
it('should sort the components alphabetically', async () => {
const exampleComponent = { ...addComponentParamsFixture };
exampleComponent.defaultScope = '';
Expand Down
1 change: 1 addition & 0 deletions components/legacy/bit-map/bit-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export class BitMap {
const gitIgnore = await getGitIgnoreHarmony(this.projectRoot, this.ignoredFiles);
await Promise.all(
this.components.map(async (componentMap) => {
if (componentMap.useExplicitFiles) return;
const rootDir = componentMap.rootDir;
if (!rootDir) return;
try {
Expand Down
14 changes: 13 additions & 1 deletion components/legacy/bit-map/component-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type ComponentMapData = {
isAvailableOnCurrentLane?: boolean;
nextVersion?: NextVersion;
config?: Config;
useExplicitFiles?: boolean;
};

export type PathChange = { from: PathLinux; to: PathLinux };
Expand Down Expand Up @@ -91,6 +92,13 @@ export class ComponentMap {
scope?: string | null; // empty string if new/staged. (undefined if legacy).
version?: string; // empty string if new. (undefined if legacy).
noFilesError?: Error; // set if during finding the files an error was found
/**
* Keep the files in .bitmap instead of deriving them from rootDir.
*
* This is needed when multiple components intentionally share a directory
* tree and an external inventory owns the file-to-component mapping.
*/
useExplicitFiles?: boolean;
config?: { [aspectId: string]: Record<string, any> | '-' };
constructor({
id,
Expand All @@ -104,6 +112,7 @@ export class ComponentMap {
isAvailableOnCurrentLane,
nextVersion,
config,
useExplicitFiles,
}: ComponentMapData) {
this.id = id;
this.files = files;
Expand All @@ -116,6 +125,7 @@ export class ComponentMap {
this.isAvailableOnCurrentLane = typeof isAvailableOnCurrentLane === 'undefined' ? true : isAvailableOnCurrentLane;
this.nextVersion = nextVersion;
this.config = config;
this.useExplicitFiles = useExplicitFiles;
}

static fromJson(componentMapObj: ComponentMapData): ComponentMap {
Expand All @@ -127,7 +137,7 @@ export class ComponentMap {
name: this.name,
scope: this.scope,
version: this.version,
files: null,
files: this.useExplicitFiles ? this.files : null,
defaultScope: this.defaultScope,
mainFile: this.mainFile,
rootDir: this.rootDir,
Expand All @@ -138,6 +148,7 @@ export class ComponentMap {
nextVersion: this.nextVersion,
localOnly: this.localOnly || null, // if false, change to null so it won't be written
config: this.configToObject(),
useExplicitFiles: this.useExplicitFiles || null,
};

res = pickBy(res, (value) => !isNil(value));
Expand Down Expand Up @@ -266,6 +277,7 @@ export class ComponentMap {
* updated list of the files
*/
async trackDirectoryChangesHarmony(consumerPath: PathOsBasedAbsolute, ignoredFiles?: string[]): Promise<void> {
if (this.useExplicitFiles) return;
const trackDir = this.rootDir;
if (!trackDir) {
return;
Expand Down
8 changes: 6 additions & 2 deletions components/legacy/consumer-component/consumer-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,12 @@ export class Component {
if (!inScopeWithAnyVersion) throw new ComponentsPendingImport([id.toString()]);
}
const deprecated = componentFromModel ? componentFromModel.deprecated : false;
const compDirAbs = path.join(consumer.getPath(), componentMap.getComponentDir());
if (!fs.existsSync(compDirAbs)) throw new ComponentNotFoundInPath(componentMap.getComponentDir());
const componentDir = componentMap.getComponentDir();
if (!componentDir && !componentMap.useExplicitFiles) {
throw new ComponentNotFoundInPath(componentDir);
}
const compDirAbs = componentDir ? path.join(consumer.getPath(), componentDir) : consumer.getPath();
if (!fs.existsSync(compDirAbs)) throw new ComponentNotFoundInPath(componentDir);

// Load the base entry from the root dir in map file in case it was imported using -path
// Or created using bit create so we don't want all the path but only the relative one
Expand Down
139 changes: 123 additions & 16 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect } from 'chai';
import { getPnpmVcsRootTrackerConfig } from './component-writer.main.runtime';

describe('pnpm VCS root component bootstrap', () => {
it('accepts only the component named by a normalized root topology', () => {
const config = {
pnpmVcs: {
schemaVersion: 1,
requirements: {},
appliedProfile: {},
workspace: {
schemaVersion: 1,
rootComponent: 'acme.workspace/root',
components: [],
},
},
};
const component = {
id: {
toStringWithoutVersion: () => 'acme.workspace/root',
toString: () => 'acme.workspace/root@abc123',
},
extensions: {
findCoreExtension: () => ({ config }),
},
} as any;

expect(getPnpmVcsRootTrackerConfig(component)).to.equal(config);
config.pnpmVcs.workspace.rootComponent = 'acme.workspace/other';
expect(getPnpmVcsRootTrackerConfig(component)).to.be.undefined;
});
});
48 changes: 42 additions & 6 deletions scopes/component/component-writer/component-writer.main.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ManyComponentsWriterParams {
throwForExistingDir?: boolean;
// when the target dir is occupied, import into an available empty dir (e.g. "foo" => "foo_1") instead of failing.
writeToEmptyDir?: boolean;
pnpmVcsRoot?: boolean;
writeConfig?: boolean;
skipDependencyInstallation?: boolean;
verbose?: boolean;
Expand Down Expand Up @@ -107,7 +108,7 @@ export class ComponentWriterMain {
opts.mergeStrategy
);
}
if (this.workspace.externalPackageManagerIsUsed()) {
if (this.workspace.externalPackageManagerIsUsed() && !this.isPnpmVcsWorkspace()) {
await this.installer.writeDependenciesToPackageJson();
} else if (!opts.skipDependencyInstallation) {
installationError = await this.installPackagesGracefully(
Expand All @@ -120,6 +121,14 @@ export class ComponentWriterMain {
return { installationError, compilationError, workspaceConfigUpdateResult };
}

private isPnpmVcsWorkspace(): boolean {
const rootMap = this.consumer.bitMap.components.find(
(component) => !component.rootDir && component.useExplicitFiles
);
const trackerConfig = rootMap?.config?.['teambit.component/tracker'];
return Boolean(trackerConfig && trackerConfig !== '-' && trackerConfig.pnpmVcs?.schemaVersion === 1);
}

private async installPackagesGracefully(
componentIds: ComponentID[],
skipWriteConfigFiles = false
Expand Down Expand Up @@ -160,6 +169,9 @@ export class ComponentWriterMain {
await dataToPersist.persistAllToFS();
}
private async populateComponentsFilesToWrite(opts: ManyComponentsWriterParams) {
if (opts.pnpmVcsRoot && opts.components.length !== 1) {
throw new BitError('a pnpm VCS root bootstrap must write exactly one component');
}
const writeComponentsParams = opts.components.map((component) =>
this.getWriteParamsOfOneComponent(component, opts)
);
Expand All @@ -177,7 +189,7 @@ export class ComponentWriterMain {
(await componentWriter.addComponentToBitMap(componentWriter.writeToPath));
const componentConfigPath = path.join(
this.workspace.path,
componentWriter.existingComponentMap.rootDir,
componentWriter.existingComponentMap.rootDir || '',
COMPONENT_CONFIG_FILE_NAME
);
const componentConfigExist = await fs.pathExists(componentConfigPath);
Expand Down Expand Up @@ -272,14 +284,20 @@ export class ComponentWriterMain {
component: ConsumerComponent,
opts: ManyComponentsWriterParams
): ComponentWriterProps {
const componentRootDir: PathLinuxRelative = opts.writeToPath
? pathNormalizeToLinux(this.consumer.getPathRelativeToConsumer(path.resolve(opts.writeToPath)))
: this.consumer.composeRelativeComponentPath(component.id);
const pnpmVcsRootConfig = opts.pnpmVcsRoot ? getPnpmVcsRootTrackerConfig(component) : undefined;
if (opts.pnpmVcsRoot && !pnpmVcsRootConfig) {
throw new BitError(`component ${component.id} is not a pnpm VCS workspace root`);
}
const componentRootDir: PathLinuxRelative = opts.pnpmVcsRoot
? ''
: opts.writeToPath
? pathNormalizeToLinux(this.consumer.getPathRelativeToConsumer(path.resolve(opts.writeToPath)))
: this.consumer.composeRelativeComponentPath(component.id);
// components can't be saved with multiple versions, so we can ignore the version to find the component in bit.map
const existingComponentMap = this.consumer?.bitMap.getComponentIfExist(component.id, { ignoreVersion: true });
// with --write-to-empty-dir, dir-conflict resolution is deferred to relocateOccupiedDirs() so it runs after the
// fixDirs* passes (which may still adjust writeToPath); otherwise fail here when the target dir is occupied.
if (this.consumer && !opts.writeToEmptyDir) {
if (this.consumer && !opts.writeToEmptyDir && !opts.pnpmVcsRoot) {
this.throwErrorWhenDirectoryNotEmpty(componentRootDir, existingComponentMap, opts);
}
return {
Expand All @@ -290,9 +308,12 @@ export class ComponentWriterMain {
writeConfig: opts.writeConfig,
skipUpdatingBitMap: opts.skipUpdatingBitMap,
existingComponentMap: existingComponentMap ?? undefined,
useExplicitFiles: Boolean(opts.pnpmVcsRoot),
componentConfig: pnpmVcsRootConfig ? { 'teambit.component/tracker': pnpmVcsRootConfig } : undefined,
};
}
private moveComponentsIfNeeded(opts: ManyComponentsWriterParams) {
if (opts.pnpmVcsRoot) return;
if (opts.writeToPath && this.consumer) {
opts.components.forEach((component) => {
const componentMap = component.componentMap as ComponentMap;
Expand Down Expand Up @@ -434,6 +455,21 @@ either use --path to specify a different directory or modify "defaultDirectory"
}
}

export function getPnpmVcsRootTrackerConfig(component: ConsumerComponent): Record<string, any> | undefined {
const trackerEntry = component.extensions.findCoreExtension('teambit.component/tracker');
const pnpmVcs = trackerEntry?.config?.pnpmVcs;
const workspace = pnpmVcs?.workspace;
if (
pnpmVcs?.schemaVersion !== 1 ||
workspace?.schemaVersion !== 1 ||
workspace.rootComponent !== component.id.toStringWithoutVersion() ||
!Array.isArray(workspace.components)
) {
return undefined;
}
return trackerEntry?.config;
}

ComponentWriterAspect.addRuntime(ComponentWriterMain);

export default ComponentWriterMain;
Expand Down
21 changes: 19 additions & 2 deletions scopes/component/component-writer/component-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export type ComponentWriterProps = {
deleteBitDirContent?: boolean;
existingComponentMap?: ComponentMap;
skipUpdatingBitMap?: boolean;
useExplicitFiles?: boolean;
componentConfig?: { [aspectId: string]: Record<string, any> | '-' };
};

export default class ComponentWriter {
Expand All @@ -42,6 +44,8 @@ export default class ComponentWriter {
deleteBitDirContent: boolean | undefined;
existingComponentMap: ComponentMap | undefined;
skipUpdatingBitMap?: boolean;
useExplicitFiles: boolean;
componentConfig?: { [aspectId: string]: Record<string, any> | '-' };

constructor({
component,
Expand All @@ -57,6 +61,8 @@ export default class ComponentWriter {
deleteBitDirContent,
existingComponentMap,
skipUpdatingBitMap,
useExplicitFiles = false,
componentConfig,
}: ComponentWriterProps) {
this.component = component;
this.writeToPath = writeToPath;
Expand All @@ -72,6 +78,8 @@ export default class ComponentWriter {
this.deleteBitDirContent = deleteBitDirContent;
this.existingComponentMap = existingComponentMap;
this.skipUpdatingBitMap = skipUpdatingBitMap;
this.useExplicitFiles = useExplicitFiles;
this.componentConfig = componentConfig;
}

async populateComponentsFilesToWrite(): Promise<Component> {
Expand Down Expand Up @@ -132,13 +140,22 @@ export default class ComponentWriter {
? undefined
: await this.workspace.componentDefaultScopeFromComponentDirAndName(rootDir, bitId.fullName);

return this.bitMap.addComponent({
const componentMap = this.bitMap.addComponent({
componentId: compId,
files: filesForBitMap,
defaultScope,
mainFile: pathNormalizeToLinux(this.component.mainFile),
rootDir,
rootDir: rootDir || undefined,
config: this.componentConfig,
});
// Harmony normally ignores package.json because Bit generates it. A model
// component that explicitly contains package.json (such as a pnpm VCS
// component) owns that manifest as source, so directory scanning must not
// silently drop it after import.
if (this.useExplicitFiles || filesForBitMap.some((file) => file.relativePath === 'package.json')) {
componentMap.useExplicitFiles = true;
}
return componentMap;
}

private async replaceSnapWithTagIfNeeded(): Promise<ComponentID> {
Expand Down
Loading
Loading