Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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,48 @@ 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;

const envJsonc = parse(envJsoncFile.contents.toString()) as EnvJsonc;
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 +1658,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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { EnvPolicy, EnvPolicyConfigObject } from './env-policy';
export { EnvPolicy, EnvPolicyConfigObject, EnvJsoncPolicyEntry } 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';
167 changes: 167 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,167 @@
---
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);
}
```

### Updating workspace.jsonc

Preserve team preferences for indentation when updating workspace config:

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

const updatedWorkspace = updateJsoncPreservingFormatting(
workspaceContent,
(config) => {
config['teambit.workspace/workspace'].defaultScope = 'my-org.my-scope';
return config;
}
);
```

### 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);
```

## Real-World Example

This is exactly how Bit's `bit update` command preserves your env.jsonc formatting when updating dependencies:

```ts
async updateEnvJsoncPolicies(outdatedPkgs) {
await pMapSeries(components, async (component) => {
const envJsoncFile = component.filesystem.files.find(
(file) => file.relative === 'env.jsonc'
);
if (!envJsoncFile) return;

const envJsoncContent = envJsoncFile.contents.toString();
const updatedContent = updateJsoncPreservingFormatting(
envJsoncContent,
(envJsonc) => {
// Update dependency versions
const dep = envJsonc.policy?.runtime?.find(
(d) => d.name === pkg.name
);
if (dep) {
dep.version = pkg.latestRange;
}
return envJsonc;
}
);

await fs.writeFile(absPath, updatedContent);
});
}
```

## Type Definitions

```ts
export type JsoncFormatting = {
indent: string;
newline: string;
};
```

## Why Preserve Formatting?

When working in teams, developers often have different preferences for code formatting:
- Some prefer 2 spaces, others 4 spaces or tabs
- Windows users might have CRLF line endings
- Comments provide important context

By preserving the original formatting, you avoid:
- ❌ Unnecessary git diffs
- ❌ Formatting debates in code reviews
- ❌ Lost comments and documentation
- ❌ Breaking team conventions

Instead, you get:
- ✅ Clean, minimal diffs showing only actual changes
- ✅ Preserved comments and documentation
- ✅ Respect for team and individual preferences
- ✅ Better collaboration
Loading