Skip to content

Commit cbca268

Browse files
authored
CLI Generate feature remote configs (#363)
Resolves: BUC-3398, BUC-3387 ## Copilot Summary This pull request introduces several enhancements and changes to the `packages/cli` module, focusing on improving the configuration options, command-line interface, and type generation processes. The most important changes include updating the `typesOutput` configuration, modifying command options, and adding new utility functions. ### Configuration and Command Updates: * [`packages/cli/README.md`](diffhunk://#diff-15986190ef9581ab59bcd5483b2c09e7fd0bd439d6f6cddbc94b0b1de094ee51L73-R89): Updated the `typesOutput` configuration to support an array of objects with `path` and `format` properties, and modified related command options (`--overwrite`, `--format`) to reflect these changes. [[1]](diffhunk://#diff-15986190ef9581ab59bcd5483b2c09e7fd0bd439d6f6cddbc94b0b1de094ee51L73-R89) [[2]](diffhunk://#diff-15986190ef9581ab59bcd5483b2c09e7fd0bd439d6f6cddbc94b0b1de094ee51L96-R106) [[3]](diffhunk://#diff-15986190ef9581ab59bcd5483b2c09e7fd0bd439d6f6cddbc94b0b1de094ee51L110-R115) [[4]](diffhunk://#diff-15986190ef9581ab59bcd5483b2c09e7fd0bd439d6f6cddbc94b0b1de094ee51R124) [[5]](diffhunk://#diff-15986190ef9581ab59bcd5483b2c09e7fd0bd439d6f6cddbc94b0b1de094ee51L173-R186) ### Command Implementation Changes: * [`packages/cli/commands/features.ts`](diffhunk://#diff-62cb1138f98fd9620a8f451d3171b4558f8690fa8fad668d98cd60a507f04315L8-R17): Refactored the `generateTypesAction` to handle multiple `typesOutput` configurations, generating types for each specified format and path. Added new option `typesFormatOption` for specifying the format. [[1]](diffhunk://#diff-62cb1138f98fd9620a8f451d3171b4558f8690fa8fad668d98cd60a507f04315L8-R17) [[2]](diffhunk://#diff-62cb1138f98fd9620a8f451d3171b4558f8690fa8fad668d98cd60a507f04315L83-R96) [[3]](diffhunk://#diff-62cb1138f98fd9620a8f451d3171b4558f8690fa8fad668d98cd60a507f04315L104-R123) [[4]](diffhunk://#diff-62cb1138f98fd9620a8f451d3171b4558f8690fa8fad668d98cd60a507f04315R156-R166) * [`packages/cli/commands/init.ts`](diffhunk://#diff-76aae8c1424cc73067ec33d08cef49f3f8745b2827c72ed4d1e408b1141ed2d8L8-R14): Replaced `--force` with `--overwrite` option and added a prompt for selecting the output format during initialization. [[1]](diffhunk://#diff-76aae8c1424cc73067ec33d08cef49f3f8745b2827c72ed4d1e408b1141ed2d8L8-R14) [[2]](diffhunk://#diff-76aae8c1424cc73067ec33d08cef49f3f8745b2827c72ed4d1e408b1141ed2d8L24-R26) [[3]](diffhunk://#diff-76aae8c1424cc73067ec33d08cef49f3f8745b2827c72ed4d1e408b1141ed2d8R77-R96) [[4]](diffhunk://#diff-76aae8c1424cc73067ec33d08cef49f3f8745b2827c72ed4d1e408b1141ed2d8L100-R110) * [`packages/cli/commands/new.ts`](diffhunk://#diff-afaf24e9c51fdcf6d151a9d6a6ac53c50de2e6cc274fd860fa34a6473e91f988R11): Added `typesFormatOption` to the `new` command and updated the configuration handling to support the new `typesOutput` format. [[1]](diffhunk://#diff-afaf24e9c51fdcf6d151a9d6a6ac53c50de2e6cc274fd860fa34a6473e91f988R11) [[2]](diffhunk://#diff-afaf24e9c51fdcf6d151a9d6a6ac53c50de2e6cc274fd860fa34a6473e91f988R43-R55) ### Utility and Schema Changes: * [`packages/cli/stores/config.ts`](diffhunk://#diff-0c6bdf2dfe67c672cea5959d6db4ea9117d53e302e9f4410508a599cb0b6689dR2): Added `typeFormats` and `TypesOutput` types, and a helper function `normalizeTypesOutput` to standardize the `typesOutput` configuration. [[1]](diffhunk://#diff-0c6bdf2dfe67c672cea5959d6db4ea9117d53e302e9f4410508a599cb0b6689dR2) [[2]](diffhunk://#diff-0c6bdf2dfe67c672cea5959d6db4ea9117d53e302e9f4410508a599cb0b6689dR17) [[3]](diffhunk://#diff-0c6bdf2dfe67c672cea5959d6db4ea9117d53e302e9f4410508a599cb0b6689dL26-R43) [[4]](diffhunk://#diff-0c6bdf2dfe67c672cea5959d6db4ea9117d53e302e9f4410508a599cb0b6689dL43-R66) * [`packages/cli/schema.json`](diffhunk://#diff-1ce328445e6bae65ca6b80ba4983acad1e6a8df9062eea0aac912326f53799b5L16-R24): Updated the schema to reflect the new `typesOutput` structure, allowing an array of objects with `path` and `format` properties. ### Miscellaneous: * [`packages/cli/package.json`](diffhunk://#diff-6e2e2a1851648938b325ba84de634407a4e69a644ea61102df15ca4a8a7a9758L3-R3): Bumped the version to `0.1.3`, added new scripts for testing and coverage, and included additional dependencies. [[1]](diffhunk://#diff-6e2e2a1851648938b325ba84de634407a4e69a644ea61102df15ca4a8a7a9758L3-R3) [[2]](diffhunk://#diff-6e2e2a1851648938b325ba84de634407a4e69a644ea61102df15ca4a8a7a9758R34-R49) [[3]](diffhunk://#diff-6e2e2a1851648938b325ba84de634407a4e69a644ea61102df15ca4a8a7a9758L57-R63) * [`packages/cli/index.ts`](diffhunk://#diff-ce752ce36be970cbc0110dae521e82462b9ee7dbca55f45b51648c82998da18dR14): Added a utility function `stripTrailingSlash` to clean up URLs before setting the configuration. [[1]](diffhunk://#diff-ce752ce36be970cbc0110dae521e82462b9ee7dbca55f45b51648c82998da18dR14) [[2]](diffhunk://#diff-ce752ce36be970cbc0110dae521e82462b9ee7dbca55f45b51648c82998da18dR29-R34) * [`packages/cli/services/features.ts`](diffhunk://#diff-2475cdaadabf46b6c77ce79efe6909efb569949ed9d0aba37c9be2ebf7e7c73bL3-R48): Enhanced the `listFeatures` function to include remote configurations and added new types for handling remote config variants.
1 parent c672cd2 commit cbca268

26 files changed

Lines changed: 1740 additions & 111 deletions

packages/cli/README.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,24 @@ Here's a comprehensive list of configuration options available in the `bucket.co
7070
"baseUrl": "https://app.bucket.co",
7171
"apiUrl": "https://app.bucket.co/api",
7272
"appId": "ap123456789",
73-
"typesOutput": "gen/features.ts",
73+
"typesOutput": [
74+
{
75+
"path": "gen/features.ts",
76+
"format": "react"
77+
}
78+
],
7479
"keyFormat": "camelCase"
7580
}
7681
```
7782

78-
| Option | Description | Default |
79-
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
80-
| `$schema` | Autocompletion for the config. `latest` can be replaced with a specific version. | "https://unpkg.com/@bucketco/cli@latest/schema.json" |
81-
| `baseUrl` | Base URL for Bucket services. | "https://app.bucket.co" |
82-
| `apiUrl` | API URL for Bucket services (overrides baseUrl for API calls). | "https://app.bucket.co/api" |
83-
| `appId` | Your Bucket application ID. | Required |
84-
| `typesOutput` | Path where TypeScript types will be generated. | "gen/features.ts" |
85-
| `keyFormat` | Format for feature keys (options: custom, pascalCase, camelCase, snakeCaseUpper, snakeCaseLower, kebabCaseUpper, kebabCaseLower). | "custom" |
83+
| Option | Description | Default |
84+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
85+
| `$schema` | Autocompletion for the config. `latest` can be replaced with a specific version. | "https://unpkg.com/@bucketco/cli@latest/schema.json" |
86+
| `baseUrl` | Base URL for Bucket services. | "https://app.bucket.co" |
87+
| `apiUrl` | API URL for Bucket services (overrides baseUrl for API calls). | "https://app.bucket.co/api" |
88+
| `appId` | Your Bucket application ID. | Required |
89+
| `typesOutput` | Path(s) where TypeScript types will be generated. Can be a string or an array of objects with `path` and `format` properties. Available formats: `react` and `node`. | "gen/features.ts" with format "react" |
90+
| `keyFormat` | Format for feature keys (options: custom, pascalCase, camelCase, snakeCaseUpper, snakeCaseLower, kebabCaseUpper, kebabCaseLower). | "custom" |
8691

8792
You can override these settings using command-line options for individual commands.
8893

@@ -93,12 +98,12 @@ You can override these settings using command-line options for individual comman
9398
Initialize a new Bucket configuration in your project. This creates a `bucket.config.json` file with your settings and prompts for any required information not provided via options.
9499

95100
```bash
96-
bucket init [--force]
101+
bucket init [--overwrite]
97102
```
98103

99104
Options:
100105

101-
- `--force`: Overwrite existing configuration file if one exists
106+
- `--overwrite`: Overwrite existing configuration file if one exists
102107
- `--app-id <id>`: Set the application ID
103108
- `--key-format <format>`: Set the key format for features
104109

@@ -107,7 +112,7 @@ Options:
107112
All-in-one command to get started quickly. This command combines `init`, feature creation, and type generation in a single step. Use this for the fastest way to get up and running with Bucket.
108113

109114
```bash
110-
bucket new "My Feature" [--key my-feature] [--app-id ap123456789] [--key-format custom] [--out gen/features.ts]
115+
bucket new "My Feature" [--key my-feature] [--app-id ap123456789] [--key-format custom] [--out gen/features.ts] [--format react]
111116
```
112117

113118
Options:
@@ -116,6 +121,7 @@ Options:
116121
- `--app-id`: App ID to use
117122
- `--key-format`: Format for feature keys (custom, snake, camel, etc.)
118123
- `--out`: Path to generate TypeScript types
124+
- `--format`: Format of the generated types (react or node)
119125

120126
If you prefer more control over each step, you can use the individual commands (`init`, `features create`, `features types`) instead.
121127

@@ -170,13 +176,14 @@ Options:
170176
Generate TypeScript types for your features. This ensures type safety when using Bucket features in your TypeScript/JavaScript applications.
171177

172178
```bash
173-
bucket features types [--app-id ap123456789] [--out gen/features.ts]
179+
bucket features types [--app-id ap123456789] [--out gen/features.ts] [--format react]
174180
```
175181

176182
Options:
177183

178184
- `--app-id`: App ID to use
179185
- `--out`: Path to generate TypeScript types
186+
- `--format`: Format of the generated types (react or node)
180187

181188
### `bucket apps`
182189

packages/cli/commands/features.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import { mkdir, writeFile } from "node:fs/promises";
55
import { dirname, isAbsolute, join, relative } from "node:path";
66
import ora, { Ora } from "ora";
77

8-
import { createFeature, listFeatures } from "../services/features.js";
8+
import { createFeature, Feature, listFeatures } from "../services/features.js";
99
import { configStore } from "../stores/config.js";
1010
import { handleError, MissingAppIdError } from "../utils/errors.js";
11-
import { genDTS, genFeatureKey, KeyFormatPatterns } from "../utils/gen.js";
11+
import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js";
1212
import {
1313
appIdOption,
1414
featureKeyOption,
1515
featureNameArgument,
1616
keyFormatOption,
17+
typesFormatOption,
1718
typesOutOption,
1819
} from "../utils/options.js";
1920

@@ -80,16 +81,19 @@ export const listFeaturesAction = async () => {
8081
};
8182

8283
export const generateTypesAction = async () => {
83-
const { baseUrl, appId, typesOutput } = configStore.getConfig();
84+
const { baseUrl, appId } = configStore.getConfig();
85+
const typesOutput = configStore.getConfig("typesOutput");
8486

8587
let spinner: Ora | undefined;
86-
let featureKeys: string[] = [];
88+
let features: Feature[] = [];
8789
try {
8890
if (!appId) throw new MissingAppIdError();
8991
spinner = ora(
9092
`Loading features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`,
9193
).start();
92-
featureKeys = (await listFeatures(appId)).map(({ key }) => key);
94+
features = await listFeatures(appId, {
95+
includeRemoteConfigs: true,
96+
});
9397
spinner.succeed(
9498
`Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}`,
9599
);
@@ -101,15 +105,22 @@ export const generateTypesAction = async () => {
101105

102106
try {
103107
spinner = ora("Generating feature types...").start();
104-
const types = genDTS(featureKeys);
105108
const projectPath = configStore.getProjectPath();
106-
const outPath = isAbsolute(typesOutput)
107-
? typesOutput
108-
: join(projectPath, typesOutput);
109-
await mkdir(dirname(outPath), { recursive: true });
110-
await writeFile(outPath, types);
109+
110+
// Generate types for each output configuration
111+
for (const output of typesOutput) {
112+
const types = await genTypes(features, output.format);
113+
const outPath = isAbsolute(output.path)
114+
? output.path
115+
: join(projectPath, output.path);
116+
117+
await mkdir(dirname(outPath), { recursive: true });
118+
await writeFile(outPath, types);
119+
spinner.text = `Generated ${output.format} types for ${chalk.cyan(appId)} in ${chalk.cyan(relative(projectPath, outPath))}.`;
120+
}
121+
111122
spinner.succeed(
112-
`Generated types for ${chalk.cyan(appId)} in ${chalk.cyan(relative(projectPath, outPath))}.`,
123+
`Generated types for ${chalk.cyan(appId)} in ${typesOutput.length} location(s).`,
113124
);
114125
} catch (error) {
115126
spinner?.fail("Type generation failed");
@@ -142,12 +153,17 @@ export function registerFeatureCommands(cli: Command) {
142153
.description("Generate feature types")
143154
.addOption(appIdOption)
144155
.addOption(typesOutOption)
156+
.addOption(typesFormatOption)
145157
.action(generateTypesAction);
146158

147159
// Update the config with the cli override values
148160
featuresCommand.hook("preAction", (_, command) => {
149-
const { appId, keyFormat, out } = command.opts();
150-
configStore.setConfig({ appId, keyFormat, typesOutput: out });
161+
const { appId, keyFormat, out, format } = command.opts();
162+
configStore.setConfig({
163+
appId,
164+
keyFormat,
165+
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
166+
});
151167
});
152168

153169
cli.addCommand(featuresCommand);

packages/cli/commands/init.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { relative } from "node:path";
55
import ora, { Ora } from "ora";
66

77
import { App, listApps } from "../services/bootstrap.js";
8-
import { configStore } from "../stores/config.js";
8+
import { configStore, typeFormats } from "../stores/config.js";
99
import { chalkBrand, DEFAULT_TYPES_OUTPUT } from "../utils/constants.js";
1010
import { handleError } from "../utils/errors.js";
11-
import { initOverrideOption } from "../utils/options.js";
11+
import { overwriteOption } from "../utils/options.js";
1212

1313
type InitArgs = {
14-
force?: boolean;
14+
overwrite?: boolean;
1515
};
1616

1717
export const initAction = async (args: InitArgs = {}) => {
@@ -21,9 +21,9 @@ export const initAction = async (args: InitArgs = {}) => {
2121
try {
2222
// Check if config already exists
2323
const configPath = configStore.getConfigPath();
24-
if (configPath && !args.force) {
24+
if (configPath && !args.overwrite) {
2525
throw new Error(
26-
"Bucket is already initialized. Use --force to overwrite.",
26+
"Bucket is already initialized. Use --overwrite to overwrite.",
2727
);
2828
}
2929

@@ -74,16 +74,26 @@ export const initAction = async (args: InitArgs = {}) => {
7474
default: DEFAULT_TYPES_OUTPUT,
7575
});
7676

77+
// Get types output format
78+
const typesFormat = await select({
79+
message: "What is the output format?",
80+
choices: typeFormats.map((format) => ({
81+
name: format,
82+
value: format,
83+
})),
84+
default: "react",
85+
});
86+
7787
// Update config
7888
configStore.setConfig({
7989
appId,
8090
keyFormat,
81-
typesOutput,
91+
typesOutput: [{ path: typesOutput, format: typesFormat }],
8292
});
8393

8494
// Create config file
8595
spinner = ora("Creating configuration...").start();
86-
await configStore.saveConfigFile(args.force);
96+
await configStore.saveConfigFile(args.overwrite);
8797
spinner.succeed(
8898
`Configuration created at ${chalk.cyan(relative(process.cwd(), configStore.getConfigPath()!))}`,
8999
);
@@ -97,6 +107,6 @@ export function registerInitCommand(cli: Command) {
97107
cli
98108
.command("init")
99109
.description("Initialize a new Bucket configuration")
100-
.addOption(initOverrideOption)
110+
.addOption(overwriteOption)
101111
.action(initAction);
102112
}

packages/cli/commands/new.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
featureKeyOption,
99
featureNameArgument,
1010
keyFormatOption,
11+
typesFormatOption,
1112
typesOutOption,
1213
} from "../utils/options.js";
1314

@@ -39,13 +40,18 @@ export function registerNewCommand(cli: Command) {
3940
.addOption(appIdOption)
4041
.addOption(keyFormatOption)
4142
.addOption(typesOutOption)
43+
.addOption(typesFormatOption)
4244
.addOption(featureKeyOption)
4345
.addArgument(featureNameArgument)
4446
.action(newAction);
4547

4648
// Update the config with the cli override values
4749
cli.hook("preAction", (command) => {
48-
const { appId, keyFormat, out } = command.opts();
49-
configStore.setConfig({ appId, keyFormat, typesOutput: out });
50+
const { appId, keyFormat, out, format } = command.opts();
51+
configStore.setConfig({
52+
appId,
53+
keyFormat,
54+
typesOutput: out ? [{ path: out, format: format || "react" }] : undefined,
55+
});
5056
});
5157
}

packages/cli/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { registerNewCommand } from "./commands/new.js";
1111
import { authStore } from "./stores/auth.js";
1212
import { configStore } from "./stores/config.js";
1313
import { apiUrlOption, baseUrlOption, debugOption } from "./utils/options.js";
14+
import { stripTrailingSlash } from "./utils/path.js";
1415

1516
async function main() {
1617
// Must load tokens and config before anything else
@@ -25,9 +26,12 @@ async function main() {
2526
// Pre-action hook
2627
program.hook("preAction", () => {
2728
const { debug, baseUrl, apiUrl } = program.opts();
29+
const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim());
2830
configStore.setConfig({
29-
baseUrl,
30-
apiUrl: apiUrl || (baseUrl && `${baseUrl}/api`),
31+
baseUrl: cleanedBaseUrl,
32+
apiUrl:
33+
stripTrailingSlash(apiUrl) ||
34+
(cleanedBaseUrl && `${cleanedBaseUrl}/api`),
3135
});
3236

3337
if (debug) {

packages/cli/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bucketco/cli",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"packageManager": "yarn@4.1.1",
55
"description": "CLI for Bucket service",
66
"main": "./dist/index.js",
@@ -31,17 +31,22 @@
3131
"scripts": {
3232
"build": "tsc && shx chmod +x dist/index.js",
3333
"bucket": "yarn build && ./dist/index.js",
34+
"test": "vitest -c vite.config.js",
35+
"test:ci": "vitest run -c vite.config.js --reporter=default --reporter=junit --outputFile=junit.xml",
36+
"coverage": "vitest run --coverage",
3437
"lint": "eslint .",
3538
"lint:ci": "eslint --output-file eslint-report.json --format json .",
3639
"prettier": "prettier --check .",
37-
"format": "yarn lint --fix && yarn prettier --write"
40+
"format": "yarn lint --fix && yarn prettier --write",
41+
"preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.js && yarn build"
3842
},
3943
"dependencies": {
4044
"@inquirer/prompts": "^5.3.8",
4145
"ajv": "^8.17.1",
4246
"chalk": "^5.3.0",
4347
"change-case": "^5.4.4",
4448
"commander": "^12.1.0",
49+
"fast-deep-equal": "^3.1.3",
4550
"find-up": "^7.0.0",
4651
"json5": "^2.2.3",
4752
"open": "^10.1.0",
@@ -54,6 +59,7 @@
5459
"eslint": "^9.21.0",
5560
"prettier": "^3.5.2",
5661
"shx": "^0.3.4",
57-
"typescript": "^5.5.4"
62+
"typescript": "^5.5.4",
63+
"vitest": "^3.0.8"
5864
}
5965
}

packages/cli/schema.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@
1313
"type": "string"
1414
},
1515
"typesOutput": {
16-
"type": "string"
16+
"type": "array",
17+
"items": {
18+
"type": "object",
19+
"properties": {
20+
"path": { "type": "string" },
21+
"format": { "type": "string", "enum": ["react", "node"] }
22+
},
23+
"required": ["path"]
24+
}
1725
},
1826
"keyFormat": {
1927
"type": "string",

0 commit comments

Comments
 (0)