Skip to content

Commit 5b42a14

Browse files
authored
feat(config), support saving config values in the local workspace/scope (teambit#9555)
## Proposed Changes - introduce `--local` flag for `bit config set` to save the config in the local scope at `.bit/scope.json`. - introduce `--local-track` flag for `bit config set` to save the config in the workspace.jsonc. - default to save to the global-scope, same as it is now. - introduce `--detailed` flag for `bit config list` to show the config-stores of all: workspace, scope and global. Separately and combined. - introduce `--origin` flag for `bit config list` to show the config of a specific store: global, scope or workspace. - introduce `--json` flag for `bit config list` to print the config in json format.
1 parent 0f908c7 commit 5b42a14

File tree

52 files changed

+805
-299
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+805
-299
lines changed

.bitmap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,20 @@
450450
"mainFile": "index.ts",
451451
"rootDir": "scopes/workspace/config-merger"
452452
},
453+
"config-store": {
454+
"name": "config-store",
455+
"scope": "",
456+
"version": "",
457+
"defaultScope": "teambit.harmony",
458+
"mainFile": "index.ts",
459+
"rootDir": "components/config-store",
460+
"config": {
461+
"teambit.harmony/aspect": {},
462+
"teambit.envs/envs": {
463+
"env": "teambit.harmony/aspect"
464+
}
465+
}
466+
},
453467
"constants": {
454468
"name": "constants",
455469
"scope": "teambit.legacy",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* eslint max-classes-per-file: 0 */
2+
import chalk from 'chalk';
3+
import rightpad from 'pad-right';
4+
import { BASE_DOCS_DOMAIN } from '@teambit/legacy.constants';
5+
import { Command, CommandOptions } from '@teambit/cli';
6+
import { ConfigStoreMain, StoreOrigin } from './config-store.main.runtime';
7+
8+
class ConfigSet implements Command {
9+
name = 'set <key> <val>';
10+
description = 'set a configuration. default to save it globally';
11+
extendedDescription = `to set temporary configuration by env variable, prefix with "BIT_CONFIG", replace "." with "_" and change to upper case.
12+
for example, "user.token" becomes "BIT_CONFIG_USER_TOKEN"`;
13+
baseUrl = 'reference/config/bit-config/';
14+
alias = '';
15+
skipWorkspace = true;
16+
options = [
17+
['l', 'local', 'set the configuration in the current scope (saved in .bit/scope.json)'],
18+
['t', 'local-track', 'set the configuration in the current workspace (saved in workspace.jsonc)'],
19+
] as CommandOptions;
20+
21+
constructor(private configStore: ConfigStoreMain) {}
22+
23+
async report([key, value]: [string, string], { local, localTrack }: { local?: boolean, localTrack?: boolean }) {
24+
const getOrigin = () => {
25+
if (local) return 'scope';
26+
if (localTrack) return 'workspace';
27+
return 'global';
28+
}
29+
await this.configStore.setConfig(key, value, getOrigin());
30+
return chalk.green('added configuration successfully');
31+
}
32+
}
33+
34+
class ConfigGet implements Command {
35+
name = 'get <key>';
36+
description = 'get a value from global configuration';
37+
alias = '';
38+
options = [] as CommandOptions;
39+
40+
constructor(private configStore: ConfigStoreMain) {}
41+
42+
async report([key]: [string]) {
43+
const value = this.configStore.getConfig(key);
44+
return value || '';
45+
}
46+
}
47+
48+
class ConfigList implements Command {
49+
name = 'list';
50+
description = 'list all configuration(s)';
51+
alias = '';
52+
options = [
53+
['o', 'origin <origin>', 'list configuration specifically from the following: [scope, workspace, global]'],
54+
['d', 'detailed', 'list all configuration(s) with the origin'],
55+
['j', 'json', 'output as JSON'],
56+
] as CommandOptions;
57+
58+
constructor(private configStore: ConfigStoreMain) {}
59+
60+
async report(_, { origin, detailed }: { origin?: StoreOrigin, detailed?: boolean }) {
61+
62+
const objToFormattedString = (conf: Record<string, string>) => {
63+
return Object.entries(conf)
64+
.map((tuple) => {
65+
tuple[0] = rightpad(tuple[0], 45, ' ');
66+
return tuple.join('');
67+
})
68+
.join('\n');
69+
}
70+
71+
if (origin) {
72+
const conf = this.configStore.stores[origin].list();
73+
return objToFormattedString(conf);
74+
}
75+
76+
if (detailed) {
77+
const formatTitle = (str: string) => chalk.bold(str.toUpperCase());
78+
const origins = Object.keys(this.configStore.stores).map((originName) => {
79+
const conf = this.configStore.stores[originName].list();
80+
return formatTitle(originName) + '\n' + objToFormattedString(conf);
81+
}).join('\n\n');
82+
const combined = this.configStore.listConfig();
83+
84+
const combinedFormatted = objToFormattedString(combined);
85+
return `${origins}\n\n${formatTitle('All Combined')}\n${combinedFormatted}`;
86+
}
87+
88+
const conf = this.configStore.listConfig();
89+
return objToFormattedString(conf);
90+
}
91+
92+
async json(_, { origin, detailed }: { origin?: StoreOrigin, detailed?: boolean }) {
93+
if (origin) {
94+
return this.configStore.stores[origin].list();
95+
}
96+
if (detailed) {
97+
const allStores = Object.keys(this.configStore.stores).reduce((acc, current) => {
98+
acc[current] = this.configStore.stores[current].list();
99+
return acc;
100+
}, {} as Record<string, Record<string, string>>);
101+
allStores.combined = this.configStore.listConfig();
102+
return allStores;
103+
}
104+
return this.configStore.listConfig();
105+
}
106+
}
107+
108+
class ConfigDel implements Command {
109+
name = 'del <key>';
110+
description = 'delete given key from global configuration';
111+
alias = '';
112+
options = [
113+
['o', 'origin <origin>', 'default to delete whenever it found first. specify to delete specifically from the following: [scope, workspace, global]'],
114+
] as CommandOptions;
115+
116+
constructor(private configStore: ConfigStoreMain) {}
117+
118+
async report([key]: [string], { origin }: { origin?: StoreOrigin }) {
119+
await this.configStore.delConfig(key, origin);
120+
return chalk.green('deleted successfully');
121+
}
122+
}
123+
124+
export class ConfigCmd implements Command {
125+
name = 'config';
126+
description = 'config management';
127+
extendedDescription = `${BASE_DOCS_DOMAIN}reference/config/bit-config`;
128+
group = 'general';
129+
alias = '';
130+
loadAspects = false;
131+
commands: Command[] = [];
132+
options = [] as CommandOptions;
133+
134+
constructor(private configStore: ConfigStoreMain) {
135+
this.commands = [
136+
new ConfigSet(configStore), new ConfigDel(configStore), new ConfigGet(configStore), new ConfigList(configStore)
137+
];
138+
}
139+
140+
async report() {
141+
return new ConfigList(this.configStore).report(undefined, { });
142+
}
143+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import gitconfig from '@teambit/gitconfig';
2+
import { isNil } from 'lodash';
3+
import { GlobalConfig } from '@teambit/legacy.global-config';
4+
5+
export const ENV_VARIABLE_CONFIG_PREFIX = 'BIT_CONFIG_';
6+
7+
export interface Store {
8+
list(): Record<string, string>;
9+
set(key: string, value: string): void;
10+
del(key: string): void;
11+
write(): Promise<void>;
12+
invalidateCache(): Promise<void>;
13+
}
14+
15+
/**
16+
* Singleton cache for the config object. so it can be used everywhere even by non-aspects components.
17+
*/
18+
export class ConfigGetter {
19+
private _store: Record<string, string> | undefined;
20+
private _globalConfig: GlobalConfig | undefined;
21+
private gitStore: Record<string, string | undefined> = {};
22+
23+
get globalConfig() {
24+
if (!this._globalConfig) {
25+
this._globalConfig = GlobalConfig.loadSync();
26+
}
27+
return this._globalConfig;
28+
}
29+
30+
get store() {
31+
if (!this._store) {
32+
this._store = this.globalConfig.toPlainObject();
33+
}
34+
return this._store;
35+
}
36+
37+
/**
38+
* in case a config-key exists in both, the new one (the given store) wins.
39+
*/
40+
addStore(store: Store) {
41+
const currentStore = this.store;
42+
this._store = { ...currentStore, ...store.list() };
43+
}
44+
45+
getConfig(key: string): string | undefined {
46+
if (!key) {
47+
return undefined;
48+
}
49+
50+
const envVarName = toEnvVariableName(key);
51+
if (process.env[envVarName]) {
52+
return process.env[envVarName];
53+
}
54+
55+
const store = this.store;
56+
const val = store[key];
57+
if (!isNil(val)) {
58+
return val;
59+
}
60+
61+
if (key in this.gitStore) {
62+
return this.gitStore[key];
63+
}
64+
try {
65+
const gitVal = gitconfig.get.sync(key);
66+
this.gitStore[key] = gitVal;
67+
} catch {
68+
// Ignore error from git config get
69+
this.gitStore[key] = undefined;
70+
}
71+
return this.gitStore[key];
72+
}
73+
getConfigNumeric(key: string): number | undefined {
74+
const fromConfig = this.getConfig(key);
75+
if (isNil(fromConfig)) return undefined;
76+
const num = Number(fromConfig);
77+
if (Number.isNaN(num)) {
78+
throw new Error(`config of "${key}" is invalid. Expected number, got "${fromConfig}"`);
79+
}
80+
return num;
81+
}
82+
getConfigBoolean(key: string): boolean | undefined {
83+
const result = this.getConfig(key);
84+
if (isNil(result)) return undefined;
85+
if (typeof result === 'boolean') return result;
86+
if (result === 'true') return true;
87+
if (result === 'false') return false;
88+
throw new Error(`the configuration "${key}" has an invalid value "${result}". it should be boolean`);
89+
}
90+
listConfig() {
91+
const store = this.store;
92+
return store;
93+
}
94+
invalidateCache() {
95+
this._store = undefined;
96+
}
97+
getGlobalStore(): Store {
98+
return {
99+
list: () => this.globalConfig.toPlainObject(),
100+
set: (key: string, value: string) => this.globalConfig.set(key, value),
101+
del: (key: string) => this.globalConfig.delete(key),
102+
write: async () => this.globalConfig.write(),
103+
invalidateCache: async () => this._globalConfig = undefined,
104+
};
105+
}
106+
}
107+
108+
export const configGetter = new ConfigGetter();
109+
110+
export function getConfig(key: string): string | undefined {
111+
return configGetter.getConfig(key);
112+
}
113+
export function getNumberFromConfig(key: string): number | undefined {
114+
return configGetter.getConfigNumeric(key);
115+
}
116+
export function listConfig(): Record<string, string> {
117+
return configGetter.listConfig();
118+
}
119+
120+
function toEnvVariableName(configName: string): string {
121+
return `${ENV_VARIABLE_CONFIG_PREFIX}${configName.replace(/\./g, '_').toUpperCase()}`;
122+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Aspect } from '@teambit/harmony';
2+
3+
export const ConfigStoreAspect = Aspect.create({
4+
id: 'teambit.harmony/config-store',
5+
});
6+
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import CLIAspect, { CLIMain, MainRuntime } from '@teambit/cli';
2+
import { ConfigStoreAspect } from './config-store.aspect';
3+
import { configGetter, Store } from './config-getter';
4+
import { ConfigCmd } from './config-cmd';
5+
6+
export type StoreOrigin = 'scope' | 'workspace' | 'global';
7+
8+
export class ConfigStoreMain {
9+
private _stores: { [origin: string]: Store } | undefined;
10+
get stores (): { [origin: string]: Store } {
11+
if (!this._stores) {
12+
this._stores = {
13+
global: configGetter.getGlobalStore()
14+
};
15+
}
16+
return this._stores;
17+
}
18+
addStore(origin: StoreOrigin, store: Store) {
19+
this.stores[origin] = store;
20+
configGetter.addStore(store);
21+
}
22+
async invalidateCache() {
23+
configGetter.invalidateCache();
24+
for await (const origin of Object.keys(this.stores)) {
25+
const store = this.stores[origin];
26+
await store.invalidateCache();
27+
configGetter.addStore(store);
28+
};
29+
}
30+
async setConfig(key: string, value: string, origin: StoreOrigin = 'global') {
31+
const store = this.stores[origin];
32+
if (!store) throw new Error(`unable to set config, "${origin}" origin is missing`);
33+
store.set(key, value);
34+
await store.write();
35+
await this.invalidateCache();
36+
}
37+
getConfig(key: string): string | undefined {
38+
return configGetter.getConfig(key);
39+
}
40+
getConfigBoolean(key: string): boolean | undefined {
41+
return configGetter.getConfigBoolean(key);
42+
}
43+
getConfigNumeric(key: string): number | undefined {
44+
return configGetter.getConfigNumeric(key);
45+
}
46+
async delConfig(key: string, origin?: StoreOrigin) {
47+
const getOrigin = () => {
48+
if (origin) return origin;
49+
return Object.keys(this.stores).find(originName => key in this.stores[originName].list());
50+
}
51+
const foundOrigin = getOrigin();
52+
if (!foundOrigin) return; // if the key is not found in any store (or given store), nothing to do.
53+
const store = this.stores[foundOrigin];
54+
store.del(key);
55+
await store.write();
56+
await this.invalidateCache();
57+
}
58+
listConfig() {
59+
return configGetter.listConfig();
60+
}
61+
62+
static slots = [];
63+
static dependencies = [CLIAspect];
64+
static runtime = MainRuntime;
65+
static async provider([cli]: [CLIMain]) {
66+
const configStore = new ConfigStoreMain();
67+
cli.register(new ConfigCmd(configStore));
68+
return configStore;
69+
70+
}
71+
}
72+
73+
ConfigStoreAspect.addRuntime(ConfigStoreMain);
74+
75+
export default ConfigStoreMain;

components/config-store/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ConfigStoreAspect } from './config-store.aspect';
2+
3+
export type { ConfigStoreMain } from './config-store.main.runtime';
4+
export default ConfigStoreAspect;
5+
export { ConfigStoreAspect };
6+
export { getConfig, getNumberFromConfig, listConfig, Store } from './config-getter';

components/legacy/analytics/analytics-sender.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-disable no-console */
22
import fetch from 'node-fetch';
3-
import { getSync } from '@teambit/legacy.global-config';
43
import { CFG_ANALYTICS_DOMAIN_KEY, DEFAULT_ANALYTICS_DOMAIN } from '@teambit/legacy.constants';
4+
import { getConfig } from '@teambit/config-store';
55

6-
const ANALYTICS_DOMAIN = getSync(CFG_ANALYTICS_DOMAIN_KEY) || DEFAULT_ANALYTICS_DOMAIN;
6+
const ANALYTICS_DOMAIN = getConfig(CFG_ANALYTICS_DOMAIN_KEY) || DEFAULT_ANALYTICS_DOMAIN;
77
/**
88
* to debug errors here, first, change the parent to have { silent: false }, in analytics.js `fork` call.
99
*/

0 commit comments

Comments
 (0)