Skip to content
Open
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
8 changes: 8 additions & 0 deletions .bitmap
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,14 @@
"mainFile": "index.ts",
"rootDir": "scopes/defender/jest"
},
"json/jsonc-utils": {
"name": "json/jsonc-utils",
"scope": "",
"version": "",
"defaultScope": "teambit.toolbox",
"mainFile": "index.ts",
"rootDir": "scopes/toolbox/json/jsonc-utils"
},
"lanes": {
"name": "lanes",
"scope": "teambit.lanes",
Expand Down
67 changes: 67 additions & 0 deletions e2e/harmony/dependencies/env-jsonc-policies.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,71 @@ describe('env-jsonc-policies', function () {
});
});
});
describe('bit update', function () {
let envId;
before(() => {
helper = new Helper();
helper.scopeHelper.setWorkspaceWithRemoteScope();
envId = 'react-based-env';
helper.env.setCustomNewEnv(undefined, undefined, {
policy: {
runtime: [
{
name: 'is-string',
version: '1.0.5',
force: true,
},
],
},
}, false, envId);
helper.command.install();
});
after(() => {
helper.scopeHelper.destroy();
});

it('should update env.jsonc dependency to latest version', () => {
const envJsoncPath = path.join(helper.scopes.localPath, envId, 'env.jsonc');
const originalEnvJsonc = fs.readJsonSync(envJsoncPath);
expect(originalEnvJsonc.policy.runtime[0].version).to.equal('1.0.5');

helper.command.update('is-string --yes');

const updatedEnvJsonc = fs.readJsonSync(envJsoncPath);
expect(updatedEnvJsonc.policy.runtime[0].version).to.not.equal('1.0.5');
});

it('should update supportedRange for peerDependencies when new version is outside existing range', () => {
const envId2 = 'react-based-env-peers';
helper.env.setCustomNewEnv(undefined, undefined, {
policy: {
peers: [
{
name: 'react',
version: '16.8.0',
supportedRange: '^16.8.0',
},
],
},
}, false, envId2);
helper.command.install();

const envJsoncPath = path.join(helper.scopes.localPath, envId2, 'env.jsonc');
const originalEnvJsonc = fs.readJsonSync(envJsoncPath);
expect(originalEnvJsonc.policy.peers[0].supportedRange).to.equal('^16.8.0');

// Update react to latest (which is > 16.8.0, likely 18.x)
helper.command.update('react --yes');

const updatedEnvJsonc = fs.readJsonSync(envJsoncPath);
const newVersion = updatedEnvJsonc.policy.peers[0].version;
const newSupportedRange = updatedEnvJsonc.policy.peers[0].supportedRange;

expect(newVersion).to.not.equal('16.8.0');
// Should now contain the old range OR the new range/version
expect(newSupportedRange).to.include(' || ');
expect(newSupportedRange).to.include('^16.8.0');
expect(newSupportedRange).to.include(newVersion);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import multimatch from 'multimatch';
import { isSnap } from '@teambit/component-version';
import { BitError } from '@teambit/bit-error';
Expand Down Expand Up @@ -37,7 +38,7 @@ import { Http } from '@teambit/scope.network';
import type { Dependency as LegacyDependency } from '@teambit/legacy.consumer-component';
import { ConsumerComponent as LegacyComponent } from '@teambit/legacy.consumer-component';
import fs from 'fs-extra';
import { assign } from 'comment-json';
import { assign, parse } from 'comment-json';
import { ComponentID } from '@teambit/component-id';
import { readCAFileSync } from '@pnpm/network.ca-file';
import { parseBareSpecifier } from '@pnpm/npm-resolver';
Expand Down Expand Up @@ -88,7 +89,7 @@ import { DependenciesFragment, DevDependenciesFragment, PeerDependenciesFragment
import { dependencyResolverSchema } from './dependency-resolver.graphql';
import type { DependencyDetector } from './detector-hook';
import { DependenciesService } from './dependencies.service';
import { EnvPolicy } from './policy/env-policy';
import { EnvPolicy, type EnvJsoncPolicyEntry } from './policy/env-policy';
import type { ConfigStoreMain } from '@teambit/config-store';
import { ConfigStoreAspect } from '@teambit/config-store';

Expand Down Expand Up @@ -1493,6 +1494,7 @@ as an alternative, you can use "+" to keep the same version installed in the wor
variantPoliciesByPatterns,
componentPolicies,
components,
includeEnvJsoncDeps: true,
});
if (patterns?.length) {
const selectedPkgNames = new Set(
Expand All @@ -1514,10 +1516,12 @@ as an alternative, you can use "+" to keep the same version installed in the wor
variantPoliciesByPatterns,
componentPolicies,
components,
includeEnvJsoncDeps = false,
}: {
variantPoliciesByPatterns: Record<string, VariantPolicyConfigObject>;
componentPolicies: Array<{ componentId: ComponentID; policy: any }>;
components: Component[];
includeEnvJsoncDeps?: boolean;
}): CurrentPkg[] {
const localComponentPkgNames = new Set(components.map((component) => this.getPackageName(component)));
const componentModelVersions: ComponentModelVersion[] = components
Expand All @@ -1542,12 +1546,55 @@ as an alternative, you can use "+" to keep the same version installed in the wor
}));
})
.flat();
return getAllPolicyPkgs({
rootPolicy: this.getWorkspacePolicyFromConfig(),
variantPoliciesByPatterns,
componentPolicies,
componentModelVersions,
});
return [
...getAllPolicyPkgs({
rootPolicy: this.getWorkspacePolicyFromConfig(),
variantPoliciesByPatterns,
componentPolicies,
componentModelVersions,
}),
...(includeEnvJsoncDeps ? this.getEnvJsoncPolicyPkgs(components) : []),
];
}

getEnvJsoncPolicyPkgs(components: Component[]): CurrentPkg[] {
const policies = [
{ field: 'peers', targetField: 'peerDependencies' as const },
{ field: 'dev', targetField: 'devDependencies' as const },
{ field: 'runtime', targetField: 'dependencies' as const },
];
const pkgs: CurrentPkg[] = [];
for (const component of components) {
const isEnv = this.envs.isEnv(component);
if (!isEnv) continue;

const envJsoncFile = component.filesystem.files.find((file) => file.relative === 'env.jsonc');
if (!envJsoncFile) continue;

let envJsonc: EnvJsonc;
try {
envJsonc = parse(envJsoncFile.contents.toString()) as EnvJsonc;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.warn(`Failed to parse env.jsonc for component ${component.id.toString()}: ${errorMessage}`);
continue;
}
if (!envJsonc.policy) continue;

for (const { field, targetField } of policies) {
const deps: EnvJsoncPolicyEntry[] = envJsonc.policy?.[field] || [];
for (const dep of deps) {
pkgs.push({
name: dep.name,
currentRange: dep.version,
source: 'env-jsonc',
componentId: component.id,
targetField,
});
}
}
}
return pkgs;
}

getAllDedupedDirectDependencies(opts: {
Expand Down Expand Up @@ -1618,9 +1665,7 @@ as an alternative, you can use "+" to keep the same version installed in the wor
rootDir: string;
forceVersionBump?: 'major' | 'minor' | 'patch' | 'compatible';
},
pkgs: Array<
{ name: string; currentRange: string; source: 'variants' | 'component' | 'rootPolicy' | 'component-model' } & T
>
pkgs: Array<{ name: string; currentRange: string; source: CurrentPkgSource; } & T>
): Promise<Array<{ name: string; currentRange: string; latestRange: string } & T>> {
this.logger.setStatusLine('checking the latest versions of dependencies');
const resolver = await this.getVersionResolver();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export type CurrentPkgSource =
// these are dependencies from the dependencies policy in "workspace.jsonc"
| 'rootPolicy'
// these are dependencies stored in the component object (snapped/tagged version)
| 'component-model';
| 'component-model'
// these are dependencies defined in env.jsonc files
| 'env-jsonc';

export type CurrentPkg = {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion scopes/dependencies/dependency-resolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type {
SemverVersion,
DependenciesManifest,
} from './dependencies';
export { WorkspacePolicy, VariantPolicy, EnvPolicy } from './policy';
export { WorkspacePolicy, VariantPolicy, EnvPolicy, EnvPolicyEnvJsoncConfigObject } from './policy';
export type {
WorkspacePolicyEntry,
WorkspacePolicyConfigObject,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { EnvPolicy, EnvPolicyConfigObject } from './env-policy';
export { EnvPolicy, EnvPolicyConfigObject, EnvJsoncPolicyEntry, EnvPolicyEnvJsoncConfigObject } from './env-policy';
2 changes: 1 addition & 1 deletion scopes/dependencies/dependency-resolver/policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ export {
VariantPolicyConfigArr,
} from './variant-policy';

export { EnvPolicy, EnvPolicyConfigObject } from './env-policy';
export { EnvPolicy, EnvPolicyConfigObject, EnvPolicyEnvJsoncConfigObject } from './env-policy';
7 changes: 7 additions & 0 deletions scopes/toolbox/json/jsonc-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
detectJsoncFormatting,
parseJsoncWithFormatting,
stringifyJsonc,
updateJsoncPreservingFormatting,
type JsoncFormatting,
} from './jsonc-utils';
91 changes: 91 additions & 0 deletions scopes/toolbox/json/jsonc-utils/jsonc-utils.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
labels: ['typescript', 'utils', 'json', 'jsonc', 'formatting', 'comments', 'parser']
description: 'Utilities for parsing and stringifying JSONC files while preserving formatting and comments.'
---

# JSONC Utils

Utilities for working with JSONC (JSON with Comments) files while preserving their original formatting, including:
- **Indentation style** (2 spaces, 4 spaces, tabs)
- **Newline characters** (LF, CRLF)
- **Comments**

This is particularly useful when you need to programmatically update configuration files without losing their human-readable formatting.

## Installation

```bash
bit install @teambit/toolbox.json.jsonc-utils
```

## Quick Start

The easiest way to update a JSONC file while preserving its formatting:

```ts
import { updateJsoncPreservingFormatting } from '@teambit/toolbox.json.jsonc-utils';

const originalContent = `{
// This is a comment
"name": "my-package",
"version": "1.0.0"
}`;

const updatedContent = updateJsoncPreservingFormatting(originalContent, (data) => {
data.version = "2.0.0";
return data;
});

// Output preserves comments and formatting:
// {
// // This is a comment
// "name": "my-package",
// "version": "2.0.0"
// }
```

## Use Cases

### Updating env.jsonc Files

When updating component environment configurations, preserve the original formatting:

```ts
import fs from 'fs-extra';
import { updateJsoncPreservingFormatting } from '@teambit/toolbox.json.jsonc-utils';

async function updateEnvDependency(
envJsoncPath: string,
pkgName: string,
newVersion: string
) {
const content = await fs.readFile(envJsoncPath, 'utf-8');

const updated = updateJsoncPreservingFormatting(content, (envJsonc) => {
const dep = envJsonc.policy?.runtime?.find((d) => d.name === pkgName);
if (dep) {
dep.version = newVersion;
}
return envJsonc;
});

await fs.writeFile(envJsoncPath, updated);
}
```

### Working with Different Indentation Styles

Handle files with different formatting preferences:

```ts
import { detectJsoncFormatting, stringifyJsonc } from '@teambit/toolbox.json.jsonc-utils';

// Detect existing formatting
const formatting = detectJsoncFormatting(originalContent);

// Modify data
const updatedData = { ...parsedData, version: '2.0.0' };

// Stringify with original formatting
const result = stringifyJsonc(updatedData, formatting);
```
Loading