Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): ENG-5881 dynamic cli options #2773

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ jobs:
- name: Run tests
run: yarn test
- name: Initialize react native app
run: yarn workspace @brandingbrand/code-example prebuild --build internal --env prod --platform android --verbose
run: yarn workspace @brandingbrand/code-example prebuild --build internal --platform android --app-env-initial prod --app-env-dir ./coderc/env --verbose
1 change: 1 addition & 0 deletions apps/example/.flagshipappenvrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"hiddenEnvs": [],
"singleEnv": false,
"dir": "./coderc/env"
}
101 changes: 68 additions & 33 deletions apps/example/coderc/plugins/plugin-env/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,84 @@
/**
* Defines a plugin for @brandingbrand/code-cli-kit.
* @module Plugin
*/

import {
definePlugin,
fs,
path,
withInfoPlist,
withStrings,
} from '@brandingbrand/code-cli-kit';

/**
* Defines a plugin with functions for both iOS and Android platforms.
* @alias module:Plugin
* @param {Object} build - The build configuration object.
* @param {Object} options - The options object.
* Custom error for missing required options.
*/
class MissingOptionError extends Error {
constructor(optionName: string) {
super(`MissingOptionError: missing ${optionName} variable`);
this.name = 'MissingOptionError';
}
}

/**
* Type definition for the plugin options.
*/
interface PluginOptions {
appEnvInitial: string;
appEnvDir: string;
appEnvHide?: string[];
release: boolean;
}

/**
* Helper function to validate required options.
*/
function validateOptions(options: PluginOptions) {
if (!options.appEnvInitial) {
throw new MissingOptionError('appEnvInitial');
}
if (!options.appEnvDir) {
throw new MissingOptionError('appEnvDir');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably also need to verify that these folders exist and the initial environment exists in the app environment directory. currently it seems like it works fine if I input invalid values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we could add additional validation of the directory

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


/**
* Helper function to write the environment configuration file.
*/
async function writeEnvConfig(options: PluginOptions) {
const configPath = path.join(process.cwd(), '.flagshipappenvrc');
const configData = {
hiddenEnvs: options.appEnvHide || [],
singleEnv: options.release,
dir: options.appEnvDir,
};

await fs.writeFile(configPath, JSON.stringify(configData, null, 2) + '\n');
}

/**
* Defines a plugin for both iOS and Android platforms.
*/
export default definePlugin({
/**
* Function to be executed for iOS platform.
* @param {Object} _build - The build configuration object for iOS.
* @param {Object} _options - The options object for iOS.
* @returns {Promise<void>} A promise that resolves when the process completes.
*/
ios: async function (_build: object, _options: object): Promise<void> {
await withInfoPlist(plist => {
return {
...plist,
FlagshipEnv: 'prod',
FlagshipDevMenu: true,
};
});
ios: async (_build: object, options: any): Promise<void> => {
validateOptions(options);

await withInfoPlist(plist => ({
...plist,
FlagshipEnv: options.appEnvInitial,
FlagshipDevMenu: options.release,
}));

await writeEnvConfig(options);
},

/**
* Function to be executed for Android platform.
* @param {Object} _build - The build configuration object for Android.
* @param {Object} _options - The options object for Android.
* @returns {Promise<void>} A promise that resolves when the process completes.
*/
android: async function (_build: object, _options: object): Promise<void> {
return withStrings(xml => {
xml.resources.string?.push({$: {name: 'flagship_env'}, _: 'prod'});
xml.resources.string?.push({$: {name: 'flagship_dev_menu'}, _: 'true'});
android: async (_build: object, options: any): Promise<void> => {
validateOptions(options);

await withStrings(xml => {
xml.resources.string?.push(
{$: {name: 'flagship_env'}, _: options.appEnvInitial},
{$: {name: 'flagship_dev_menu'}, _: `${options.release}`},
);
return xml;
});

await writeEnvConfig(options);
},
});
22 changes: 22 additions & 0 deletions packages/app-env/.flagshipcoderc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"options": [
{
"flags": "--app-env-initial <value>",
"description": "Specifies the initial environment to be used when the application starts (e.g., 'development', 'staging', or 'production').",
"required": true,
"example": "--app-env-initial development"
},
{
"flags": "--app-env-dir <value>",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We technically could aggregate this with babel traversal and looking at imports etc.

"description": "Defines the directory path where environment configuration files are stored (relative or absolute path).",
"required": true,
"example": "--app-env-dir ./config/environments"
NickBurkhartBB marked this conversation as resolved.
Show resolved Hide resolved
},
{
"flags": "--app-env-hide <value>",
"description": "Specifies one or more environments to hide from selection or visibility (comma-separated list).",
"required": false,
"example": "--app-env-hide staging,test"
}
]
}
5 changes: 5 additions & 0 deletions packages/cli/__tests__/common_fixtures/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"@brandingbrand/fsapp": "11.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/cli/__tests__/env-switcher-java.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @jest-environment-options {"requireTemplate": true}
* @jest-environment-options {"requireTemplate": true, "fixtures": "common_fixtures"}
*/

/// <reference types="@brandingbrand/code-jest-config" />
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/__tests__/env-switcher-m.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @jest-environment-options {"requireTemplate": true}
* @jest-environment-options {"requireTemplate": true, "fixtures": "common_fixtures"}
*/

/// <reference types="@brandingbrand/code-jest-config" />
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/__tests__/main-application-java.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @jest-environment-options {"requireTemplate": true}
* @jest-environment-options {"requireTemplate": true, "fixtures": "project-pbxproj_fixtures"}
*/

/// <reference types="@brandingbrand/code-jest-config" />
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/__tests__/native-constants-java.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @jest-environment-options {"requireTemplate": true}
* @jest-environment-options {"requireTemplate": true, "fixtures": "common_fixtures"}
*/

/// <reference types="@brandingbrand/code-jest-config" />
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/__tests__/native-constants-m.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @jest-environment-options {"requireTemplate": true}
* @jest-environment-options {"requireTemplate": true, "fixtures": "common_fixtures"}
*/

/// <reference types="@brandingbrand/code-jest-config" />
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/__tests__/project-pbxproj_fixtures/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"@brandingbrand/fsapp": "11.0.0"
}
}
4 changes: 4 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"detect-package-manager": "^3.0.1",
"esbuild": "^0.20.0",
"execa": "^9.3.0",
"find-node-modules": "^2.1.3",
"fp-ts": "^2.16.2",
"glob": "^11.0.0",
"ink": "^4.4.1",
"ink-spinner": "^5.0.0",
"io-ts": "^2.2.21",
Expand All @@ -57,9 +59,11 @@
"@repo/typescript-config": "*",
"@types/ansi-align": "3.0.5",
"@types/eslint": "^8.56.1",
"@types/find-node-modules": "^2.1.2",
"@types/node": "^20.10.6",
"@types/npmcli__package-json": "^4.0.4",
"@types/react": "18.2.6",
"ajv": "^8.17.1",
"react": "^18.2.0",
"type-fest": "^4.10.2",
"typescript": "^5.3.3"
Expand Down
74 changes: 74 additions & 0 deletions packages/cli/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Flagship Code Commands Configuration",
"description": "Schema for defining custom commander options in flagship-code.commands.json",
"type": "object",
"properties": {
"options": {
"type": "array",
"description": "An array of custom options to be dynamically added to commander commands",
"items": {
"type": "object",
"properties": {
"flags": {
"type": "string",
"description": "The flag syntax for the option, following commander conventions (e.g., '--flag <value>').",
"pattern": "^--[a-zA-Z0-9\\-_]+(\\s<[a-zA-Z0-9\\-_]+>|\\s\\[[a-zA-Z0-9\\-_]+\\])?$",
"examples": [
"--custom-flag <value>",
"--enable-feature",
"--mode [type]"
],
"minLength": 2
},
"description": {
"type": "string",
"description": "A brief description of the option, including the expected value format or type.",
"minLength": 1,
"examples": [
"Specifies the initial environment to use, e.g., 'development', 'staging', or 'production'.",
"Defines the path to the environments directory.",
"A comma-separated list of environments to hide."
]
},
"example": {
"type": "string",
"description": "A concrete example showing how to use the option.",
"examples": [
"--app-env-initial development",
"--app-env-dir ./config/environments",
"--app-env-hide staging,test"
]
},
"defaultValue": {
"description": "The default value for the option if it is not specified by the user.",
"anyOf": [
{"type": "string"},
{"type": "number"},
{"type": "boolean"},
{"type": "null"}
]
},
"choices": {
"type": "array",
"description": "Restrict the option's possible values to a predefined set.",
"items": {
"type": "string"
},
"uniqueItems": true,
"examples": [["debug", "release", "test"]]
},
"required": {
"type": "boolean",
"description": "Indicates whether the option is mandatory.",
"default": false
}
},
"required": ["flags", "description"],
"additionalProperties": false
}
}
},
"required": ["options"],
"additionalProperties": false
}
37 changes: 35 additions & 2 deletions packages/cli/src/actions/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
getReactNativeVersion,
} from '@brandingbrand/code-cli-kit';

import {config, defineAction, isGenerateCommand} from '@/lib';
import {FSAPP_DEPENDENCY, config, defineAction, isGenerateCommand} from '@/lib';
import {hasDependency, matchesFilePatterns} from '@/lib/dependencies';

/**
* Define an action to initialize a project template.
Expand All @@ -29,6 +30,38 @@ export default defineAction(async () => {
`react-native-${reactNativeVersion}`,
);

/**
* Filters files during a copy operation to exclude specific native files if the `fsapp` dependency is not installed.
*
* The function checks if the current project does not have the `fsapp` dependency and matches the source file
* against a predefined set of patterns. If both conditions are true, the file is excluded.
*
* @param src - The source file path being processed.
* @returns `false` if the file should be excluded; otherwise, `true`.
*
* @example
* ```typescript
* const shouldCopy = filter('/path/to/ios/EnvSwitcher.java');
* console.log(shouldCopy); // true or false depending on the presence of 'fsapp' and file match
* ```
*/
const fileFilter = (src: string): boolean => {
if (
!hasDependency(process.cwd(), FSAPP_DEPENDENCY) &&
matchesFilePatterns(src, [
'EnvSwitcher.java',
'EnvSwitcher.m',
'NativeConstants.java',
'NativeConstants.m',
'EnvSwitcherPackage.java',
'NativeConstantsPackage.java',
])
) {
return false;
}
return true;
};

// If the generate cli command was executed copy the plugin template only
// WARNING: Consider moving this in future.
if (isGenerateCommand()) {
Expand Down Expand Up @@ -138,7 +171,7 @@ export default defineAction(async () => {
return false;
}

return true;
return fileFilter(path);
},
})
.catch(e => {
Expand Down
Loading
Loading