feat: migrate to null-safety (#24)

This commit is contained in:
ngxingyu
2021-03-04 11:56:17 +08:00
committed by GitHub
parent 71b7e45d8e
commit 5edfa2e981
24 changed files with 758 additions and 177 deletions

2
.gitignore vendored
View File

@ -8,6 +8,7 @@
.buildlog/
.history
.svn/
#*.env
# IntelliJ related
*.iml
@ -34,4 +35,3 @@ build/
!example/ios/**/default.pbxuser
!example/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
.env*

View File

@ -9,6 +9,19 @@ Release notes are available on [github][notes].
[pub-semver-readme]: https://pub.dartlang.org/packages/pub_semver
[notes]: https://github.com/java-james/flutter_dotenv/releases
4.0.0-nullsafety.0
-----
- [BREAKING] Opt into null-safety
- [deps] Upgrade dart sdk constraints to ```>=2.12.0-0 <3.0.0```
- [new] Allow for escape of $ ' " and \n characters
- [fix] Ensure swallow function only removes leading 'export' keyword
- [fix] Retain spaces within single or double quotes
- [fix] Allow for comments after matching end quotes
- [new] Migrate to null safety
- [new] Create unit test cases for parse
#### 3.1.0
- [new] Allow merging with a custom map on load

View File

@ -52,7 +52,7 @@ Add the `.env` file to your assets bundle in `pubspec.yaml`
Optionally add the `.env` file as an entry in your `.gitignore` if it isn't already
```sh
.env*
*.env
```
Load the `.env` file in `main.dart`

3
example/.gitignore vendored
View File

@ -249,4 +249,5 @@ DerivedData/
GoogleService-Info.plist
google-services.json
# Plugins (already resolved through pubspec.yaml)
.flutter-plugins
.flutter-plugins
*.env

10
example/.metadata Normal file
View File

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 4b50ca7f7fbf56be72e54cd200825b760416a356
channel: beta
project_type: app

View File

@ -0,0 +1,6 @@
package com.example.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

31
example/assets/.env Normal file
View File

@ -0,0 +1,31 @@
FOO=foo
BAR=bar
FOOBAR=\$FOO$BAR
ESCAPED_DOLLAR_SIGN="\$1000"
ESCAPED_QUOTE='\''
BASIC=basic
BASIC=basic1
# previous line intentionally left blank
AFTER_LINE=after_line
EMPTY=
SINGLE_QUOTES='single_quotes'
SINGLE_QUOTES_SPACED=' single quotes '
DOUBLE_QUOTES="double_quotes"
DOUBLE_QUOTES_SPACED=" double quotes "
EXPAND_NEWLINES="expand\nnew\nlines"
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
# COMMENTS=work
EQUAL_SIGNS=equals==
RETAIN_INNER_QUOTES={"foo": "bar"}
RETAIN_LEADING_DQUOTE="retained
RETAIN_LEADING_SQUOTE='retained
RETAIN_TRAILING_DQUOTE=retained"
RETAIN_TRAILING_SQUOTE=retained'
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
USERNAME=therealnerdybeast@example.tld
SPACED_KEY = parsed

View File

@ -1,70 +1,43 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart' as DotEnv;
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart' as dotenv;
Future main() async {
await DotEnv.load();
await dotenv.load(mergeWith: {
'TEST_VAR': '5',
}); // mergeWith optional, you can include Platform.environment for Mobile/Desktop app
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
print(DotEnv.env);
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:}',
title: 'Dotenv Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Dotenv Demo'),
),
body: SingleChildScrollView(
child: FutureBuilder<String>(
future: rootBundle.loadString('assets/.env'),
initialData: '',
builder: (context, snapshot) => Container(
padding: EdgeInsets.all(50),
child: Column(
children: [
Text(
'Env map: ${dotenv.env.toString()}',
),
Divider(thickness: 5),
Text('Original'),
Divider(),
Text(snapshot.data ?? ''),
],
),
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
Text(
'Env map: ${DotEnv.env}',
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}

View File

@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
sdk: ">=2.12.0-0 <3.0.0"
dependencies:
flutter:
@ -40,37 +40,5 @@ dev_dependencies:
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- .env
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
- assets/.env

View File

@ -1,30 +1 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_dotenv_example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
void main() {}

BIN
example/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

45
example/web/index.html Normal file
View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
Fore more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>example</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('flutter-first-frame', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>

23
example/web/manifest.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "example",
"short_name": "example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -58,11 +58,25 @@ Future load(
_isInitialized = true;
}
Future testLoad(
{String fileInput = '',
Parser parser = const Parser(),
Map<String, String> mergeWith = const {}}) async {
clean();
final linesFromFile = fileInput.split('\n');
final linesFromMergeWith =
mergeWith.entries.map((entry) => "${entry.key}=${entry.value}").toList();
final allLines = linesFromMergeWith..addAll(linesFromFile);
final envEntries = parser.parse(allLines);
_envMap.addAll(envEntries);
_isInitialized = true;
}
/// True if all supplied variables have nonempty value; false otherwise.
/// Differs from [containsKey](dart:core) by excluding null values.
/// Note [load] should be called first.
bool isEveryDefined(Iterable<String> vars) =>
vars.every((k) => _envMap[k] != null && _envMap[k].isNotEmpty);
vars.every((k) => _envMap[k]?.isNotEmpty ?? false);
Future<List<String>> _getEntriesFromFile(String filename) async {
try {

View File

@ -1,14 +1,13 @@
import 'package:meta/meta.dart';
/// Creates key-value pairs from strings formatted as environment
/// variable definitions.
class Parser {
static const _singleQuot = "'";
static const _keyword = 'export';
static final _comment = RegExp(r'''#.*(?:[^'"])$''');
static final _surroundQuotes = RegExp(r'''^(['"])(.*)\1$''');
static final _bashVar = RegExp(r'(?:\\)?(\$)(?:{)?([a-zA-Z_][\w]*)+(?:})?');
static final _leadingExport = RegExp(r'''^ *export ?''');
static final _comment = RegExp(r'''#[^'"]*$''');
static final _commentWithQuotes = RegExp(r'''#.*$''');
static final _surroundQuotes = RegExp(r'''^(["'])(.*?[^\\])\1''');
static final _bashVar =
RegExp(r'(?<=^|[^\\])(\$)(?:{)?([a-zA-Z_][\w]*)+(?:})?');
/// [Parser] methods are pure functions.
const Parser();
@ -17,18 +16,17 @@ class Parser {
/// Duplicate keys are silently discarded.
Map<String, String> parse(Iterable<String> lines) {
var out = <String, String>{};
lines.forEach((line) {
for (var line in lines) {
var kv = parseOne(line, env: out);
if (kv.isEmpty) return;
if (kv.isEmpty) continue;
out.putIfAbsent(kv.keys.single, () => kv.values.single);
});
}
return out;
}
/// Parses a single line into a key-value pair.
@visibleForTesting
Map<String, String> parseOne(String line,
{Map<String, String> env: const {}}) {
{Map<String, String> env = const {}}) {
var stripped = strip(line);
if (!_isValid(stripped)) return {};
@ -40,48 +38,50 @@ class Parser {
var rhs = stripped.substring(idx + 1, stripped.length).trim();
var quotChar = surroundingQuote(rhs);
var v = unquote(rhs);
if (quotChar == _singleQuot) {
v = v.replaceAll("\\'", "'");
return {k: v};
}
final interpolatedValue = interpolate(v, env);
if (quotChar == '"') {
v = v.replaceAll('\\"', '"').replaceAll('\\n', '\n');
}
final interpolatedValue = interpolate(v, env).replaceAll("\\\$", "\$");
return {k: interpolatedValue};
}
/// Substitutes $bash_vars in [val] with values from [env].
@visibleForTesting
String interpolate(String val, Map<String, String> env) =>
String interpolate(String val, Map<String, String?> env) =>
val.replaceAllMapped(_bashVar, (m) {
var k = m.group(2);
var k = m.group(2)!;
if (!_has(env, k)) return '';
return env[k];
return env[k]!;
});
/// If [val] is wrapped in single or double quotes, returns the quote character.
/// Otherwise, returns the empty string.
@visibleForTesting
String surroundingQuote(String val) {
if (!_surroundQuotes.hasMatch(val)) return '';
return _surroundQuotes.firstMatch(val).group(1);
return _surroundQuotes.firstMatch(val)!.group(1)!;
}
/// Removes quotes (single or double) surrounding a value.
@visibleForTesting
String unquote(String val) =>
val.replaceFirstMapped(_surroundQuotes, (m) => m[2]).trim();
String unquote(String val) {
if (!_surroundQuotes.hasMatch(val))
return strip(val, includeQuotes: true).trim();
return _surroundQuotes.firstMatch(val)!.group(2)!;
}
/// Strips comments (trailing or whole-line).
@visibleForTesting
String strip(String line) => line.replaceAll(_comment, '').trim();
String strip(String line, {bool includeQuotes = false}) =>
line.replaceAll(includeQuotes ? _commentWithQuotes : _comment, '').trim();
/// Omits 'export' keyword.
@visibleForTesting
String swallow(String line) => line.replaceAll(_keyword, '').trim();
String swallow(String line) => line.replaceAll(_leadingExport, '').trim();
bool _isValid(String s) => s.isNotEmpty && s.contains('=');
/// [null] is a valid value in a Dart map, but the env var representation is empty string, not the string 'null'
bool _has(Map<String, String> map, String key) =>
/// [ null ] is a valid value in a Dart map, but the env var representation is empty string, not the string 'null'
bool _has(Map<String, String?> map, String key) =>
map.containsKey(key) && map[key] != null;
}

View File

@ -1,55 +1,111 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "14.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.41.2"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
version: "2.5.0-nullsafety.3"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0-nullsafety.3"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0-nullsafety.5"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0-nullsafety.3"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0-nullsafety.3"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
version: "1.15.0-nullsafety.5"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0-nullsafety.3"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
flutter:
dependency: "direct main"
description: flutter
@ -60,87 +116,283 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3-nullsafety.3"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
version: "0.12.10-nullsafety.3"
meta:
dependency: "direct main"
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0-nullsafety.6"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.13"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
version: "1.8.0-nullsafety.3"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.3"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0-nullsafety.3"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.9"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.9+2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.4"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.4"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.10-nullsafety.3"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
version: "1.8.0-nullsafety.4"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.10.0-nullsafety.6"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0-nullsafety.3"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0-nullsafety.3"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0-nullsafety.3"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0-nullsafety.17"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
version: "0.2.19-nullsafety.6"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.12-nullsafety.15"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0-nullsafety.5"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0-nullsafety.5"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "5.5.0"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.4"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.10.0-110 <2.11.0"
dart: ">=2.12.0-0.0 <3.0.0"

View File

@ -1,14 +1,15 @@
name: flutter_dotenv
version: 3.1.0
version: 4.0.0-nullsafety.0
description: Easily configure any flutter application with global variables using a `.env` file.
author: java-james <james-collins@hotmail.co.nz>
homepage: https://github.com/java-james/flutter_dotenv
environment:
sdk: '>=2.0.0 <3.0.0'
sdk: '>=2.12.0-0 <3.0.0'
dependencies:
flutter:
sdk: flutter
meta: ^1.1.6
dev_dependencies:
test:
flutter_test:
sdk: flutter

31
test/.env Normal file
View File

@ -0,0 +1,31 @@
FOO=foo
BAR=bar
FOOBAR=\$FOO$BAR
ESCAPED_DOLLAR_SIGN="\$1000"
ESCAPED_QUOTE='\''
BASIC=basic
BASIC=basic1
# previous line intentionally left blank
AFTER_LINE=after_line
EMPTY=
SINGLE_QUOTES='single_quotes'
SINGLE_QUOTES_SPACED=' single quotes '
DOUBLE_QUOTES="double_quotes"
DOUBLE_QUOTES_SPACED=" double quotes "
EXPAND_NEWLINES="expand\nnew\nlines"
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
# COMMENTS=work
EQUAL_SIGNS=equals==
RETAIN_INNER_QUOTES={"foo": "bar"}
RETAIN_LEADING_DQUOTE="retained
RETAIN_LEADING_SQUOTE='retained
RETAIN_TRAILING_DQUOTE=retained"
RETAIN_TRAILING_SQUOTE=retained'
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
USERNAME=therealnerdybeast@example.tld
SPACED_KEY = parsed

42
test/dotenv_test.dart Normal file
View File

@ -0,0 +1,42 @@
import 'dart:io';
import 'package:flutter_dotenv/flutter_dotenv.dart' as dotenv;
import 'package:test/test.dart';
void main() {
group('dotenv', () {
setUp(() {
print(Directory.current.toString());
dotenv.testLoad(
fileInput: File('.env')
.readAsStringSync()); //, mergeWith: Platform.environment
});
test('able to load .env', () {
expect(dotenv.env['FOO'], 'foo');
expect(dotenv.env['BAR'], 'bar');
expect(dotenv.env['FOOBAR'], '\$FOObar');
expect(dotenv.env['ESCAPED_DOLLAR_SIGN'], '\$1000');
expect(dotenv.env['ESCAPED_QUOTE'], "'");
expect(dotenv.env['BASIC'], 'basic');
expect(dotenv.env['AFTER_LINE'], 'after_line');
expect(dotenv.env['EMPTY'], '');
expect(dotenv.env['SINGLE_QUOTES'], 'single_quotes');
expect(dotenv.env['SINGLE_QUOTES_SPACED'], ' single quotes ');
expect(dotenv.env['DOUBLE_QUOTES'], 'double_quotes');
expect(dotenv.env['DOUBLE_QUOTES_SPACED'], ' double quotes ');
expect(dotenv.env['EXPAND_NEWLINES'], "expand\nnew\nlines");
expect(dotenv.env['DONT_EXPAND_UNQUOTED'], 'dontexpand\\nnewlines');
expect(dotenv.env['DONT_EXPAND_SQUOTED'], 'dontexpand\\nnewlines');
expect(dotenv.env['COMMENTS'], null);
expect(dotenv.env['EQUAL_SIGNS'], 'equals==');
expect(dotenv.env['RETAIN_INNER_QUOTES'], '{"foo": "bar"}');
expect(dotenv.env['RETAIN_LEADING_DQUOTE'], "\"retained");
expect(dotenv.env['RETAIN_LEADING_SQUOTE'], '\'retained');
expect(dotenv.env['RETAIN_TRAILING_DQUOTE'], 'retained\"');
expect(dotenv.env['RETAIN_TRAILING_SQUOTE'], "retained\'");
expect(dotenv.env['RETAIN_INNER_QUOTES_AS_STRING'], '{"foo": "bar"}');
expect(dotenv.env['TRIM_SPACE_FROM_UNQUOTED'], 'some spaced out string');
expect(dotenv.env['USERNAME'], 'therealnerdybeast@example.tld');
expect(dotenv.env['SPACED_KEY'], 'parsed');
});
});
}

170
test/parser_test.dart Normal file
View File

@ -0,0 +1,170 @@
import 'dart:math';
import 'package:flutter_dotenv/src/parser.dart';
import 'package:test/test.dart';
const ceil = 100000;
void main() {
late Random rand;
const _psr = Parser();
group('[Parser]', () {
setUp(() => rand = Random());
test('it swallows leading "export"', () {
var out = _psr.swallow(' export foo = bar ');
expect(out, equals('foo = bar'));
out = _psr.swallow(' foo = bar export');
expect(out, equals('foo = bar export'));
});
test('it strips trailing comments', () {
var out = _psr.strip(
'needs="explanation" # It was the year when they finally immanentized the Eschaton.');
expect(out, equals('needs="explanation"'));
out = _psr.strip(
'needs="explanation # It was the year when they finally immanentized the Eschaton." ');
expect(
out,
equals(
'needs="explanation # It was the year when they finally immanentized the Eschaton."'));
out = _psr.strip(
'needs=explanation # It was the year when they finally immanentized the Eschaton."',
includeQuotes: true);
expect(out, equals('needs=explanation'));
out = _psr.strip(' # It was the best of times, it was a waste of time.');
expect(out, isEmpty);
});
test('it knows quoted # is not a comment', () {
var doub = _psr.parseOne('foo = "ab#c"');
var single = _psr.parseOne("foo = 'ab#c'");
expect(doub['foo'], equals('ab#c'));
expect(single['foo'], equals('ab#c'));
});
test('it handles quotes in a comment', () {
// note terminal whitespace
var sing = _psr.parseOne("fruit = 'banana' # comments can be 'sneaky!' ");
var doub =
_psr.parseOne('fruit = " banana" # comments can be "sneaky!" ');
var none =
_psr.parseOne('fruit = banana # comments can be "sneaky!" ');
expect(sing['fruit'], equals('banana'));
expect(doub['fruit'], equals(' banana'));
expect(none['fruit'], equals('banana'));
});
test('treats all # in unquoted as comments', () {
var fail =
_psr.parseOne('fruit = banana # I\'m a comment with a final "quote"');
expect(fail['fruit'], equals('banana'));
});
test('it handles unquoted values', () {
var out = _psr.unquote(' str ');
expect(out, equals('str'));
});
test('it handles double quoted values', () {
var out = _psr.unquote('"val "');
expect(out, equals('val '));
});
test('it handles single quoted values', () {
var out = _psr.unquote("' val'");
expect(out, equals(' val'));
});
test('retain trailing single quote', () {
var out = _psr.unquote("retained'");
expect(out, equals("retained'"));
});
// test('it handles escaped quotes within values', () { // Does not
// var out = _psr.unquote('''\'val_with_\\"escaped\\"_\\'quote\\'s \'''');
// expect(out, equals('''val_with_"escaped"_'quote's '''));
// out = _psr.unquote(" val_with_\"escaped\"_\'quote\'s ");
// expect(out, equals('''val_with_"escaped"_'quote's'''));
// });
test('it skips empty lines', () {
var out = _psr.parse([
'# Define environment variables.',
' # comments will be stripped',
'foo=bar # trailing junk',
' baz = qux',
'# another comment'
]);
expect(out, equals({'foo': 'bar', 'baz': 'qux'}));
});
test('it ignores duplicate keys', () {
var out = _psr.parse(['foo=bar', 'foo=baz']);
expect(out, equals({'foo': 'bar'}));
});
test('it substitutes known variables into other values', () {
var out = _psr.parse(['foo=bar', r'baz=super$foo']);
expect(out, equals({'foo': 'bar', 'baz': 'superbar'}));
});
test('it discards surrounding quotes', () {
var out = _psr.parse([r"foo = 'bar'", r'export baz="qux"']);
expect(out, equals({'foo': 'bar', 'baz': 'qux'}));
});
test('it detects unquoted values', () {
var out = _psr.surroundingQuote('no quotes here!');
expect(out, isEmpty);
});
test('it detects double-quoted values', () {
var out = _psr.surroundingQuote('"double quoted"');
expect(out, equals('"'));
});
test('it detects single-quoted values', () {
var out = _psr.surroundingQuote("'single quoted'");
expect(out, equals("'"));
});
test('it performs variable substitution', () {
var out = _psr.interpolate(r'a$foo$baz', {'foo': 'bar', 'baz': 'qux'});
expect(out, equals('abarqux'));
});
test('it skips undefined variables', () {
var r = rand.nextInt(ceil); // avoid runtime collision with real env vars
var out = _psr.interpolate('a\$jinx_$r', {});
expect(out, equals('a'));
});
test('it handles explicitly null values in env', () {
var r = rand.nextInt(ceil); // avoid runtime collision with real env vars
var out = _psr.interpolate('a\$foo_$r\$baz_$r', {'foo_$r': null});
expect(out, equals('a'));
});
test('it handles \${surrounding braces} on vars', () {
var r = rand.nextInt(ceil); // avoid runtime collision with real env vars
var out = _psr.interpolate('optional_\${foo_$r}', {'foo_$r': 'curlies'});
expect(out, equals('optional_curlies'));
});
test('it handles equal signs in values', () {
var none = _psr.parseOne('foo=bar=qux');
var sing = _psr.parseOne("foo='bar=qux'");
var doub = _psr.parseOne('foo="bar=qux"');
expect(none['foo'], equals('bar=qux'));
expect(sing['foo'], equals('bar=qux'));
expect(doub['foo'], equals('bar=qux'));
});
test('it skips var substitution in single quotes', () {
var r = rand.nextInt(ceil); // avoid runtime collision with real env vars
var out = _psr.parseOne("some_var='my\$key_$r'", env: {'key_$r': 'val'});
expect(out['some_var'], equals('my\$key_$r'));
});
test('it performs var subs in double quotes', () {
var r = rand.nextInt(ceil); // avoid runtime collision with real env vars
var out = _psr.parseOne('some_var="my\$key_$r"', env: {'key_$r': 'val'});
expect(out['some_var'], equals('myval'));
});
test('it performs var subs without quotes', () {
var r = rand.nextInt(ceil); // avoid runtime collision with real env vars
var out = _psr.parseOne("some_var=my\$key_$r", env: {'key_$r': 'val'});
expect(out['some_var'], equals('myval'));
});
});
}