├── .gitignore ├── lib ├── flutterflow_cli.dart └── src │ ├── flutterflow_ignore.dart │ └── flutterflow_api_client.dart ├── test ├── fixtures │ └── .flutterflowignore ├── flutterflow_ignore_test.dart └── integration_test.dart ├── pubspec.yaml ├── example └── README.md ├── analysis_options.yaml ├── LICENSE ├── .github └── workflows │ └── main.yml ├── CHANGELOG.md ├── README.md ├── bin └── flutterflow_cli.dart └── pubspec.lock /.gitignore: -------------------------------------------------------------------------------- 1 | bin/flutterflow 2 | .dart_tool/ 3 | export/ 4 | -------------------------------------------------------------------------------- /lib/flutterflow_cli.dart: -------------------------------------------------------------------------------- 1 | library flutterflow_cli; 2 | 3 | export 'package:flutterflow_cli/src/flutterflow_api_client.dart'; 4 | -------------------------------------------------------------------------------- /test/fixtures/.flutterflowignore: -------------------------------------------------------------------------------- 1 | file 2 | dir/* 3 | dir_recursive/**/file 4 | dir_recursive_implicit/ 5 | this/file/exactly 6 | this/directory/ 7 | 8 | #comment 9 | #another_comment 10 | 11 | LEADING_SPACES.md 12 | TRAILING_SPACES.md 13 | LEADING_AND_TRAILING_SPACES.md 14 | 15 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutterflow_cli 2 | description: >- 3 | Command-line client for FlutterFlow. Export code from FlutterFlow projects. 4 | version: 0.0.28 5 | homepage: https://github.com/FlutterFlow/flutterflow-cli 6 | issue_tracker: https://github.com/flutterflow/flutterflow-issues 7 | 8 | environment: 9 | sdk: ">=2.17.0 <4.0.0" 10 | 11 | dependencies: 12 | archive: ^4.0.2 13 | args: ^2.6.0 14 | glob: ^2.1.3 15 | http: ^1.3.0 16 | path: ^1.9.0 17 | 18 | dev_dependencies: 19 | lints: ^3.0.0 20 | test: ^1.25.8 21 | 22 | executables: 23 | flutterflow: flutterflow_cli 24 | -------------------------------------------------------------------------------- /lib/src/flutterflow_ignore.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:glob/glob.dart'; 5 | import 'package:path/path.dart' as path_util; 6 | 7 | final kIgnoreFile = '.flutterflowignore'; 8 | 9 | class FlutterFlowIgnore { 10 | final List _globs = []; 11 | 12 | FlutterFlowIgnore({path = '.'}) { 13 | final String text; 14 | 15 | try { 16 | text = File(path_util.join(path, kIgnoreFile)).readAsStringSync(); 17 | } catch (e) { 18 | return; 19 | } 20 | 21 | if (text.isEmpty) { 22 | return; 23 | } 24 | 25 | final lines = LineSplitter().convert(text).map((line) { 26 | return line.trim().replaceAll(RegExp(r'/$'), '/**'); 27 | }); 28 | 29 | for (var line in lines) { 30 | if (line.isNotEmpty && !line.startsWith('#')) { 31 | _globs.add(Glob(line)); 32 | } 33 | } 34 | } 35 | 36 | bool matches(String path) { 37 | return _globs.any((glob) => glob.matches(path)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | ```sh 3 | dart pub global activate flutterflow_cli 4 | ``` 5 | 6 | ## How to use 7 | 8 | Base command with required flags: 9 | ```sh 10 | flutterflow export-code --project ProjectID --token APIToken 11 | ``` 12 | See [README](https://pub.dev/packages/flutterflow_cli) for the full list of available flags. 13 | 14 | ## Setting API Token Variable 15 | 16 | Your API Token can be added as the environment variable `FLUTTERFLOW_API_TOKEN`. This means you don't have to manually pass the `--token` flag. 17 | 18 | See your corresponding operating system guide: 19 | 20 | * [MacOS](https://support.apple.com/en-gb/guide/terminal/apd382cc5fa-4f58-4449-b20a-41c53c006f8f/mac) 21 | * [Linux](https://linuxize.com/post/how-to-set-and-list-environment-variables-in-linux/#persistent-environment-variables) 22 | * [Windows](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-7.4#saving-environment-variables-with-the-system-control-panel) -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, FlutterFlow 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: FlutterFlow 2 | on: push 3 | 4 | env: 5 | # Keep this in sync with the version used by FlutterFlow. 6 | DART_VERSION: 3.6.1 7 | FLUTTER_VERSION: 3.27.3 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-24.04 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Dart 19 | uses: dart-lang/setup-dart@v1 20 | with: 21 | sdk: ${{ env.DART_VERSION }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | dart pub get --enforce-lockfile 26 | 27 | - name: Analyze code 28 | run: | 29 | dart analyze 30 | 31 | - name: Format code 32 | run: | 33 | dart format . --set-exit-if-changed 34 | 35 | build_and_test: 36 | runs-on: ${{ matrix.os }} 37 | needs: check 38 | timeout-minutes: 30 39 | 40 | strategy: 41 | # Can't run in parallel otherwise we get rate limted 42 | max-parallel: 1 43 | matrix: 44 | os: [ubuntu-24.04, windows-2025, windows-2022, macos-14] 45 | 46 | steps: 47 | - name: Clone repository 48 | uses: actions/checkout@v4 49 | 50 | - name: Setup Flutter 51 | uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # v2 52 | with: 53 | channel: master 54 | flutter-version: ${{ env.FLUTTER_VERSION }} 55 | cache: true 56 | 57 | - name: Install dependencies 58 | run: | 59 | dart pub get --enforce-lockfile 60 | 61 | - name: Build 62 | run: | 63 | dart compile exe bin/flutterflow_cli.dart 64 | 65 | - name: Run 66 | run: | 67 | dart run bin/flutterflow_cli.dart -h 68 | 69 | - name: Test 70 | env: 71 | FF_TESTER_TOKEN: ${{ secrets.FF_TESTER_TOKEN }} 72 | run: | 73 | dart test 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.28 2 | 3 | - Update path version. 4 | 5 | ## 0.0.27 6 | 7 | - Update default endpoint. 8 | 9 | ## 0.0.26 10 | 11 | - Update dependency versions. 12 | 13 | ## 0.0.25 14 | 15 | - Update URL parsing to support more types of endpoints. 16 | 17 | ## 0.0.24 18 | 19 | - Fix an issue on Windows. 20 | - Add support for `.flutterflowignore`. 21 | ## 0.0.23 22 | 23 | - Add more logging. 24 | 25 | ## 0.0.22 26 | 27 | - Add `--project-environment` option. 28 | 29 | ## 0.0.21 30 | 31 | - Add `--as-debug` option. 32 | 33 | ## 0.0.20 34 | 35 | - Add `--commit-hash` option. 36 | 37 | ## 0.0.19 38 | 39 | - Add `exportAsDebug` option to `FlutterFlowApi.exportCode()`. 40 | 41 | ## 0.0.18 42 | 43 | - Add `deploy-firebase` command. 44 | 45 | ## 0.0.17 46 | 47 | - Add `format` option to `FlutterFlowApi.exportCode()`. 48 | 49 | ## 0.0.16 50 | - Set `include_assets_map` based on `--[no-]include-assets` when calling the API. 51 | 52 | ## 0.0.15 53 | 54 | - Add environment variable for `FLUTTERFLOW_ENDPOINT`. 55 | 56 | ## 0.0.14 57 | 58 | - Update `FlutterFlowApi` to throw exceptions instead of calling `exit()`. 59 | 60 | ## 0.0.13 61 | 62 | - Add `--as-module` option. 63 | 64 | ## 0.0.12 65 | 66 | - Fix `FLUTTERFLOW_PROJECT` environment variable issue. 67 | - Fix downloading assets when the parent directory does not exist. 68 | 69 | ## 0.0.11 70 | 71 | - Fix `--fix` `--no-parent-folder` edge case. 72 | - Add `--environment` option. 73 | 74 | ## 0.0.10 75 | 76 | - Adds bug fix to duplicate archiving. 77 | 78 | ## 0.0.9 79 | 80 | - Add `--[no-]-parent option`. 81 | - Allow importing it inside a Dart/Flutter project. 82 | - Allow passing project id via environment. 83 | - Upgrade dependencies. 84 | 85 | ## 0.0.8 86 | 87 | - Add `--fix` option. 88 | 89 | ## 0.0.7 90 | 91 | - Fix handling non-existent assets. 92 | 93 | ## 0.0.6 94 | 95 | - Add "branch-name" parameter to enable specifying a branch for a given project when generating code. 96 | 97 | ## 0.0.5 98 | 99 | - Send API token as a header. 100 | 101 | ## 0.0.4 102 | 103 | - Update API endpoint. 104 | 105 | ## 0.0.3 106 | 107 | - Switch to base64. 108 | 109 | ## 0.0.2 110 | 111 | - Lower min SDK version to 2.17.0. 112 | - Allow passing SDK token as a command line argument. 113 | 114 | ## 0.0.1 115 | 116 | - Initial version. 117 | -------------------------------------------------------------------------------- /test/flutterflow_ignore_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutterflow_cli/src/flutterflow_ignore.dart'; 2 | 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('does not match when file is not found', () { 7 | var ignore = FlutterFlowIgnore(path: '/this/path/is/wrong/'); 8 | 9 | expect(ignore.matches('file'), false); 10 | }); 11 | 12 | test('basic globbing syntax', () { 13 | var ignore = FlutterFlowIgnore(path: 'test/fixtures'); 14 | 15 | expect(ignore.matches('file'), true); 16 | expect(ignore.matches('dir/foo'), true); 17 | expect(ignore.matches('dir/bar'), true); 18 | expect(ignore.matches('this/file/exactly'), true); 19 | 20 | expect(ignore.matches('this/directory/matches'), true); 21 | expect(ignore.matches('this/directory/also/matches'), true); 22 | 23 | expect(ignore.matches('dir_recursive/foo/file'), true); 24 | expect(ignore.matches('dir_recursive/foo/bar/file'), true); 25 | expect(ignore.matches('dir_recursive/foo/bar/foo/file'), true); 26 | 27 | expect(ignore.matches('dir_recursive_implicit/foo/file'), true); 28 | expect(ignore.matches('dir_recursive_implicit/foo/bar/file'), true); 29 | expect(ignore.matches('dir_recursive_implicit/foo/bar/foo/file'), true); 30 | 31 | expect(ignore.matches('this/file'), false); 32 | expect(ignore.matches('this/file/not_really'), false); 33 | expect(ignore.matches('file/not_a_directory'), false); 34 | expect(ignore.matches('file_not_found'), false); 35 | expect(ignore.matches('dir_not_found/foo'), false); 36 | expect(ignore.matches('dir_not_found/bar'), false); 37 | expect(ignore.matches('this/directory'), false); 38 | 39 | expect(ignore.matches('dir_recursive/foo/file_not_found'), false); 40 | expect(ignore.matches('dir_recursive/foo/bar/file_not_found'), false); 41 | expect(ignore.matches('dir_recursive/foo/bar/foo/file_not_found'), false); 42 | }); 43 | 44 | test('comments are ignored', () { 45 | var ignore = FlutterFlowIgnore(path: 'test/fixtures'); 46 | 47 | expect(ignore.matches('#comment'), false); 48 | expect(ignore.matches('#another_comment'), false); 49 | }); 50 | 51 | test('does not match empty paths', () { 52 | var ignore = FlutterFlowIgnore(path: 'test/fixtures'); 53 | 54 | expect(ignore.matches(''), false); 55 | }); 56 | 57 | test('ignores leading and trailing spaces', () { 58 | var ignore = FlutterFlowIgnore(path: 'test/fixtures'); 59 | 60 | expect(ignore.matches('LEADING_SPACES.md'), true); 61 | expect(ignore.matches('TRAILING_SPACES.md'), true); 62 | expect(ignore.matches('LEADING_AND_TRAILING_SPACES.md'), true); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlutterFlow CLI 2 | 3 | FlutterFlow CLI client: download code from your FlutterFlow project to your device for local running or deployment. 4 | 5 | ## API Token 6 | 7 | API access is available only to users with active subscriptions. Visit https://app.flutterflow.io/account to generate your API token. 8 | 9 | ## Installation 10 | 11 | `dart pub global activate flutterflow_cli` 12 | 13 | ## Export Code 14 | 15 | ### Usage 16 | 17 | `flutterflow export-code --project --dest --[no-]include-assets --token --[no-]fix --[no-]parent-folder --[no-]as-module --[no-]as-debug` 18 | 19 | * Instead of passing `--token` you can set `FLUTTERFLOW_API_TOKEN` environment variable. 20 | * Instead of passing `--project` you can set `FLUTTERFLOW_PROJECT` environment variable. 21 | 22 | In case you are updating an existing project and you don't want existing files to be changed, you can create a `.flutterflowignore` file at the root of the output folder 23 | with a list of files to be ignored using [globbing syntax](https://pub.dev/packages/glob#syntax). 24 | 25 | ### Flags 26 | 27 | | Flag | Abbreviation | Usage | 28 | | ----------- | ----------- | ----------- | 29 | | `--project` | `-p` | [Required or environment variable] Project ID. | 30 | | `--token` | `-t` | [Required or environment variable] API Token. | 31 | | `--dest` | `-d` | [Optional] Output folder. Defaults to the current directory if none is specified. | 32 | | `--[no-]include-assets` | None | [Optional] Whether to include media assets. Defaults to `false`. | 33 | | `--branch-name` | `-b` | [Optional] Which branch to download. Defaults to `main`. | 34 | | `--[no-]fix` | None | [Optional] Whether to run `dart fix` on the downloaded code. Defaults to `false`. | 35 | | `--[no-]parent-folder` | None | [Optional] Whether to download code into a project-named sub-folder. If true, downloads all project files directly to the specified directory. Defaults to `true`. | 36 | | `--[no-]as-module` | None | [Optional] Whether to generate the project as a Flutter module. Defaults to `false`. | 37 | | `--[no-]as-debug` | None | [Optional] Whether to generate the project with debug logging to be able to use FlutterFlow Debug Panel inside the DevTools. Defaults to `false`. | 38 | | `--project-environment` | None | [Optional] Which project environment to be used. If empty, the current environment in the project will be used.| 39 | ## Deploy Firebase 40 | 41 | ### Prerequisites 42 | 43 | `npm` and `firebase-tools` must be installed in order to deploy to Firebase. You can follow the instructions at https://firebase.google.com/docs/cli#install_the_firebase_cli. 44 | 45 | ### Usage 46 | 47 | `flutterflow deploy-firebase --project --[no]-append-rules --token ` 48 | 49 | * Instead of passing `--token` you can set `FLUTTERFLOW_API_TOKEN` environment variable. 50 | * Instead of passing `--project` you can set `FLUTTERFLOW_PROJECT` environment variable. 51 | 52 | ### Flags 53 | 54 | | Flag | Abbreviation | Usage | 55 | | ----------- | ----------- | ----------- | 56 | | `--project` | `-p` | [Required or environment variable] Project ID. | 57 | | `--token` | `-t` | [Required or environment variable] API Token. | 58 | | `--append-rules` | `-a` | Whether to append to existing Firestore rules, instead of overwriting them. | 59 | 60 | ## Issues 61 | 62 | Please file any issues in [this repository](https://github.com/flutterflow/flutterflow-issues). 63 | -------------------------------------------------------------------------------- /bin/flutterflow_cli.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:flutterflow_cli/src/flutterflow_api_client.dart'; 5 | 6 | const kDefaultEndpoint = 'https://api.flutterflow.io/v2'; 7 | 8 | Future appMain(List args) async { 9 | final parsedArguments = _parseArgs(args); 10 | 11 | final token = 12 | parsedArguments['token'] ?? Platform.environment['FLUTTERFLOW_API_TOKEN']; 13 | 14 | final project = parsedArguments.command!['project'] ?? 15 | Platform.environment['FLUTTERFLOW_PROJECT']; 16 | 17 | if (project == null || project.isEmpty) { 18 | stderr.write( 19 | 'Either --project option or FLUTTERFLOW_PROJECT environment variable must be set.\n'); 20 | exit(1); 21 | } 22 | 23 | if (parsedArguments['endpoint'] != null && 24 | parsedArguments['environment'] != null) { 25 | stderr.write( 26 | 'Only one of --endpoint and --environment options can be set.\n'); 27 | exit(1); 28 | } 29 | 30 | if (token?.isEmpty ?? true) { 31 | stderr.write( 32 | 'Either --token option or FLUTTERFLOW_API_TOKEN environment variable must be set.\n'); 33 | exit(1); 34 | } 35 | 36 | late String endpoint; 37 | if (parsedArguments['endpoint'] != null) { 38 | endpoint = parsedArguments['endpoint']; 39 | } else if (parsedArguments['environment'] != null) { 40 | endpoint = 41 | "https://api-${parsedArguments['environment']}.flutterflow.io/v2"; 42 | } else { 43 | endpoint = Platform.environment['FLUTTERFLOW_ENDPOINT'] ?? kDefaultEndpoint; 44 | } 45 | 46 | try { 47 | switch (parsedArguments.command?.name) { 48 | case 'export-code': 49 | await exportCode( 50 | token: token, 51 | endpoint: endpoint, 52 | projectId: project, 53 | destinationPath: parsedArguments.command!['dest'], 54 | includeAssets: parsedArguments.command!['include-assets'], 55 | branchName: parsedArguments.command!['branch-name'], 56 | commitHash: parsedArguments.command!['commit-hash'], 57 | unzipToParentFolder: parsedArguments.command!['parent-folder'], 58 | fix: parsedArguments.command!['fix'], 59 | exportAsModule: parsedArguments.command!['as-module'], 60 | exportAsDebug: parsedArguments.command!['as-debug'], 61 | environmentName: parsedArguments.command!['project-environment'], 62 | ); 63 | break; 64 | case 'deploy-firebase': 65 | await firebaseDeploy( 66 | token: token, 67 | projectId: project, 68 | appendRules: parsedArguments.command!['append-rules'], 69 | endpoint: endpoint, 70 | ); 71 | break; 72 | default: 73 | } 74 | } catch (e) { 75 | stderr.write('Error running the application: $e\n'); 76 | exit(1); 77 | } 78 | } 79 | 80 | ArgResults _parseArgs(List args) { 81 | final exportCodeCommandParser = ArgParser() 82 | ..addOption('project', abbr: 'p', help: 'Project id') 83 | ..addOption( 84 | 'dest', 85 | abbr: 'd', 86 | help: 'Destination directory', 87 | defaultsTo: '.', 88 | ) 89 | ..addOption( 90 | 'branch-name', 91 | abbr: 'b', 92 | help: '(Optional) Specify a branch name', 93 | ) 94 | ..addOption( 95 | 'commit-hash', 96 | abbr: 'c', 97 | help: '(Optional) Specify a commit hash', 98 | ) 99 | ..addFlag( 100 | 'include-assets', 101 | negatable: true, 102 | help: 'Include assets. By default, assets are not included.\n' 103 | 'We recommend setting this flag only when calling this command ' 104 | 'for the first time or after updating assets.\n' 105 | 'Downloading code without assets is typically much faster.', 106 | defaultsTo: false, 107 | ) 108 | ..addFlag( 109 | 'fix', 110 | negatable: true, 111 | help: 'Run "dart fix" on the downloaded code.', 112 | defaultsTo: false, 113 | ) 114 | ..addFlag( 115 | 'parent-folder', 116 | negatable: true, 117 | help: 'Download into a sub-folder. By default, project is downloaded \n' 118 | 'into a folder named .\nSetting this flag to false will ' 119 | 'download all project code directly into the specified directory, ' 120 | 'or the current directory if --dest is not set.', 121 | defaultsTo: true, 122 | ) 123 | ..addFlag( 124 | 'as-module', 125 | negatable: true, 126 | help: 'Generate the project as a Flutter module.', 127 | defaultsTo: false, 128 | ) 129 | ..addFlag( 130 | 'as-debug', 131 | negatable: true, 132 | help: 'Generate the project with debug logging to be able to use ' 133 | 'FlutterFlow Debug Panel inside the DevTools.', 134 | defaultsTo: false, 135 | ) 136 | ..addOption( 137 | 'project-environment', 138 | help: '(Optional) Specify a project environment name.', 139 | ); 140 | 141 | final firebaseDeployCommandParser = ArgParser() 142 | ..addOption('project', abbr: 'p', help: 'Project id') 143 | ..addFlag( 144 | 'append-rules', 145 | abbr: 'a', 146 | help: 'Append to rules, instead of overwriting them.', 147 | defaultsTo: false, 148 | ); 149 | 150 | final parser = ArgParser() 151 | ..addOption('endpoint', abbr: 'e', help: 'Endpoint', hide: true) 152 | ..addOption('environment', help: 'Environment', hide: true) 153 | ..addOption('token', abbr: 't', help: 'API Token') 154 | ..addFlag('help', negatable: false, abbr: 'h', help: 'Help') 155 | ..addCommand('export-code', exportCodeCommandParser) 156 | ..addCommand('deploy-firebase', firebaseDeployCommandParser); 157 | 158 | late ArgResults parsed; 159 | try { 160 | parsed = parser.parse(args); 161 | } catch (e) { 162 | stderr.write('$e\n'); 163 | stderr.write(parser.usage); 164 | exit(1); 165 | } 166 | 167 | if (parsed['help'] ?? false) { 168 | print(parser.usage); 169 | if (parsed.command != null) { 170 | print(parser.commands[parsed.command!.name]!.usage); 171 | } else { 172 | print('Available commands: ${parser.commands.keys.join(', ')}.'); 173 | } 174 | exit(0); 175 | } 176 | 177 | if (parsed.command == null) { 178 | print(parser.usage); 179 | print('Available commands: ${parser.commands.keys.join(', ')}.'); 180 | exit(1); 181 | } 182 | 183 | return parsed; 184 | } 185 | 186 | void main(List args) async { 187 | await appMain(args); 188 | } 189 | -------------------------------------------------------------------------------- /test/integration_test.dart: -------------------------------------------------------------------------------- 1 | import '../bin/flutterflow_cli.dart'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:path/path.dart' as p; 5 | 6 | import 'dart:io'; 7 | 8 | String kProjectId = 'app-with-assets-and-custom-fonts-qxwg6o'; 9 | String kToken = Platform.environment['FF_TESTER_TOKEN'] ?? 'not-set'; 10 | 11 | Future buildProject(String project) async { 12 | var result = await Process.run('flutter', ['build', 'web'], 13 | workingDirectory: p.normalize(project), runInShell: true); 14 | 15 | return result.exitCode == 0; 16 | } 17 | 18 | bool checkAssets(String project) { 19 | var assets = [ 20 | 'assets/fonts/JetBrainsMonoNerdFont-Bold.ttf', 21 | 'assets/fonts/JetBrainsMonoNerdFont-Italic.ttf', 22 | 'assets/fonts/JetBrainsMonoNerdFont-Regular.ttf', 23 | 'assets/fonts/MartianMonoNerdFont-Bold.ttf', 24 | 'assets/fonts/MartianMonoNerdFont-Medium.ttf', 25 | 'assets/fonts/MartianMonoNerdFont-Regular.ttf', 26 | 'assets/fonts/ProFontIIxNerdFont-Regular.ttf', 27 | 'assets/fonts/ProFontIIxNerdFontMono-Regular.ttf', 28 | 'assets/fonts/ProFontIIxNerdFontPropo-Regular.ttf', 29 | 'assets/images/6740ae761553efad6aa2a5d4.png', 30 | 'assets/images/6740ae76c0adf77347645294.png', 31 | 'assets/images/6740af8ffbd4c3414fcf5728.png', 32 | 'assets/images/6740aff03c7e45b220ed9775.png', 33 | 'assets/images/6740aff0d10e0295e5fe33e6.png', 34 | 'assets/images/6740b3cc6d3624484183520b.png', 35 | 'assets/images/6740b3cca8e014dee9325b5d.png', 36 | 'assets/images/6740b4b494d7239248fa491e.png', 37 | 'assets/images/6740b4b5c0adf773476a5452.png', 38 | 'assets/images/6740b4b5cf7b4fdf95795e2c.png', 39 | 'assets/images/6740bca9c28f22a68495d368.png', 40 | 'assets/images/6740bca9ed26c9a34b1ab1ce.png', 41 | 'assets/images/6744ab4d50d5a3dad758fa39.png', 42 | 'assets/images/6785366c215b774f00c041a3.png', 43 | 'assets/images/6785366c3e83b0072fdc8ef4.png', 44 | 'assets/images/6785366c77c17f02779e160c.png', 45 | 'assets/images/67895d616be6f220ee4ec9c3.png', 46 | 'assets/images/67895d6177fc072b5e166fd1.png', 47 | 'assets/images/67895d61a7af8d11cb9aa957.png', 48 | ]; 49 | 50 | for (var asset in assets) { 51 | if (fileExists('$project/$asset') == false) { 52 | return false; 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | 59 | bool fileExists(path) { 60 | return File(p.normalize(path)).existsSync(); 61 | } 62 | 63 | bool fileContains(path, data) { 64 | return File(p.normalize(path)).readAsStringSync().contains(data); 65 | } 66 | 67 | void main() { 68 | group('export-code', () { 69 | test('default parameters', () async { 70 | final project = 'export/app_with_assets_and_custom_fonts'; 71 | 72 | await appMain([ 73 | 'export-code', 74 | '--project', 75 | kProjectId, 76 | '--token', 77 | kToken, 78 | '-d', 79 | 'export', 80 | ]); 81 | 82 | // Missing assets 83 | expect(checkAssets(project), false); 84 | 85 | final buildResult = await buildProject(project); 86 | expect(buildResult, false); 87 | }); 88 | 89 | test('fix code', () async { 90 | final project = 'export/fix_code'; 91 | 92 | await appMain([ 93 | 'export-code', 94 | '--no-parent-folder', 95 | '--include-assets', 96 | '--project', 97 | kProjectId, 98 | '--token', 99 | kToken, 100 | '-d', 101 | p.normalize(project), 102 | '--fix', 103 | ]); 104 | 105 | // Fix will add 'const' to a lot of stuff :-) 106 | expect( 107 | fileContains( 108 | '$project/lib/main.dart', 'localizationsDelegates: const ['), 109 | true); 110 | 111 | expect(checkAssets(project), true); 112 | 113 | final buildResult = await buildProject(project); 114 | expect(buildResult, true); 115 | }); 116 | 117 | test('branch', () async { 118 | final project = 'export/branch'; 119 | 120 | await appMain([ 121 | 'export-code', 122 | '--no-parent-folder', 123 | '--include-assets', 124 | '--project', 125 | kProjectId, 126 | '--token', 127 | kToken, 128 | '-d', 129 | p.normalize(project), 130 | '--branch-name', 131 | 'TestBranch', 132 | ]); 133 | 134 | expect( 135 | fileExists( 136 | '$project/lib/pages/page_only_on_this_branch/page_only_on_this_branch_widget.dart'), 137 | true); 138 | 139 | expect(checkAssets(project), true); 140 | 141 | final buildResult = await buildProject(project); 142 | expect(buildResult, true); 143 | }); 144 | 145 | test('commit', () async { 146 | final project = 'export/commit'; 147 | 148 | await appMain([ 149 | 'export-code', 150 | '--no-parent-folder', 151 | '--include-assets', 152 | '--project', 153 | kProjectId, 154 | '--token', 155 | kToken, 156 | '-d', 157 | p.normalize(project), 158 | '--commit-hash', 159 | '0jfsCktnCmIcNp02q3yW', 160 | ]); 161 | 162 | expect( 163 | fileExists( 164 | '$project/lib/pages/page_only_on_this_commit/page_only_on_this_commit_widget.dart'), 165 | true); 166 | 167 | expect(checkAssets(project), true); 168 | 169 | final buildResult = await buildProject(project); 170 | expect(buildResult, true); 171 | }); 172 | 173 | test('debug', () async { 174 | final project = 'export/debug'; 175 | 176 | await appMain([ 177 | 'export-code', 178 | '--no-parent-folder', 179 | '--include-assets', 180 | '--project', 181 | kProjectId, 182 | '--token', 183 | kToken, 184 | '-d', 185 | p.normalize(project), 186 | '--as-debug', 187 | ]); 188 | 189 | // Debug instrumentation added by the flag 190 | expect(fileContains('$project/lib/main.dart', 'debugLogGlobalProperty'), 191 | true); 192 | 193 | expect(checkAssets(project), true); 194 | 195 | final buildResult = await buildProject(project); 196 | expect(buildResult, true); 197 | }); 198 | 199 | test('module', () async { 200 | final project = 'export/module'; 201 | 202 | await appMain([ 203 | 'export-code', 204 | '--no-parent-folder', 205 | '--include-assets', 206 | '--project', 207 | kProjectId, 208 | '--token', 209 | kToken, 210 | '-d', 211 | p.normalize(project), 212 | '--as-module', 213 | ]); 214 | 215 | expect(fileContains('$project/pubspec.yaml', 'module:'), true); 216 | 217 | expect(checkAssets(project), true); 218 | 219 | final buildResult = await buildProject(project); 220 | expect(buildResult, true); 221 | }); 222 | 223 | test('environment', () async { 224 | final project = 'export/environment'; 225 | 226 | await appMain([ 227 | 'export-code', 228 | '--no-parent-folder', 229 | '--include-assets', 230 | '--project', 231 | kProjectId, 232 | '--token', 233 | kToken, 234 | '-d', 235 | p.normalize(project), 236 | '--project-environment', 237 | 'Development', 238 | ]); 239 | 240 | expect( 241 | fileContains('$project/assets/environment_values/environment.json', 242 | '"foobar": "barfoo"'), 243 | true); 244 | 245 | expect(checkAssets(project), true); 246 | 247 | final buildResult = await buildProject(project); 248 | expect(buildResult, true); 249 | }); 250 | }, timeout: Timeout(Duration(minutes: 30))); 251 | } 252 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "82.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "13c1e6c6fd460522ea840abec3f677cc226f5fec7872c04ad7b425517ccf54f7" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "7.4.4" 20 | archive: 21 | dependency: "direct main" 22 | description: 23 | name: archive 24 | sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "4.0.2" 28 | args: 29 | dependency: "direct main" 30 | description: 31 | name: args 32 | sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.6.0" 36 | async: 37 | dependency: transitive 38 | description: 39 | name: async 40 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.12.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.2" 52 | collection: 53 | dependency: transitive 54 | description: 55 | name: collection 56 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.19.1" 60 | convert: 61 | dependency: transitive 62 | description: 63 | name: convert 64 | sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "3.1.2" 68 | coverage: 69 | dependency: transitive 70 | description: 71 | name: coverage 72 | sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.11.1" 76 | crypto: 77 | dependency: transitive 78 | description: 79 | name: crypto 80 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "3.0.6" 84 | ffi: 85 | dependency: transitive 86 | description: 87 | name: ffi 88 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.1.3" 92 | file: 93 | dependency: transitive 94 | description: 95 | name: file 96 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "7.0.1" 100 | frontend_server_client: 101 | dependency: transitive 102 | description: 103 | name: frontend_server_client 104 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "4.0.0" 108 | glob: 109 | dependency: "direct main" 110 | description: 111 | name: glob 112 | sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "2.1.3" 116 | http: 117 | dependency: "direct main" 118 | description: 119 | name: http 120 | sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.3.0" 124 | http_multi_server: 125 | dependency: transitive 126 | description: 127 | name: http_multi_server 128 | sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "3.2.2" 132 | http_parser: 133 | dependency: transitive 134 | description: 135 | name: http_parser 136 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "4.1.2" 140 | io: 141 | dependency: transitive 142 | description: 143 | name: io 144 | sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.0.5" 148 | js: 149 | dependency: transitive 150 | description: 151 | name: js 152 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "0.7.1" 156 | lints: 157 | dependency: "direct dev" 158 | description: 159 | name: lints 160 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "3.0.0" 164 | logging: 165 | dependency: transitive 166 | description: 167 | name: logging 168 | sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.3.0" 172 | matcher: 173 | dependency: transitive 174 | description: 175 | name: matcher 176 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "0.12.17" 180 | meta: 181 | dependency: transitive 182 | description: 183 | name: meta 184 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.16.0" 188 | mime: 189 | dependency: transitive 190 | description: 191 | name: mime 192 | sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.0.0" 196 | node_preamble: 197 | dependency: transitive 198 | description: 199 | name: node_preamble 200 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "2.0.2" 204 | package_config: 205 | dependency: transitive 206 | description: 207 | name: package_config 208 | sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "2.1.1" 212 | path: 213 | dependency: "direct main" 214 | description: 215 | name: path 216 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.9.1" 220 | pool: 221 | dependency: transitive 222 | description: 223 | name: pool 224 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "1.5.1" 228 | posix: 229 | dependency: transitive 230 | description: 231 | name: posix 232 | sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "6.0.1" 236 | pub_semver: 237 | dependency: transitive 238 | description: 239 | name: pub_semver 240 | sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "2.1.5" 244 | shelf: 245 | dependency: transitive 246 | description: 247 | name: shelf 248 | sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "1.4.2" 252 | shelf_packages_handler: 253 | dependency: transitive 254 | description: 255 | name: shelf_packages_handler 256 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "3.0.2" 260 | shelf_static: 261 | dependency: transitive 262 | description: 263 | name: shelf_static 264 | sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "1.1.3" 268 | shelf_web_socket: 269 | dependency: transitive 270 | description: 271 | name: shelf_web_socket 272 | sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "2.0.1" 276 | source_map_stack_trace: 277 | dependency: transitive 278 | description: 279 | name: source_map_stack_trace 280 | sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "2.1.2" 284 | source_maps: 285 | dependency: transitive 286 | description: 287 | name: source_maps 288 | sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "0.10.13" 292 | source_span: 293 | dependency: transitive 294 | description: 295 | name: source_span 296 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "1.10.1" 300 | stack_trace: 301 | dependency: transitive 302 | description: 303 | name: stack_trace 304 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.12.1" 308 | stream_channel: 309 | dependency: transitive 310 | description: 311 | name: stream_channel 312 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "2.1.4" 316 | string_scanner: 317 | dependency: transitive 318 | description: 319 | name: string_scanner 320 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "1.4.1" 324 | term_glyph: 325 | dependency: transitive 326 | description: 327 | name: term_glyph 328 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "1.2.2" 332 | test: 333 | dependency: "direct dev" 334 | description: 335 | name: test 336 | sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d" 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "1.25.14" 340 | test_api: 341 | dependency: transitive 342 | description: 343 | name: test_api 344 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "0.7.4" 348 | test_core: 349 | dependency: transitive 350 | description: 351 | name: test_core 352 | sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "0.6.8" 356 | typed_data: 357 | dependency: transitive 358 | description: 359 | name: typed_data 360 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "1.4.0" 364 | vm_service: 365 | dependency: transitive 366 | description: 367 | name: vm_service 368 | sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "15.0.0" 372 | watcher: 373 | dependency: transitive 374 | description: 375 | name: watcher 376 | sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "1.1.1" 380 | web: 381 | dependency: transitive 382 | description: 383 | name: web 384 | sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "1.1.0" 388 | web_socket: 389 | dependency: transitive 390 | description: 391 | name: web_socket 392 | sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "0.1.6" 396 | web_socket_channel: 397 | dependency: transitive 398 | description: 399 | name: web_socket_channel 400 | sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "3.0.2" 404 | webkit_inspection_protocol: 405 | dependency: transitive 406 | description: 407 | name: webkit_inspection_protocol 408 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "1.2.1" 412 | yaml: 413 | dependency: transitive 414 | description: 415 | name: yaml 416 | sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce 417 | url: "https://pub.dev" 418 | source: hosted 419 | version: "3.1.3" 420 | sdks: 421 | dart: ">=3.5.0 <4.0.0" 422 | -------------------------------------------------------------------------------- /lib/src/flutterflow_api_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:archive/archive_io.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:path/path.dart' as path_util; 7 | 8 | import 'flutterflow_ignore.dart'; 9 | 10 | const kDefaultEndpoint = 'https://api.flutterflow.io/v2'; 11 | 12 | /// The `FlutterFlowApi` class provides methods for exporting code from a 13 | /// FlutterFlow project. 14 | class FlutterFlowApi { 15 | /// Exports the code from a FlutterFlow project. 16 | /// 17 | /// * [token] is the FlutterFlow API token for accessing the project. 18 | /// * [projectId] is the ID of the project to export. 19 | /// * [destinationPath] is the path where the exported code will be saved. 20 | /// * [includeAssets] flag indicates whether to include project assets 21 | /// in the export. 22 | /// * [endpoint] is the API endpoint to use for exporting the code. 23 | /// * [branchName] is the name of the branch to export from (optional). 24 | /// * [unzipToParentFolder] flag indicates whether to unzip the exported code 25 | /// to the parent folder. 26 | /// * [fix] flag indicates whether to fix any issues in the exported code. 27 | /// * [exportAsModule] flag indicates whether to export the code as a module. 28 | /// * [format] flag indicates whether to format the exported code. 29 | /// * [exportAsDebug] flag indicates whether to export the code as debug for 30 | /// local run. 31 | /// * [environmentName] is the name of the environment to export the code for. 32 | /// 33 | /// Returns a [Future] that completes with the path to the exported code, or 34 | /// throws an error if the export fails. 35 | static Future export({ 36 | required String token, 37 | required String projectId, 38 | required String destinationPath, 39 | required bool includeAssets, 40 | String endpoint = kDefaultEndpoint, 41 | String? branchName, 42 | String? environmentName, 43 | String? commitHash, 44 | bool unzipToParentFolder = false, 45 | bool fix = false, 46 | bool exportAsModule = false, 47 | bool format = true, 48 | bool exportAsDebug = false, 49 | }) => 50 | exportCode( 51 | token: token, 52 | endpoint: endpoint, 53 | projectId: projectId, 54 | destinationPath: destinationPath, 55 | includeAssets: includeAssets, 56 | branchName: branchName, 57 | commitHash: commitHash, 58 | unzipToParentFolder: unzipToParentFolder, 59 | fix: fix, 60 | exportAsModule: exportAsModule, 61 | format: format, 62 | exportAsDebug: exportAsDebug, 63 | ); 64 | } 65 | 66 | Future exportCode({ 67 | required String token, 68 | required String endpoint, 69 | required String projectId, 70 | required String destinationPath, 71 | required bool includeAssets, 72 | required bool unzipToParentFolder, 73 | required bool fix, 74 | required bool exportAsModule, 75 | bool format = true, 76 | String? branchName, 77 | String? environmentName, 78 | String? commitHash, 79 | bool exportAsDebug = false, 80 | }) async { 81 | stderr.write('Downloading code with the FlutterFlow CLI...\n'); 82 | stderr.write('You are exporting project $projectId.\n'); 83 | stderr.write( 84 | '${branchName != null ? 'Branch: $branchName ' : ''}${environmentName != null ? 'Environment: $environmentName ' : ''}${commitHash != null ? 'Commit: $commitHash' : ''}\n'); 85 | if (exportAsDebug && exportAsModule) { 86 | throw 'Cannot export as module and debug at the same time.'; 87 | } 88 | final endpointUrl = Uri.parse(endpoint); 89 | final client = http.Client(); 90 | String? folderName; 91 | try { 92 | final result = await _callExport( 93 | client: client, 94 | token: token, 95 | endpoint: endpointUrl, 96 | projectId: projectId, 97 | branchName: branchName, 98 | environmentName: environmentName, 99 | commitHash: commitHash, 100 | exportAsModule: exportAsModule, 101 | includeAssets: includeAssets, 102 | format: format, 103 | exportAsDebug: exportAsDebug, 104 | ); 105 | // Download actual code 106 | final projectZipBytes = base64Decode(result['project_zip']); 107 | final projectFolder = ZipDecoder().decodeBytes(projectZipBytes); 108 | 109 | extractArchiveTo(projectFolder, destinationPath, unzipToParentFolder); 110 | 111 | final postCodeGenerationFutures = [ 112 | if (fix) 113 | _runFix( 114 | destinationPath: destinationPath, 115 | projectFolder: projectFolder, 116 | unzipToParentFolder: unzipToParentFolder, 117 | ), 118 | if (includeAssets) 119 | _downloadAssets( 120 | client: client, 121 | destinationPath: destinationPath, 122 | assetDescriptions: result['assets'], 123 | unzipToParentFolder: unzipToParentFolder, 124 | ), 125 | ]; 126 | 127 | if (postCodeGenerationFutures.isNotEmpty) { 128 | await Future.wait(postCodeGenerationFutures); 129 | } 130 | 131 | var fileName = projectFolder.first.name; 132 | folderName = fileName.substring(0, fileName.indexOf('/')); 133 | } finally { 134 | client.close(); 135 | } 136 | stderr.write('All done!\n'); 137 | return folderName; 138 | } 139 | 140 | // Extract files to the specified directory without a project-named 141 | // parent folder. 142 | void extractArchiveTo( 143 | Archive projectFolder, String destinationPath, bool unzipToParentFolder) { 144 | final ignore = FlutterFlowIgnore(path: destinationPath); 145 | 146 | for (final file in projectFolder.files) { 147 | if (file.isFile) { 148 | final relativeFilename = 149 | path_util.joinAll(path_util.split(file.name).sublist(1)); 150 | 151 | // Found on .flutterflowignore, move on. 152 | if (ignore.matches(unzipToParentFolder ? file.name : relativeFilename)) { 153 | stderr.write('Ignoring $relativeFilename, file remained unchanged.\n'); 154 | continue; 155 | } 156 | 157 | // Remove the `` prefix from paths if needed. 158 | final path = path_util.join( 159 | destinationPath, unzipToParentFolder ? file.name : relativeFilename); 160 | 161 | final fileOut = File(path); 162 | fileOut.createSync(recursive: true); 163 | fileOut.writeAsBytesSync(file.content as List); 164 | } 165 | } 166 | } 167 | 168 | Future _callExport({ 169 | required final http.Client client, 170 | required String token, 171 | required Uri endpoint, 172 | required String projectId, 173 | String? branchName, 174 | String? environmentName, 175 | String? commitHash, 176 | required bool exportAsModule, 177 | required bool includeAssets, 178 | required bool format, 179 | required bool exportAsDebug, 180 | }) async { 181 | final body = jsonEncode({ 182 | 'project_id': projectId, 183 | if (branchName != null) 'branch_name': branchName, 184 | if (environmentName != null) 'environment_name': environmentName, 185 | if (commitHash != null) 'commit': {'path': 'commits/$commitHash'}, 186 | 'export_as_module': exportAsModule, 187 | 'include_assets_map': includeAssets, 188 | 'format': format, 189 | 'export_as_debug': exportAsDebug, 190 | }); 191 | return await _callEndpoint( 192 | client: client, 193 | token: token, 194 | url: Uri.parse('$endpoint/exportCode'), 195 | body: body, 196 | ); 197 | } 198 | 199 | Future _callEndpoint({ 200 | required final http.Client client, 201 | required String token, 202 | required Uri url, 203 | required String body, 204 | }) async { 205 | final response = await client.post( 206 | url, 207 | body: body, 208 | headers: { 209 | 'Content-Type': 'application/json', 210 | 'Authorization': 'Bearer $token', 211 | }, 212 | ); 213 | 214 | if (response.statusCode == 429) { 215 | throw 'Too many requests. Please try again later.'; 216 | } 217 | 218 | if (response.statusCode != 200) { 219 | stderr.write('Unexpected error from the server.\n'); 220 | stderr.write('Status: ${response.statusCode}\n'); 221 | stderr.write('Body: ${response.body}\n'); 222 | throw ('Unexpected error from the server.'); 223 | } 224 | 225 | final parsedResponse = jsonDecode(response.body); 226 | final success = parsedResponse['success']; 227 | if (success == null || !success) { 228 | if (parsedResponse['reason'] != null && 229 | parsedResponse['reason'].isNotEmpty) { 230 | stderr.write('Error: ${parsedResponse['reason']}.\n'); 231 | throw 'Error: ${parsedResponse['reason']}.'; 232 | } else { 233 | stderr.write('Unexpected server error.\n'); 234 | throw 'Unexpected server error.'; 235 | } 236 | } 237 | 238 | return parsedResponse['value']; 239 | } 240 | 241 | // TODO: limit the number of parallel downloads. 242 | Future _downloadAssets({ 243 | required final http.Client client, 244 | required String destinationPath, 245 | required List assetDescriptions, 246 | required unzipToParentFolder, 247 | }) async { 248 | final futures = assetDescriptions.map((assetDescription) async { 249 | String path = assetDescription['path']; 250 | 251 | if (!unzipToParentFolder) { 252 | path = path_util.joinAll( 253 | path_util.split(path).sublist(1), 254 | ); 255 | } 256 | final url = assetDescription['url']; 257 | final fileDest = path_util.join(destinationPath, path); 258 | try { 259 | final response = await client.get(Uri.parse(url)); 260 | if (response.statusCode >= 200 && response.statusCode < 300) { 261 | final file = File(fileDest); 262 | await file.parent.create(recursive: true); 263 | await file.writeAsBytes(response.bodyBytes); 264 | } else { 265 | stderr.write('Error downloading asset $path. This is probably fine.\n'); 266 | } 267 | } catch (_) { 268 | stderr.write('Error downloading asset $path. This is probably fine.\n'); 269 | } 270 | }); 271 | stderr.write('Downloading assets...\n'); 272 | await Future.wait(futures); 273 | } 274 | 275 | Future _runFix({ 276 | required String destinationPath, 277 | required Archive projectFolder, 278 | required unzipToParentFolder, 279 | }) async { 280 | try { 281 | if (projectFolder.isEmpty) { 282 | return; 283 | } 284 | final firstFilePath = projectFolder.files.first.name; 285 | final directory = path_util.split(firstFilePath).first; 286 | 287 | final workingDirectory = unzipToParentFolder 288 | ? path_util.join(destinationPath, directory) 289 | : destinationPath; 290 | stderr.write('Running flutter pub get...\n'); 291 | final pubGetResult = await Process.run( 292 | 'flutter', 293 | ['pub', 'get'], 294 | workingDirectory: workingDirectory, 295 | runInShell: true, 296 | stdoutEncoding: utf8, 297 | stderrEncoding: utf8, 298 | ); 299 | if (pubGetResult.exitCode != 0) { 300 | stderr.write( 301 | '"flutter pub get" failed with code ${pubGetResult.exitCode}, stderr:\n${pubGetResult.stderr}\n'); 302 | return; 303 | } 304 | stderr.write('Running dart fix...\n'); 305 | final fixDirectory = unzipToParentFolder ? directory : ''; 306 | final dartFixResult = await Process.run( 307 | 'dart', 308 | ['fix', '--apply', fixDirectory], 309 | workingDirectory: destinationPath, 310 | runInShell: true, 311 | stdoutEncoding: utf8, 312 | stderrEncoding: utf8, 313 | ); 314 | if (dartFixResult.exitCode != 0) { 315 | stderr.write( 316 | '"dart fix" failed with code ${dartFixResult.exitCode}, stderr:\n${dartFixResult.stderr}\n'); 317 | } 318 | } catch (e) { 319 | stderr.write('Error running "dart fix": $e\n'); 320 | } 321 | } 322 | 323 | Future firebaseDeploy({ 324 | required String token, 325 | required String projectId, 326 | bool appendRules = false, 327 | String endpoint = kDefaultEndpoint, 328 | }) async { 329 | final endpointUrl = Uri.parse(endpoint); 330 | final body = jsonEncode({ 331 | 'project_id': projectId, 332 | 'append_rules': appendRules, 333 | }); 334 | final result = await _callEndpoint( 335 | client: http.Client(), 336 | token: token, 337 | url: Uri.https( 338 | endpointUrl.host, '${endpointUrl.path}/exportFirebaseDeployCode'), 339 | body: body, 340 | ); 341 | 342 | // Download actual code 343 | final projectZipBytes = base64Decode(result['firebase_zip']); 344 | final firebaseProjectId = result['firebase_project_id']; 345 | final projectFolder = ZipDecoder().decodeBytes(projectZipBytes); 346 | Directory? tmpFolder; 347 | 348 | try { 349 | tmpFolder = 350 | Directory.systemTemp.createTempSync('${projectId}_$firebaseProjectId'); 351 | extractArchiveTo(projectFolder, tmpFolder.path, false); 352 | final firebaseDir = '${tmpFolder.path}/firebase'; 353 | 354 | // Install required modules for deployment. 355 | await Process.run( 356 | 'npm', 357 | ['install'], 358 | workingDirectory: '$firebaseDir/functions', 359 | runInShell: true, 360 | stdoutEncoding: utf8, 361 | stderrEncoding: utf8, 362 | ); 363 | 364 | // This directory only exists if there were custom cloud functions. 365 | if (Directory('$firebaseDir/custom_cloud_functions').existsSync()) { 366 | await Process.run( 367 | 'npm', 368 | ['install'], 369 | workingDirectory: '$firebaseDir/custom_cloud_functions', 370 | runInShell: true, 371 | stdoutEncoding: utf8, 372 | stderrEncoding: utf8, 373 | ); 374 | } 375 | 376 | stderr.write('Initializing firebase...\n'); 377 | await Process.run( 378 | 'firebase', 379 | ['use', firebaseProjectId], 380 | workingDirectory: firebaseDir, 381 | runInShell: true, 382 | ); 383 | final initHostingProcess = await Process.start( 384 | 'firebase', 385 | ['init', 'hosting'], 386 | workingDirectory: firebaseDir, 387 | runInShell: true, 388 | ); 389 | final initHostingInputStream = Stream.periodic( 390 | Duration(milliseconds: 100), 391 | (count) => utf8.encode('\n'), 392 | ); 393 | initHostingProcess.stdin.addStream(initHostingInputStream); 394 | // Make sure hosting is initialized before deploying. 395 | await initHostingProcess.exitCode; 396 | 397 | final deployProcess = await Process.start( 398 | 'firebase', 399 | ['deploy', '--project', firebaseProjectId], 400 | workingDirectory: firebaseDir, 401 | runInShell: true, 402 | ); 403 | // There may be a need for the user to interactively provide inputs. 404 | deployProcess.stdout.transform(utf8.decoder).forEach(print); 405 | deployProcess.stdin.addStream(stdin); 406 | final exitCode = await deployProcess.exitCode; 407 | if (exitCode != 0) { 408 | stderr.write('Failed to deploy to Firebase.\n'); 409 | } 410 | } finally { 411 | tmpFolder?.deleteSync(recursive: true); 412 | } 413 | } 414 | --------------------------------------------------------------------------------