| [](https://github.com/dart-lang/tools/actions/workflows/extension_discovery.yml) |
| [](https://pub.dev/packages/extension_discovery) |
| [](https://pub.dev/packages/extension_discovery/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`][1] |
| 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][2] 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: |
| |
| ```dart |
| 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: |
| |
| ```dart |
| 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`**: |
| |
| ```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: |
| |
| ```yaml |
| # 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: |
| |
| ```dart |
| // 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. |
| |
| [1]: https://dart.dev/tools/pub/pubspec#topics |
| [2]: https://dart.dev/tools/pub/package-layout#project-specific-caching-for-tools |