Skip to content

Commit 04ca6ea

Browse files
authored
fix(dep-resolver), validate deps-resolver aspect data (specifically "version" field) before saving it (#9648)
Currently, the "env.jsonc" can have any string in the "version" field, and it'll be saved into the objects incorrectly. This PR makes the following validations: 1. "version" field needs to be a semver/semver-range valid. 2. for components, it can be a hash, in case the version is a snap. 3. the `*` range is valid only for packages, not for components. The reason is that for components, in case it was snapped on a lane, the package on bit cloud doesn't have "latest" tag, as a result, the installer fails.
1 parent 998b190 commit 04ca6ea

6 files changed

Lines changed: 92 additions & 26 deletions

File tree

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ module.exports = {
107107
ignoreTemplateLiterals: true,
108108
},
109109
],
110-
'max-lines': [2, 1800],
110+
'max-lines': [2, 2000],
111111
'func-names': [0],
112112

113113
// ERRORS OF plugin:react/recommended

components/legacy/e2e-helper/npm-ci-registry.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { addUser, REGISTRY_MOCK_PORT, start as startRegistryMock, prepare } from '@pnpm/registry-mock';
33
import { ChildProcess } from 'child_process';
44
import { fetch } from '@pnpm/fetch';
5+
import fs from 'fs-extra';
56
import execa from 'execa';
67
import * as path from 'path';
78

@@ -148,6 +149,22 @@ export class NpmCiRegistry {
148149
this.helper.command.runCmd('npm publish', extractedDir);
149150
}
150151

152+
/**
153+
* publish an empty package to the registry.
154+
* it's helpful in case a package is needed. not a component.
155+
* instead of going to the NPM, this method is faster.
156+
*/
157+
publishPackage(packageName: string, version = '0.0.1') {
158+
const dir = this.helper.fs.createNewDirectory();
159+
this.helper.command.runCmd(`npm init -y`, dir);
160+
// change package.json according to the packageName and version
161+
const packageJson = fs.readJsonSync(path.join(dir, 'package.json'));
162+
packageJson.name = packageName;
163+
packageJson.version = version;
164+
fs.writeJsonSync(path.join(dir, 'package.json'), packageJson);
165+
this.helper.command.runCmd('npm publish', dir);
166+
}
167+
151168
configureCiInPackageJsonHarmony() {
152169
const pkg = {
153170
packageJson: {

e2e/functionalities/peer-dependencies.e2e.3.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ describe('peer-dependencies functionality', function () {
263263
helper = new Helper();
264264
helper.scopeHelper.reInitWorkspace();
265265
helper.fixtures.populateComponents(2);
266-
helper.command.dependenciesSet('comp1', `@${helper.scopes.remote}/comp2@*`, '--peer');
266+
helper.command.dependenciesSet('comp1', `@${helper.scopes.remote}/comp2@+`, '--peer');
267267
helper.command.snapAllComponents();
268268
helper.command.build();
269269
workspaceCapsulesRootDir = helper.command.capsuleListParsed().workspaceCapsulesRootDir;
@@ -273,28 +273,28 @@ describe('peer-dependencies functionality', function () {
273273
const peerDepData = output.peerDependencies[0];
274274
expect(peerDepData.id).to.startWith(`${helper.scopes.remote}/comp2`);
275275
expect(peerDepData.packageName).to.startWith(`@${helper.scopes.remote}/comp2`);
276-
expect(peerDepData.versionRange).to.startWith('*');
276+
expect(peerDepData.versionRange).to.startWith('+');
277277
const depResolver = output.extensions.find(({ name }) => name === 'teambit.dependencies/dependency-resolver');
278278
const peerDep = depResolver.data.dependencies[0];
279279
expect(peerDep.packageName).to.eq(`@${helper.scopes.remote}/comp2`);
280280
expect(peerDep.lifecycle).to.eq('peer');
281-
expect(peerDep.versionRange).to.eq('*');
281+
expect(peerDep.versionRange).to.eq('+');
282282
});
283283
it('should save the peer dependency in the scope data', () => {
284284
const comp = helper.command.catComponent(`comp1@latest`);
285285
const depResolver = comp.extensions.find(({ name }) => name === 'teambit.dependencies/dependency-resolver');
286286
const peerDep = depResolver.data.dependencies[0];
287287
expect(peerDep.packageName).to.eq(`@${helper.scopes.remote}/comp2`);
288288
expect(peerDep.lifecycle).to.eq('peer');
289-
expect(peerDep.versionRange).to.eq('*');
289+
expect(peerDep.versionRange).to.eq('+');
290290
});
291291
it('adds peer dependency to the generated package.json', () => {
292292
const { head } = helper.command.catComponent('comp1');
293293
const pkgJson = fs.readJsonSync(
294294
path.join(workspaceCapsulesRootDir, `${helper.scopes.remote}_comp1@${head}/package.json`)
295295
);
296296
expect(pkgJson.peerDependencies).to.deep.equal({
297-
[`@${helper.scopes.remote}/comp2`]: '*',
297+
[`@${helper.scopes.remote}/comp2`]: '+',
298298
});
299299
});
300300
});

e2e/harmony/dependency-resolver.e2e.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -390,60 +390,65 @@ describe('dependency-resolver extension', function () {
390390
});
391391
(supportNpmCiRegistryTesting ? describe : describe.skip)('env.jsonc with policy.peer version="*"', () => {
392392
let npmCiRegistry: NpmCiRegistry;
393+
const examplePkg = '@ci/lodash';
393394
before(async () => {
394395
helper = new Helper({ scopesOptions: { remoteScopeWithDot: true } });
395396
helper.scopeHelper.setWorkspaceWithRemoteScope();
396397
npmCiRegistry = new NpmCiRegistry(helper);
397398
await npmCiRegistry.init();
398399
npmCiRegistry.configureCiInPackageJsonHarmony();
400+
npmCiRegistry.publishPackage(examplePkg, '0.0.1');
399401

400-
helper.fixtures.populateComponents(1);
401-
helper.command.tagAllComponents();
402402
helper.env.setEmptyEnv();
403403
helper.fs.outputFile('empty-env/env.jsonc', `{
404404
"policy": {
405405
"peers": [
406406
{
407-
"name": "${helper.general.getPackageNameByCompName('comp1')}",
407+
"name": "${examplePkg}",
408408
"version": "*",
409409
"supportedRange": "*"
410410
}
411411
]
412412
}
413413
}
414414
`);
415-
helper.command.tagAllComponents(); // it'll tag only empty-env.
415+
helper.command.tagAllComponents();
416416
helper.command.export();
417417
});
418418
after(() => {
419419
npmCiRegistry.destroy();
420420
});
421-
function validateDepData(expectedVersion: string) {
421+
function validatePkgData() {
422422
const comp = helper.command.catComponent(`${helper.scopes.remote}/empty-env@latest`);
423423
const depResolverExt = comp.extensions.find((e) => e.name === Extensions.dependencyResolver);
424-
const policy = depResolverExt.data.policy.find(p => p.dependencyId === helper.general.getPackageNameByCompName('comp1'));
424+
const policy = depResolverExt.data.policy.find(p => p.dependencyId === examplePkg);
425425
expect(policy.value.version).to.equal('*');
426-
const data = depResolverExt.data.dependencies.find(p => p.packageName === helper.general.getPackageNameByCompName('comp1'));
427-
expect(data.version).to.equal(expectedVersion);
428-
expect(data.componentId.version).to.equal(expectedVersion);
426+
const data = depResolverExt.data.dependencies.find(p => p.id === examplePkg);
427+
expect(data.version).to.equal('*');
429428
}
430429
it('should not break and save the policy correctly with the *', () => {
431-
validateDepData('0.0.1');
430+
validatePkgData();
432431
});
433-
describe('making a new version of the env dep', () => {
432+
describe('publishing a new version of the package and re-tagging', () => {
434433
before(() => {
434+
npmCiRegistry.publishPackage(examplePkg, '0.0.2');
435435
helper.command.tagAllComponents('--unmodified');
436436
helper.command.export();
437437
});
438438
it('should update the dep in the env model', () => {
439-
validateDepData('0.0.2');
439+
validatePkgData();
440440
});
441-
it('should be able to install the env on a new workspace with no errors', () => {
441+
it('should be able to install the env on a new workspace with no errors and install the latest of the pkg dep', () => {
442442
helper.scopeHelper.reInitWorkspace();
443443
helper.scopeHelper.addRemoteScope();
444444
helper.command.install(helper.general.getPackageNameByCompName('empty-env'));
445-
const pkgJson = helper.fs.readJsonFile(`node_modules/${helper.general.getPackageNameByCompName('empty-env')}/package.json`);
446-
expect(pkgJson.dependencies[`${helper.general.getPackageNameByCompName('comp1')}`]).to.equal('0.0.2');
445+
446+
const envPkgJson = helper.fs.readJsonFile(`node_modules/${helper.general.getPackageNameByCompName('empty-env')}/package.json`);
447+
expect(envPkgJson.dependencies[examplePkg]).to.equal('*');
448+
449+
const pkgJsonPath = path.join('node_modules', '.pnpm/@ci+lodash@0.0.2/node_modules/@ci/lodash/package.json');
450+
const pkgJson = helper.fs.readJsonFile(pkgJsonPath);
451+
expect(pkgJson.version).to.equal('0.0.2');
447452
});
448453
});
449454
});

scopes/dependencies/dependency-resolver/dependency-resolver.main.runtime.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import multimatch from 'multimatch';
2+
import { isSnap } from '@teambit/component-version';
23
import mapSeries from 'p-map-series';
34
import { DEPS_GRAPH, isFeatureEnabled } from '@teambit/harmony.modules.feature-toggle';
45
import { MainRuntime } from '@teambit/cli';
@@ -101,7 +102,7 @@ export { ProxyConfig, NetworkConfig } from '@teambit/scope.network';
101102
export interface DependencyResolverComponentData {
102103
packageName: string;
103104
policy: SerializedVariantPolicy;
104-
dependencies: SerializedDependency;
105+
dependencies: SerializedDependency[];
105106
}
106107

107108
export interface DependencyResolverVariantConfig {
@@ -1376,6 +1377,40 @@ export class DependencyResolverMain {
13761377
return manifest;
13771378
}
13781379

1380+
validateAspectData(data: DependencyResolverComponentData) {
1381+
const errorPrefix = `failed validating ${DependencyResolverAspect.id} aspect-data.`;
1382+
let errorMsg: undefined | string;
1383+
data.dependencies?.forEach((dep) => {
1384+
const isVersionValid = Boolean(semver.valid(dep.version) || semver.validRange(dep.version));
1385+
if (isVersionValid) return;
1386+
if (dep.__type === COMPONENT_DEP_TYPE && isSnap(dep.version)) return;
1387+
errorMsg = `${errorPrefix} the dependency version "${dep.version}" of ${dep.id} is not a valid semver version or range`;
1388+
});
1389+
data.policy?.forEach((policy) => {
1390+
const policyVersion = policy.value.version;
1391+
const allowedSpecialChars = ['+', '-'];
1392+
if (policyVersion === '*') {
1393+
// this is only valid for packages, not for components.
1394+
const isComp = data.dependencies.find(d => d.__type === COMPONENT_DEP_TYPE
1395+
&& d.packageName === policy.dependencyId);
1396+
if (!isComp) return;
1397+
errorMsg = `${errorPrefix} the policy version "${policyVersion}" of ${policy.dependencyId} is not valid for components, only for packages.
1398+
as an alternative, you can use "+" to keep the same version installed in the workspace`;
1399+
}
1400+
const isVersionValid = Boolean(
1401+
semver.valid(policyVersion) ||
1402+
semver.validRange(policyVersion) ||
1403+
allowedSpecialChars.includes(policyVersion)
1404+
);
1405+
if (isVersionValid) return;
1406+
errorMsg = `${errorPrefix} the policy version "${policyVersion}" of ${policy.dependencyId} is not a valid semver version or range`;
1407+
});
1408+
1409+
if (errorMsg) {
1410+
return { errorMsg, minBitVersion: '1.9.107' };
1411+
}
1412+
}
1413+
13791414
/**
13801415
* Return a list of outdated policy dependencies.
13811416
*/

scopes/harmony/aspect/aspect.main.runtime.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AspectLoaderAspect, AspectLoaderMain } from '@teambit/aspect-loader';
33
import { LoggerAspect, LoggerMain } from '@teambit/logger';
44
import { BuilderAspect, BuilderMain } from '@teambit/builder';
55
import { compact, merge } from 'lodash';
6-
import { EnvPolicyConfigObject } from '@teambit/dependency-resolver';
6+
import { DependencyResolverAspect, DependencyResolverMain, EnvPolicyConfigObject } from '@teambit/dependency-resolver';
77
import { BitError } from '@teambit/bit-error';
88
import { CLIAspect, CLIMain, MainRuntime } from '@teambit/cli';
99
import { EnvContext, Environment, EnvsAspect, EnvsMain, EnvTransformer } from '@teambit/envs';
@@ -39,7 +39,8 @@ export class AspectMain {
3939
readonly aspectEnv: AspectEnv,
4040
private envs: EnvsMain,
4141
private workspace: Workspace,
42-
private aspectLoader: AspectLoaderMain
42+
private aspectLoader: AspectLoaderMain,
43+
private depsResolver: DependencyResolverMain
4344
) {}
4445

4546
/**
@@ -223,6 +224,11 @@ export class AspectMain {
223224
const result = this.envs.validateEnvId(envExt);
224225
if (result) return result;
225226
}
227+
const depResolverExt = extensionDataList.findCoreExtension(DependencyResolverAspect.id);
228+
if (depResolverExt) {
229+
const result = this.depsResolver.validateAspectData(depResolverExt.data as any);
230+
if (result) return result;
231+
}
226232
}
227233

228234
static runtime = MainRuntime;
@@ -238,10 +244,12 @@ export class AspectMain {
238244
LoggerAspect,
239245
WorkerAspect,
240246
DevFilesAspect,
247+
DependencyResolverAspect,
241248
];
242249

243250
static async provider(
244-
[react, envs, builder, aspectLoader, compiler, generator, workspace, cli, loggerMain, workerMain, devFilesMain]: [
251+
[react, envs, builder, aspectLoader, compiler, generator, workspace, cli, loggerMain, workerMain, devFilesMain,
252+
depResolver]: [
245253
ReactMain,
246254
EnvsMain,
247255
BuilderMain,
@@ -253,6 +261,7 @@ export class AspectMain {
253261
LoggerMain,
254262
WorkerMain,
255263
DevFilesMain,
264+
DependencyResolverMain
256265
],
257266
config,
258267
slots,
@@ -275,7 +284,7 @@ export class AspectMain {
275284
const envContext = new EnvContext(ComponentID.fromString(ReactAspect.id), loggerMain, workerMain, harmony);
276285
generator.registerComponentTemplate(() => getTemplates(envContext));
277286
}
278-
const aspectMain = new AspectMain(aspectEnv as AspectEnv, envs, workspace, aspectLoader);
287+
const aspectMain = new AspectMain(aspectEnv as AspectEnv, envs, workspace, aspectLoader, depResolver);
279288
const aspectCmd = new AspectCmd();
280289
aspectCmd.commands = [
281290
new ListAspectCmd(aspectMain),

0 commit comments

Comments
 (0)