Skip to content

Commit d2ed1eb

Browse files
authored
Add .flutterflowignore support (#25)
* Merge the two zip extraction functions Use the same code path, because regardless if parent folder is required or not, they will both take .flutterflowignore into account. * Add .flutterflowignore support This file allows for users to create a list of files to be ignored when exporting the project code (works a bit like .gitignore). Files matching the globbing rules listed at .flutterflowignore are going to be ignored. This might break the project if changes in these files are needed to compile the project, so it should be used with extreme caution. This functionality was previously achieved with: https://github.com/krabhishek/flutterflow-filtered-pull * Create a simple CI workflow Make sure that it builds and runs on all supported platforms. * Add unit tests to newly written code
1 parent 1681b40 commit d2ed1eb

File tree

7 files changed

+207
-18
lines changed

7 files changed

+207
-18
lines changed

.github/workflows/main.yml

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: FlutterFlow
2+
on: push
3+
4+
env:
5+
# Keep this in sync with the version used by FlutterFlow.
6+
DART_VERSION: 3.4.3
7+
8+
jobs:
9+
check:
10+
runs-on: ubuntu-24.04
11+
timeout-minutes: 10
12+
13+
steps:
14+
- name: Clone repository
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Dart
18+
uses: dart-lang/setup-dart@v1
19+
with:
20+
sdk: ${{ env.DART_VERSION }}
21+
22+
- name: Install dependencies
23+
run: |
24+
dart pub get
25+
26+
- name: Analyze code
27+
run: |
28+
dart analyze
29+
30+
- name: Format code
31+
run: |
32+
dart format . --set-exit-if-changed
33+
34+
build_and_test:
35+
runs-on: ${{ matrix.os }}
36+
needs: check
37+
timeout-minutes: 10
38+
39+
strategy:
40+
matrix:
41+
os: [ubuntu-24.04, windows-2022, macos-14]
42+
43+
steps:
44+
- name: Clone repository
45+
uses: actions/checkout@v4
46+
47+
- name: Setup Dart
48+
uses: dart-lang/setup-dart@v1
49+
with:
50+
sdk: ${{ env.DART_VERSION }}
51+
52+
- name: Install dependencies
53+
run: |
54+
dart pub get
55+
56+
- name: Build
57+
run: |
58+
dart compile exe bin/flutterflow_cli.dart
59+
60+
- name: Run
61+
run: |
62+
dart run bin/flutterflow_cli.dart -h
63+
64+
- name: Test
65+
run: |
66+
dart test

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ API access is available only to users with active subscriptions. Visit https://a
1919
* Instead of passing `--token` you can set `FLUTTERFLOW_API_TOKEN` environment variable.
2020
* Instead of passing `--project` you can set `FLUTTERFLOW_PROJECT` environment variable.
2121

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+
2225
### Flags
2326

2427
| Flag | Abbreviation | Usage |

lib/src/flutterflow_api_client.dart

+19-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import 'package:archive/archive_io.dart';
55
import 'package:http/http.dart' as http;
66
import 'package:path/path.dart' as path_util;
77

8+
import 'flutterflow_ignore.dart';
9+
810
const kDefaultEndpoint = 'https://api.flutterflow.io/v1';
911

1012
/// The `FlutterFlowApi` class provides methods for exporting code from a
@@ -104,11 +106,7 @@ Future<String?> exportCode({
104106
final projectZipBytes = base64Decode(result['project_zip']);
105107
final projectFolder = ZipDecoder().decodeBytes(projectZipBytes);
106108

107-
if (unzipToParentFolder) {
108-
extractArchiveToDisk(projectFolder, destinationPath);
109-
} else {
110-
extractArchiveToCurrentDirectory(projectFolder, destinationPath);
111-
}
109+
extractArchiveTo(projectFolder, destinationPath, unzipToParentFolder);
112110

113111
var fileName = projectFolder.first.name;
114112
folderName = fileName.substring(0, fileName.indexOf(Platform.pathSeparator));
@@ -141,25 +139,28 @@ Future<String?> exportCode({
141139

142140
// Extract files to the specified directory without a project-named
143141
// parent folder.
144-
void extractArchiveToCurrentDirectory(
145-
Archive projectFolder,
146-
String destinationPath,
147-
) {
142+
void extractArchiveTo(
143+
Archive projectFolder, String destinationPath, bool unzipToParentFolder) {
144+
final ignore = FlutterFlowIgnore(path: destinationPath);
145+
148146
for (final file in projectFolder.files) {
149147
if (file.isFile) {
150-
final data = file.content as List<int>;
151-
final filename = file.name;
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+
}
152156

153-
// Remove the `<project>` prefix from paths.
157+
// Remove the `<project>` prefix from paths if needed.
154158
final path = path_util.join(
155-
destinationPath,
156-
path_util.joinAll(
157-
path_util.split(filename).sublist(1),
158-
));
159+
destinationPath, unzipToParentFolder ? file.name : relativeFilename);
159160

160161
final fileOut = File(path);
161162
fileOut.createSync(recursive: true);
162-
fileOut.writeAsBytesSync(data);
163+
fileOut.writeAsBytesSync(file.content as List<int>);
163164
}
164165
}
165166
}
@@ -347,7 +348,7 @@ Future firebaseDeploy({
347348
try {
348349
tmpFolder =
349350
Directory.systemTemp.createTempSync('${projectId}_$firebaseProjectId');
350-
extractArchiveToCurrentDirectory(projectFolder, tmpFolder.path);
351+
extractArchiveTo(projectFolder, tmpFolder.path, false);
351352
final firebaseDir = '${tmpFolder.path}/firebase';
352353

353354
// Install required modules for deployment.

lib/src/flutterflow_ignore.dart

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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<Glob> _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+
}

pubspec.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ environment:
1111
dependencies:
1212
archive: ^3.3.5
1313
args: ^2.3.0
14+
glob: ^2.1.0
1415
http: ^1.1.0
1516
path: ^1.8.0
1617

1718
dev_dependencies:
1819
lints: ^3.0.0
20+
test: ^1.25.8
1921

2022
executables:
2123
flutterflow: flutterflow_cli

test/fixtures/.flutterflowignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+

test/flutterflow_ignore_test.dart

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
}

0 commit comments

Comments
 (0)