Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/package_info_plus/package_info_plus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,55 @@ If your project was created before Flutter 3.3 you need to migrate the project a

In a web environment, the package uses the `version.json` file that it is generated in the build process.

#### `version.json` reflects the deployed version, not the running one

`PackageInfo.fromPlatform()` resolves the version on web by fetching `version.json` from the server
at runtime (with a cache buster). That file reflects the **currently deployed** version, not the
version of the bundle actually executing in the browser. If a user is running a stale, cached bundle
while a newer version has been deployed, `fromPlatform()` reports the newly deployed version.
The fetch can also fail entirely (offline, CORS, hosting rewrites), leaving every field empty.

If you need the version of the *running* bundle — e.g. to display it to the user or to gate
outdated clients — use the compile-time accessor below instead.

#### Compile-time package information (`PackageInfoEnvironment`)

Import the opt-in `package_info_plus_environment.dart` library and read
`PackageInfoEnvironment.packageInfo`:

```dart
import 'package:package_info_plus/package_info_plus_environment.dart';

final info = await PackageInfoEnvironment.packageInfo;
```

Behaviour per platform:

- **Web** — returns a `PackageInfo` built from the compile-time `PACKAGE_INFO_PLUS_*` defines.
These are embedded in the running bundle and cannot diverge from it. Provide them at build time:

```sh
flutter build web \
--dart-define=PACKAGE_INFO_PLUS_VERSION=1.2.3 \
--dart-define=PACKAGE_INFO_PLUS_BUILD_NUMBER=45
```

A **web build that omits `PACKAGE_INFO_PLUS_VERSION` fails to compile**, so a misleading version
can never ship silently. `PACKAGE_INFO_PLUS_BUILD_NUMBER`, `PACKAGE_INFO_PLUS_APP_NAME` and
`PACKAGE_INFO_PLUS_PACKAGE_NAME` are optional (empty when omitted).

- **All other platforms** — delegates to `PackageInfo.fromPlatform()`, which reads the installed
binary and is already reliable. The defines are not required there.

The accessor also recognizes tool-provided defines as fallbacks, so apps need no configuration at
all once their build front-end injects the version itself: `FLUTTER_BUILD_NAME` /
`FLUTTER_BUILD_NUMBER` (proposed in [flutter/flutter#187935](https://github.com/flutter/flutter/pull/187935))
and `dart.package.version` ([dart-lang/sdk#38855](https://github.com/dart-lang/sdk/issues/38855)).
The explicit `PACKAGE_INFO_PLUS_*` defines take precedence over both.

`PackageInfo.fromPlatform()` is unchanged; this accessor lives in a separate library that you import
only when you opt in, so existing builds are unaffected.

#### Accessing the `version.json`

The package tries to locate the `version.json` using three methods:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:package_info_plus/package_info_plus.dart';

/// Compile-time package metadata, provided at build time with `--dart-define`:
///
/// ```sh
/// flutter build web \
/// --dart-define=PACKAGE_INFO_PLUS_VERSION=1.2.3 \
/// --dart-define=PACKAGE_INFO_PLUS_BUILD_NUMBER=45
/// ```
const _version = String.fromEnvironment('PACKAGE_INFO_PLUS_VERSION');
const _buildNumber = String.fromEnvironment('PACKAGE_INFO_PLUS_BUILD_NUMBER');
const _appName = String.fromEnvironment('PACKAGE_INFO_PLUS_APP_NAME');
const _packageName = String.fromEnvironment('PACKAGE_INFO_PLUS_PACKAGE_NAME');

/// Tool-provided fallbacks, recognized so that apps need no configuration at
/// all once their build front-end injects the version itself:
///
/// * `FLUTTER_BUILD_NAME` / `FLUTTER_BUILD_NUMBER` — proposed in
/// flutter/flutter#187935 (injected by flutter_tools from the pubspec
/// `version`, like `FLUTTER_APP_FLAVOR` already is).
/// * `dart.package.version` — companion proposal for the `dart` CLI
/// (dart-lang/sdk#38855); carries the verbatim pubspec `version`,
/// including any `+buildNumber` suffix.
///
/// The explicit `PACKAGE_INFO_PLUS_*` defines take precedence over both.
const _flutterBuildName = String.fromEnvironment('FLUTTER_BUILD_NAME');
const _flutterBuildNumber = String.fromEnvironment('FLUTTER_BUILD_NUMBER');
const _dartPackageVersion = String.fromEnvironment('dart.package.version');

/// Holds the compile-time package info and enforces, at compile time, that a
/// version was provided on web builds.
///
/// The `assert` runs during constant evaluation: importing this library into a
/// **web** build that does not pass `--dart-define=PACKAGE_INFO_PLUS_VERSION`
/// is a compile error. Native builds are unaffected (`kIsWeb` is `false`
/// there), because they read the real version from the installed binary via
/// [PackageInfo.fromPlatform].
class _CompileTimePackageInfo {
const _CompileTimePackageInfo()
: assert(
!kIsWeb ||
_version != '' ||
_flutterBuildName != '' ||
_dartPackageVersion != '',
'PACKAGE_INFO_PLUS_VERSION must be provided via --dart-define on web '
'builds. On the web there is no reliable runtime source for the '
'version of the *running* bundle (version.json is fetched from the '
'server and reflects the deployed version, not the executing one). '
'Pass --dart-define=PACKAGE_INFO_PLUS_VERSION=<your version> '
'(and optionally PACKAGE_INFO_PLUS_BUILD_NUMBER / _APP_NAME / '
'_PACKAGE_NAME) to your web build.',
);

String get version {
if (_version != '') return _version;
if (_flutterBuildName != '') return _flutterBuildName;
return _dartPackageVersion.split('+').first;
}

String get buildNumber {
if (_buildNumber != '') return _buildNumber;
if (_flutterBuildNumber != '') return _flutterBuildNumber;
if (_dartPackageVersion.contains('+')) {
return _dartPackageVersion.split('+').last;
}
return '';
}

String get appName => _appName;
String get packageName => _packageName;
}

/// Opt-in accessor for the **running** application's [PackageInfo].
///
/// Import this library explicitly (it is intentionally not exported by
/// `package_info_plus.dart`) when you need a version you can trust on the web —
/// for example to display it to the user or to gate outdated clients.
///
/// Behaviour per platform:
///
/// * **Web** — returns a [PackageInfo] built from the compile-time
/// `PACKAGE_INFO_PLUS_*` defines. These are embedded in the running bundle,
/// so they cannot diverge from it the way the server-fetched `version.json`
/// used by [PackageInfo.fromPlatform] can. A web build that omits
/// `PACKAGE_INFO_PLUS_VERSION` **fails to compile** — a misleading version
/// can never ship silently.
/// * **All other platforms** — delegates to [PackageInfo.fromPlatform], which
/// reads the installed binary's metadata and is already reliable. The defines
/// are not required there.
abstract final class PackageInfoEnvironment {
/// The running application's [PackageInfo]. See [PackageInfoEnvironment].
static Future<PackageInfo> get packageInfo async {
if (kIsWeb) {
const env = _CompileTimePackageInfo();
return PackageInfo(
appName: env.appName,
packageName: env.packageName,
version: env.version,
buildNumber: env.buildNumber,
);
}
return PackageInfo.fromPlatform();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:package_info_plus/package_info_plus_environment.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

const channel = MethodChannel('dev.fluttercommunity.plus/package_info');
final log = <MethodCall>[];

TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
log.add(methodCall);
return <String, dynamic>{
'appName': 'package_info_example',
'buildNumber': '1',
'packageName': 'io.flutter.plugins.packageinfoexample',
'version': '1.0',
};
});

tearDown(log.clear);

// Off the web (these tests run on the Dart VM), the accessor delegates to
// PackageInfo.fromPlatform(). The web path is enforced at compile time and is
// covered by a compile test rather than a runtime test — a web build that
// omits PACKAGE_INFO_PLUS_VERSION does not compile.
test(
'packageInfo delegates to fromPlatform on non-web platforms',
() async {
final info = await PackageInfoEnvironment.packageInfo;
expect(info.version, '1.0');
expect(info.appName, 'package_info_example');
expect(info.packageName, 'io.flutter.plugins.packageinfoexample');
expect(info.buildNumber, '1');
expect(log, <Matcher>[isMethodCall('getAll', arguments: null)]);
},
onPlatform: {'browser': const Skip('Web path is compile-time enforced')},
);

// Exercises the tool-provided fallback chain on the web path. Run with:
//
// flutter test test/package_info_environment_test.dart --platform chrome \
// --dart-define=FLUTTER_BUILD_NAME=9.9.9 --dart-define=FLUTTER_BUILD_NUMBER=7
//
// (No PACKAGE_INFO_PLUS_VERSION: the explicit define would take precedence.)
// Note: once flutter/flutter#187935 lands, FLUTTER_BUILD_NAME becomes
// tool-reserved and is injected automatically instead of being passed by hand.
test(
'web path falls back to the tool-provided FLUTTER_BUILD_NAME defines',
() async {
if (!kIsWeb) return;
final info = await PackageInfoEnvironment.packageInfo;
expect(info.version, const String.fromEnvironment('FLUTTER_BUILD_NAME'));
expect(
info.buildNumber,
const String.fromEnvironment('FLUTTER_BUILD_NUMBER'),
);
},
skip:
const bool.hasEnvironment('PACKAGE_INFO_PLUS_VERSION') ||
!const bool.hasEnvironment('FLUTTER_BUILD_NAME')
? 'Requires --dart-define=FLUTTER_BUILD_NAME and no '
'PACKAGE_INFO_PLUS_VERSION (see comment above)'
: false,
);
}
Loading