A convention and utilities for package extension discovery.
A convention to allow other packages to provide extensions for your package (or tool). Including logic for finding extensions that honor this convention.
The convention implemented in this package is that if foo
provides an
extension for <targetPackage>
.
Then foo
must contain a config file extension/<targetPackage>/config.yaml
.
This file indicates that foo
provides an extension for <targetPackage>
.
If <targetPackage>
accepts extensions from other packages it must:
- Find extensions using
findExtensions('<targetPackage>')
from this package. - Document how extensions are implemented:
- What should the contents of the
extension/<targetPackage>/config.yaml
file be? - Should packages providing extensions have a dependency constraint on
<targetPackage>
? - What libraries/assets should packages that provide extensions include?
- Should packages providing extensions specify a topic in
pubspec.yaml
for easy discovery on pub.dev.
- What should the contents of the
The findExtensions(targetPackage, packageConfig: ...)
function will:
- Load
.dart_tool/package_config.json
and find all packages that contain a valid YAML file:extension/<targetPackage>/config.yaml
. - Provide the package name, location and contents of the
config.yaml
file, for all detected extensions (aimed attargetPackage
). - Cache the results for fast loading, comparing modification timestamps to ensure consistent results.
It is the responsibility package that can be extended to validate extensions, decide when they should be enabled, and documenting how such extensions are created.
You can also use this package (and associated convention), if you are developing
a tool that can be extended by packages. In this case, you would call
findExtensions(<my_tool_package_name>, packageConfig: ...)
where
packageConfig
points to the .dart_tool/package_config.json
in the workspace
the tool is operating on.
If you tool is not distributed through pub.dev, you might consider publishing
a placeholder package in order to reserve a unique name (and avoid collisions).
Using a placeholder package to reserve a unique is also recommended for tools
that wish to cache files in .dart_tool/<my_tool_package_name>/
.
See package layout documentation for details.
Imagine that we have a hello_world
package that defines a single method
sayHello(String language)
, along the lines of:
void sayHello(String language) {
if (language == 'danish') {
print('Hej verden');
} else {
print('Hello world!');
}
}
If we wanted to allow other packages to provide additional languages by
extending the hello_world
package. Then we could do:
import 'package:extension_discovery/extension_discovery.dart';
Future<void> sayHello(String language) async {
// Find extensions for the "hello_world" package.
// WARNING: This only works when running in JIT-mode, if running in AOT-mode
// you must supply the `packageConfig` argument, and have a local
// `.dart_tool/package_config.json` and `$PUB_CACHE`.
// See "Runtime limitations" section further down.
final extensions = await findExtensions('hello_world');
// Search extensions to see if one provides a message for language
for (final ext in extensions) {
final config = ext.config;
if (config is! Map<String, Object?>) {
continue; // ignore extensions with invalid configation
}
if (config['language'] == language) {
print(config['message']);
return; // Don't print more messages!
}
}
if (language == 'danish') {
print('Hej verden');
} else {
print('Hello world!');
}
}
The findExtensions
function will search other packages for
extension/hello_world/config.yaml
, and provide the contents of this file as
well as provide the location of the extending packages.
As authors of the hello_world
package we should also document how other
packages can extend hello_world
. This is typically done by adding a segment
to the README.md
.
If in another package hello_world_german
we wanted to extend hello_world
and provide a translation for German, then we would create a
hello_world_german
package containing an
extension/hello_world/config.yaml
:
language: german
message: "Hello Welt!"
Obviously, this is a contrived example. The authors of the hello_world
package
could specify all sorts configuration options that extension authors can
specify in extension/hello_world/config.yaml
.
The authors of hello_world
could also specify that extensions must provide
certain assets in extension/hello_world/
or that they must provide certain
Dart libraries implementing a specified interface somewhere in lib/src/...
.
It is up to the authors of hello_world
to specify what extension authors must
provide. The extension_discovery
package only provides a utility for finding
extensions.
If writing my_hello_world_app
I can now take advantage of hello_world
and
hello_world_german
. Simply write a pubspec.yaml
as follows:
# pubspec.yaml
name: my_hello_world_app
dependencies:
hello_world: ^1.0.0
hello_world_german: ^1.0.0
environment:
sdk: ^3.4.0
Then I can write a bin/hello.dart
as follows:
// bin/hello.dart
import 'package:hello_world/hello_world.dart';
Future<void> main() async {
await sayHello('german');
}
As far as the extension_discovery
package is concerned an extension can
provide anything. Naturally, it is the authors of the extendable package that
decides what extensions can be provide.
In the example above it is the authors of the hello_world
package that decides
what extension packages can provide. For this reason it is important that the
authors of hello_world
very explicitly document how an extension is written.
Obviously, authors of hello_world
should document what should be specified in
extension/hello_world/config.yaml
. They could also specify that other files
should be provided in extension/hello_world/
, or that certain Dart libraries
should be provided in lib/src/hello_world/...
or something like that.
When authors of hello_world
consumes the extensions discovered through
findExtensions
they would naturally also be wise to validate that the
extension provides the required configuration and files.
When writing an extension it is strongly encouraged to have a dependency constraint on the package being extended. This ensures that the extending package will be incompatibility with new major versions of the extended package.
In the example above, it is strongly encouraged for hello_world_german
to
have a dependency constraint hello_world: ^1.0.0
. Even if hello_world_german
doesn't import libraries from package:hello_world
.
Because the next major version of hello_world
(version 2.0.0
) might change
what is required of an extension. Thus, it's fair to assume that
hello_world_german
might not be compatible with newer versions of
hello_world
. Hence, adding a dependency constraint hello_world: ^1.0.0
saves
users from resolving dependencies that aren't compatible.
Naturally, after a new major version of hello_world
is published a new
version of hello_world_german
can then also be published, addressing any
breaking changes and bumping the dependency constraint.
Tip: Authors of packages that can be extended might want to force extension authors take dependency on their package, to ensure that they have the ability to do breaking changes in the future.
The findExtensions
function only works when running in JIT-mode, otherwise the
packageConfig
parameter must be used to provide the location of
.dart_tool/package_config.json
. Obviously, the package_config.json
must be
present, as must the pub-cache locations referenced here.
Hence, findExtensions
effectively only works from project workspace!.
You can't use findExtensions
in a compiled Flutter application or an
AOT-compiled executable distributed to end-users. Because in these environments
you don't have a package_config.json
nor do you have a pub-cache. You don't
even have access to your own source files.
If your deployment target a compiled Flutter application or AOT-compiled
executable, then you will have to create some code/asset-generation.
You code/asset-generation scripts can use findExtensions
to find extensions,
and then use the assets or Dart libraries from here to generate assets or code
that is embedded in the final Flutter application (or AOT-compiled executable).
This makes findExtensions
immediately useful, if you are writing development
tools that users will install into their project workspace. But if you're
writing a package for use in deployed applications, you'll likely need to figure
out how to embed the extensions, findExtensions
only helps
you find the extensions during code-gen.