Skip to content

Commit

Permalink
feat(cli): dynamic cli options
Browse files Browse the repository at this point in the history
  • Loading branch information
crherman7 committed Dec 17, 2024
1 parent eab73a6 commit 33b3a97
Show file tree
Hide file tree
Showing 26 changed files with 705 additions and 121 deletions.
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');
}
}

/**
* 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>",
"description": "Defines the directory path where environment configuration files are stored (relative or absolute path).",
"required": true,
"example": "--app-env-dir ./config/environments"
},
{
"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

0 comments on commit 33b3a97

Please sign in to comment.