Skip to content

Latest commit

 

History

History
 
 

extension_discovery

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

package:extension_discovery pub package package publisher

A convention and utilities for package extension discovery.

What's this?

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.

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 at targetPackage).
  • 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.

Packages that extend tools

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.

Example: Hello World

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!');
  }
}

Enabling packages to extend 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.

Extending hello_world from another package

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.

Using hello_world and hello_world_german

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');
}

What can an extension provide?

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.

Compatibility considerations

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.

Runtime limitations

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.