Skip to content

Commit 16e822d

Browse files
authored
feat: Command and README to generate bootstrapping configuration (#248)
* feat: Bootstrap configuration and generation tool * allow globals in node script/tools * README
1 parent fb7c554 commit 16e822d

File tree

10 files changed

+216
-16
lines changed

10 files changed

+216
-16
lines changed

.eslintrc.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
env: {
44
es6: true,
55
jest: true,
6+
node: true,
67
},
78
extends: [
89
'eslint:recommended',
@@ -95,5 +96,11 @@ module.exports = {
9596
'no-restricted-globals': 'off',
9697
},
9798
},
99+
{
100+
files: ['src/tools/**/*.ts'],
101+
rules: {
102+
'no-restricted-globals': 'off',
103+
},
104+
},
98105
],
99106
};

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,78 @@ When publishing releases, the following rules apply:
5050
**Note**: The release will not be published if:
5151
- A pre-release is marked as "latest"
5252
- A pre-release label is used without checking "Set as pre-release"
53+
54+
## Tools
55+
56+
### Bootstrap Configuration
57+
58+
You can generate a bootstrap configuration string from either the command line or programmatically via the
59+
ConfigurationWireHelper class.
60+
61+
The tool allows you to specify the target SDK this configuration will be used on. It is important to correctly specify
62+
the intended SDK, as this determines whether the configuration is obfuscated (for client SDKs) or not (for server SDKs).
63+
64+
#### Command Line Usage
65+
66+
**Install as a project dependency:**
67+
```bash
68+
# Install as a dependency
69+
npm install --save-dev @eppo/js-client-sdk-common
70+
71+
# or, with yarn
72+
yarn add --dev @eppo/js-client-sdk-common
73+
```
74+
75+
Common usage examples:
76+
```bash
77+
# Basic usage
78+
yarn bootstrap-config --key <sdkKey> --output bootstrap-config.json
79+
80+
# With custom SDK name (default is 'js-client-sdk')
81+
yarn bootstrap-config --key <sdkKey> --sdk android
82+
83+
# With custom base URL
84+
yarn bootstrap-config --key <sdkKey> --base-url https://api.custom-domain.com
85+
86+
# Output configuration to stdout
87+
yarn bootstrap-config --key <sdkKey>
88+
89+
# Show help
90+
yarn bootstrap-config --help
91+
```
92+
93+
The tool accepts the following arguments:
94+
- `--key, -k`: SDK key (required, can also be set via EPPO_SDK_KEY environment variable)
95+
- `--sdk`: Target SDK name (default: 'js-client-sdk')
96+
- `--base-url`: Custom base URL for the API
97+
- `--output, -o`: Output file path (if not specified, outputs to console)
98+
- `--help, -h`: Show help
99+
100+
#### Programmatic Usage
101+
```typescript
102+
import { ConfigurationHelper } from '@eppo/js-client-sdk-common';
103+
104+
async function getBootstrapConfig() {
105+
// Initialize the helper
106+
const helper = ConfigurationHelper.build(
107+
'your-sdk-key',
108+
{
109+
sdkName: 'android', // optional: target SDK name (default: 'js-client-sdk')
110+
baseUrl: 'https://api.custom-domain.com', // optional: custom base URL
111+
});
112+
113+
// Fetch the configuration
114+
const config = await helper.fetchConfiguration();
115+
const configString = config.toString();
116+
117+
// You are responsible to transport this string to the client
118+
const clientInitialData = {eppoConfig: eppoConfigString};
119+
120+
// Client-side
121+
const client = getInstance();
122+
const initialConfig = configurationFromString(clientInitialData.eppoConfig);
123+
client.setInitialConfig(configurationFromString(configString));
124+
}
125+
```
126+
127+
The tool will output a JSON string containing the configuration wire format that can be used to bootstrap Eppo SDKs.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"typecheck": "tsc",
2828
"test": "yarn test:unit",
2929
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
30-
"obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC"
30+
"obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC",
31+
"bootstrap-config": "ts-node src/tools/get-bootstrap-config"
3132
},
3233
"jsdelivr": "dist/eppo-sdk.js",
3334
"repository": {
@@ -76,7 +77,8 @@
7677
"pino": "^9.5.0",
7778
"semver": "^7.5.4",
7879
"spark-md5": "^3.0.2",
79-
"uuid": "^11.0.5"
80+
"uuid": "^11.0.5",
81+
"yargs": "^17.7.2"
8082
},
8183
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
8284
}

src/configuration-wire/configuration-wire-helper.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('ConfigurationWireHelper', () => {
1919
baseUrl: TEST_BASE_URL,
2020
});
2121

22-
const wirePacket = await helper.fetchBootstrapConfiguration();
22+
const wirePacket = await helper.fetchConfiguration();
2323

2424
expect(wirePacket.version).toBe(1);
2525
expect(wirePacket.config).toBeDefined();
@@ -47,9 +47,10 @@ describe('ConfigurationWireHelper', () => {
4747
sdkName: 'node-server',
4848
sdkVersion: '4.0.0',
4949
baseUrl: TEST_BASE_URL,
50+
fetchBandits: true,
5051
});
5152

52-
const wirePacket = await helper.fetchBootstrapConfiguration();
53+
const wirePacket = await helper.fetchConfiguration();
5354

5455
expect(wirePacket.version).toBe(1);
5556
expect(wirePacket.config).toBeDefined();
@@ -76,12 +77,12 @@ describe('ConfigurationWireHelper', () => {
7677

7778
it('should include fetchedAt timestamps', async () => {
7879
const helper = ConfigurationWireHelper.build(BANDIT_SDK_KEY, {
79-
sdkName: 'android',
80-
sdkVersion: '4.0.0',
80+
sdkName: 'node-server',
8181
baseUrl: TEST_BASE_URL,
82+
fetchBandits: true,
8283
});
8384

84-
const wirePacket = await helper.fetchBootstrapConfiguration();
85+
const wirePacket = await helper.fetchConfiguration();
8586

8687
if (!wirePacket.config) {
8788
throw new Error('Flag config not present in ConfigurationWire');

src/configuration-wire/configuration-wire-helper.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import SdkTokenDecoder from '../sdk-token-decoder';
99
import { ConfigurationWireV1, IConfigurationWire } from './configuration-wire-types';
1010

1111
export type SdkOptions = {
12-
sdkName: string;
13-
sdkVersion: string;
12+
sdkName?: string;
13+
sdkVersion?: string;
1414
baseUrl?: string;
15+
fetchBandits?: boolean;
1516
};
1617

1718
/**
@@ -23,25 +24,28 @@ export class ConfigurationWireHelper {
2324
/**
2425
* Build a new ConfigurationHelper for the target SDK Key.
2526
* @param sdkKey
27+
* @param opts
2628
*/
2729
public static build(
2830
sdkKey: string,
29-
opts: SdkOptions = { sdkName: 'android', sdkVersion: '4.0.0' },
31+
opts: SdkOptions = { sdkName: 'js-client-sdk', sdkVersion: '4.0.0' },
3032
) {
31-
const { sdkName, sdkVersion, baseUrl } = opts;
32-
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl);
33+
const { sdkName, sdkVersion, baseUrl, fetchBandits } = opts;
34+
return new ConfigurationWireHelper(sdkKey, sdkName, sdkVersion, baseUrl, fetchBandits);
3335
}
3436

3537
private constructor(
3638
sdkKey: string,
37-
targetSdkName = 'android',
39+
targetSdkName = 'js-client-sdk',
3840
targetSdkVersion = '4.0.0',
3941
baseUrl?: string,
42+
private readonly fetchBandits = false,
4043
) {
4144
const queryParams = {
4245
sdkName: targetSdkName,
4346
sdkVersion: targetSdkVersion,
4447
apiKey: sdkKey,
48+
sdkProxy: 'config-wire-helper',
4549
};
4650
const apiEndpoints = new ApiEndpoints({
4751
baseUrl,
@@ -56,7 +60,7 @@ export class ConfigurationWireHelper {
5660
* Fetches configuration data from the API and build a Bootstrap Configuration (aka an `IConfigurationWire` object).
5761
* The IConfigurationWire instance can be used to bootstrap some SDKs.
5862
*/
59-
public async fetchBootstrapConfiguration(): Promise<IConfigurationWire> {
63+
public async fetchConfiguration(): Promise<IConfigurationWire> {
6064
// Get the configs
6165
let banditResponse: IBanditParametersResponse | undefined;
6266
const configResponse: IUniversalFlagConfigResponse | undefined =
@@ -68,7 +72,7 @@ export class ConfigurationWireHelper {
6872
}
6973

7074
const flagsHaveBandits = Object.keys(configResponse.banditReferences ?? {}).length > 0;
71-
if (flagsHaveBandits) {
75+
if (this.fetchBandits && flagsHaveBandits) {
7276
banditResponse = await this.httpClient.getBanditParameters();
7377
}
7478

src/configuration-wire/configuration-wire-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export class ConfigurationWireV1 implements IConfigurationWire {
200200
readonly bandits?: IConfigResponse<IBanditParametersResponse>,
201201
) {}
202202

203+
public toString(): string {
204+
return JSON.stringify(this as IConfigurationWire);
205+
}
206+
203207
public static fromResponses(
204208
flagConfig: IUniversalFlagConfigResponse,
205209
banditConfig?: IBanditParametersResponse,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as fs from 'fs';
2+
3+
import type { CommandModule } from 'yargs';
4+
5+
import { ConfigurationWireHelper } from '../../configuration-wire/configuration-wire-helper';
6+
7+
export const bootstrapConfigCommand: CommandModule = {
8+
command: 'bootstrap-config',
9+
describe: 'Generate a bootstrap configuration string',
10+
builder: (yargs) => {
11+
return yargs.options({
12+
key: {
13+
type: 'string',
14+
description: 'SDK key',
15+
alias: 'k',
16+
default: process.env.EPPO_SDK_KEY,
17+
},
18+
sdk: {
19+
type: 'string',
20+
description: 'Target SDK name',
21+
default: 'js-client-sdk',
22+
},
23+
'base-url': {
24+
type: 'string',
25+
description: 'Base URL for the API',
26+
},
27+
output: {
28+
type: 'string',
29+
description: 'Output file path',
30+
alias: 'o',
31+
},
32+
});
33+
},
34+
handler: async (argv) => {
35+
if (!argv.key) {
36+
console.error('Error: SDK key is required');
37+
console.error('Provide it either as:');
38+
console.error('- Command line argument: --key <sdkKey> or -k <sdkKey>');
39+
console.error('- Environment variable: EPPO_SDK_KEY');
40+
process.exit(1);
41+
}
42+
43+
try {
44+
const helper = ConfigurationWireHelper.build(argv.key as string, {
45+
sdkName: argv.sdk as string,
46+
baseUrl: argv['base-url'] as string,
47+
fetchBandits: true,
48+
});
49+
const config = await helper.fetchConfiguration();
50+
51+
if (!config) {
52+
console.error('Error: Failed to fetch configuration');
53+
process.exit(1);
54+
}
55+
56+
const jsonConfig = JSON.stringify(config, null, 2);
57+
58+
if (argv.output && typeof argv.output === 'string') {
59+
fs.writeFileSync(argv.output, jsonConfig);
60+
console.log(`Configuration written to ${argv.output}`);
61+
} else {
62+
console.log('Configuration:');
63+
console.log('--------------------------------');
64+
console.log(jsonConfig);
65+
console.log('--------------------------------');
66+
}
67+
} catch (error) {
68+
console.error('Error fetching configuration:', error);
69+
process.exit(1);
70+
}
71+
},
72+
};

src/tools/get-bootstrap-config.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import yargs from 'yargs';
2+
import { hideBin } from 'yargs/helpers';
3+
4+
import { bootstrapConfigCommand } from './commands/bootstrap-config';
5+
6+
/**
7+
* Script to run the bootstrap-config command directly.
8+
*
9+
* For usage, run: `ts-node src/tools/get-bootstrap-config.ts --help`
10+
*/
11+
async function main() {
12+
await yargs(hideBin(process.argv))
13+
.command({
14+
command: '$0',
15+
describe: bootstrapConfigCommand.describe,
16+
builder: bootstrapConfigCommand.builder,
17+
handler: bootstrapConfigCommand.handler,
18+
})
19+
.help()
20+
.alias('help', 'h')
21+
.parse();
22+
}
23+
24+
main().catch((error) => {
25+
console.error('Error in main:', error);
26+
process.exit(1);
27+
});

src/tools/node-shim.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Required type shape for `process`.
2+
// We don't pin this project to node so eslint complains about the use of `process`. We declare a type shape here to
3+
// appease the linter.
4+
export declare const process: {
5+
exit: (code: number) => void;
6+
env: { [key: string]: string | undefined };
7+
argv: string[];
8+
};

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5081,7 +5081,7 @@ yargs-parser@^21.1.1:
50815081
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
50825082
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
50835083

5084-
yargs@^17.3.1:
5084+
yargs@^17.3.1, yargs@^17.7.2:
50855085
version "17.7.2"
50865086
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
50875087
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==

0 commit comments

Comments
 (0)