Skip to content

Commit 13e02bd

Browse files
committed
feat(package_info_plus): add opt-in compile-time package info accessor
Adds an opt-in library, package_info_plus_environment.dart, exposing PackageInfoEnvironment.packageInfo. It returns the *running* app's PackageInfo: on web from compile-time PACKAGE_INFO_PLUS_* dart-defines, on every other platform by delegating to PackageInfo.fromPlatform(). On web there is no reliable runtime source for the running version: version.json is fetched from the server, so it reflects the deployed version rather than the executing (possibly cached) bundle, and the fetch can fail entirely (offline, CORS, hosting rewrites). The compile-time constants are embedded in the bundle and cannot diverge from it. A web build that omits PACKAGE_INFO_PLUS_VERSION fails to compile, so a misleading version can never ship silently. Native builds are unaffected. PackageInfo.fromPlatform() is unchanged and the new library is not exported by the package barrel, so this is purely additive and opt-in. Related: fluttercommunity#2675, fluttercommunity#225, fluttercommunity#456, flutter/flutter#149031.
1 parent 6427bc6 commit 13e02bd

3 files changed

Lines changed: 165 additions & 0 deletions

File tree

packages/package_info_plus/package_info_plus/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,49 @@ If your project was created before Flutter 3.3 you need to migrate the project a
122122

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

125+
#### `version.json` reflects the deployed version, not the running one
126+
127+
`PackageInfo.fromPlatform()` resolves the version on web by fetching `version.json` from the server
128+
at runtime (with a cache buster). That file reflects the **currently deployed** version, not the
129+
version of the bundle actually executing in the browser. If a user is running a stale, cached bundle
130+
while a newer version has been deployed, `fromPlatform()` reports the newly deployed version.
131+
The fetch can also fail entirely (offline, CORS, hosting rewrites), leaving every field empty.
132+
133+
If you need the version of the *running* bundle — e.g. to display it to the user or to gate
134+
outdated clients — use the compile-time accessor below instead.
135+
136+
#### Compile-time package information (`PackageInfoEnvironment`)
137+
138+
Import the opt-in `package_info_plus_environment.dart` library and read
139+
`PackageInfoEnvironment.packageInfo`:
140+
141+
```dart
142+
import 'package:package_info_plus/package_info_plus_environment.dart';
143+
144+
final info = await PackageInfoEnvironment.packageInfo;
145+
```
146+
147+
Behaviour per platform:
148+
149+
- **Web** — returns a `PackageInfo` built from the compile-time `PACKAGE_INFO_PLUS_*` defines.
150+
These are embedded in the running bundle and cannot diverge from it. Provide them at build time:
151+
152+
```sh
153+
flutter build web \
154+
--dart-define=PACKAGE_INFO_PLUS_VERSION=1.2.3 \
155+
--dart-define=PACKAGE_INFO_PLUS_BUILD_NUMBER=45
156+
```
157+
158+
A **web build that omits `PACKAGE_INFO_PLUS_VERSION` fails to compile**, so a misleading version
159+
can never ship silently. `PACKAGE_INFO_PLUS_BUILD_NUMBER`, `PACKAGE_INFO_PLUS_APP_NAME` and
160+
`PACKAGE_INFO_PLUS_PACKAGE_NAME` are optional (empty when omitted).
161+
162+
- **All other platforms** — delegates to `PackageInfo.fromPlatform()`, which reads the installed
163+
binary and is already reliable. The defines are not required there.
164+
165+
`PackageInfo.fromPlatform()` is unchanged; this accessor lives in a separate library that you import
166+
only when you opt in, so existing builds are unaffected.
167+
125168
#### Accessing the `version.json`
126169

127170
The package tries to locate the `version.json` using three methods:
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2017 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart' show kIsWeb;
6+
import 'package:package_info_plus/package_info_plus.dart';
7+
8+
/// Compile-time package metadata, provided at build time with `--dart-define`:
9+
///
10+
/// ```sh
11+
/// flutter build web \
12+
/// --dart-define=PACKAGE_INFO_PLUS_VERSION=1.2.3 \
13+
/// --dart-define=PACKAGE_INFO_PLUS_BUILD_NUMBER=45
14+
/// ```
15+
const _version = String.fromEnvironment('PACKAGE_INFO_PLUS_VERSION');
16+
const _buildNumber = String.fromEnvironment('PACKAGE_INFO_PLUS_BUILD_NUMBER');
17+
const _appName = String.fromEnvironment('PACKAGE_INFO_PLUS_APP_NAME');
18+
const _packageName = String.fromEnvironment('PACKAGE_INFO_PLUS_PACKAGE_NAME');
19+
20+
/// Holds the compile-time package info and enforces, at compile time, that a
21+
/// version was provided on web builds.
22+
///
23+
/// The `assert` runs during constant evaluation: importing this library into a
24+
/// **web** build that does not pass `--dart-define=PACKAGE_INFO_PLUS_VERSION`
25+
/// is a compile error. Native builds are unaffected (`kIsWeb` is `false`
26+
/// there), because they read the real version from the installed binary via
27+
/// [PackageInfo.fromPlatform].
28+
class _CompileTimePackageInfo {
29+
const _CompileTimePackageInfo()
30+
: assert(
31+
!kIsWeb || _version != '',
32+
'PACKAGE_INFO_PLUS_VERSION must be provided via --dart-define on web '
33+
'builds. On the web there is no reliable runtime source for the '
34+
'version of the *running* bundle (version.json is fetched from the '
35+
'server and reflects the deployed version, not the executing one). '
36+
'Pass --dart-define=PACKAGE_INFO_PLUS_VERSION=<your version> '
37+
'(and optionally PACKAGE_INFO_PLUS_BUILD_NUMBER / _APP_NAME / '
38+
'_PACKAGE_NAME) to your web build.',
39+
);
40+
41+
String get version => _version;
42+
String get buildNumber => _buildNumber;
43+
String get appName => _appName;
44+
String get packageName => _packageName;
45+
}
46+
47+
/// Opt-in accessor for the **running** application's [PackageInfo].
48+
///
49+
/// Import this library explicitly (it is intentionally not exported by
50+
/// `package_info_plus.dart`) when you need a version you can trust on the web —
51+
/// for example to display it to the user or to gate outdated clients.
52+
///
53+
/// Behaviour per platform:
54+
///
55+
/// * **Web** — returns a [PackageInfo] built from the compile-time
56+
/// `PACKAGE_INFO_PLUS_*` defines. These are embedded in the running bundle,
57+
/// so they cannot diverge from it the way the server-fetched `version.json`
58+
/// used by [PackageInfo.fromPlatform] can. A web build that omits
59+
/// `PACKAGE_INFO_PLUS_VERSION` **fails to compile** — a misleading version
60+
/// can never ship silently.
61+
/// * **All other platforms** — delegates to [PackageInfo.fromPlatform], which
62+
/// reads the installed binary's metadata and is already reliable. The defines
63+
/// are not required there.
64+
abstract final class PackageInfoEnvironment {
65+
/// The running application's [PackageInfo]. See [PackageInfoEnvironment].
66+
static Future<PackageInfo> get packageInfo async {
67+
if (kIsWeb) {
68+
const env = _CompileTimePackageInfo();
69+
return PackageInfo(
70+
appName: env.appName,
71+
packageName: env.packageName,
72+
version: env.version,
73+
buildNumber: env.buildNumber,
74+
);
75+
}
76+
return PackageInfo.fromPlatform();
77+
}
78+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/services.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:package_info_plus/package_info_plus_environment.dart';
8+
9+
void main() {
10+
TestWidgetsFlutterBinding.ensureInitialized();
11+
12+
const channel = MethodChannel('dev.fluttercommunity.plus/package_info');
13+
final log = <MethodCall>[];
14+
15+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
16+
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
17+
log.add(methodCall);
18+
return <String, dynamic>{
19+
'appName': 'package_info_example',
20+
'buildNumber': '1',
21+
'packageName': 'io.flutter.plugins.packageinfoexample',
22+
'version': '1.0',
23+
};
24+
});
25+
26+
tearDown(log.clear);
27+
28+
// Off the web (these tests run on the Dart VM), the accessor delegates to
29+
// PackageInfo.fromPlatform(). The web path is enforced at compile time and is
30+
// covered by a compile test rather than a runtime test — a web build that
31+
// omits PACKAGE_INFO_PLUS_VERSION does not compile.
32+
test(
33+
'packageInfo delegates to fromPlatform on non-web platforms',
34+
() async {
35+
final info = await PackageInfoEnvironment.packageInfo;
36+
expect(info.version, '1.0');
37+
expect(info.appName, 'package_info_example');
38+
expect(info.packageName, 'io.flutter.plugins.packageinfoexample');
39+
expect(info.buildNumber, '1');
40+
expect(log, <Matcher>[isMethodCall('getAll', arguments: null)]);
41+
},
42+
onPlatform: {'browser': const Skip('Web path is compile-time enforced')},
43+
);
44+
}

0 commit comments

Comments
 (0)