Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial version of package:extension_discovery #129

Merged
merged 6 commits into from
Aug 2, 2023
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ don't naturally belong to other topic monorepos (like
| Package | Description | Version |
| --- | --- | --- |
| [cli_config](pkgs/cli_config/) | A package to take config values from configuration files, CLI arguments, and environment variables. |[![pub package](https://img.shields.io/pub/v/cli_config.svg)](https://pub.dev/packages/cli_config) |
| [extension_discovery](pkgs/extension_discovery/) | Discovery of packages that provide extensions for your package. |[![pub package](https://img.shields.io/pub/v/extension_discovery.svg)](https://pub.dev/packages/extension_discovery) |
| [graphs](pkgs/graphs/) | Graph algorithms that do not specify a particular approach for representing a Graph. |[![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) |
| [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [![pub package](https://img.shields.io/pub/v/unified_analytics.svg)](https://pub.dev/packages/unified_analytics) |

Expand Down
5 changes: 5 additions & 0 deletions pkgs/extension_discovery/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.dart_tool/
.packages
.pub/
build/
pubspec.lock
3 changes: 3 additions & 0 deletions pkgs/extension_discovery/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

- Initial version.
27 changes: 27 additions & 0 deletions pkgs/extension_discovery/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright 2023, the Dart project authors.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
233 changes: 233 additions & 0 deletions pkgs/extension_discovery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# 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.json`.
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.json` 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 JSON file: `extension/<targetPackage>/config.json`.
jonasfj marked this conversation as resolved.
Show resolved Hide resolved
* Provide the package name, location and contents of the `config.json` 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.
Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add here that it is also the responsibility of the package that will be extended to detect when the cache has been invalidated so that they can call findExtensions again to get an updated set of extensions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If people want to watch the file-system for changes, then I think it makes sense to build it in here.

You'd need to watch .dart_tool/package_config.json and config.json for every mutable extension.

I imagine that devtools will load extensions when a debugging instance is launched. If I do dart pub get after launching my app, can I expect hotreloading of all dependencies to work?

I don't mind adding some watch logic.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some watch logic would be great. In the short term, we can probably get by with having a way for the user to manually refresh the list of extensions from the extensions settings menu and by refreshing the list of available extensions on each hot reload or restart event. This of course would only be possible for Flutter. If there was a running dart CLI app that DevTools was connected to, the app would need to be killed, restarted, and reconnected to DevTools to pick up any changes that happened from running dart pub get.

I imagine that devtools will load extensions when a debugging instance is launched. If I do dart pub get after launching my app, can I expect hotreloading of all dependencies to work?

@christopherfujino do you know if all the dependencies are reloaded on hot reload? If we can expect this to work, DevTools can automatically refresh the list of extensions on hot reload / restart.



## 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/packge_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.json`, 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.json`**:

```json
{
"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.json`.

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.0.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.json`. 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
Comment on lines +183 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noting that this will not apply for DevTools extensions since we are not shipped as a pub package. Not sure if there are other "non-package" use cases for extensions, or if DevTools is the exception.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's recommendation because it really depends on the use-case.

In many cases if you're writing an extension to a package, then you're implementing an interface from the package, so then it's also natural to have a dependency on it.

If you wanted to have to ability to do, you could ask devtools extension packages to have a dependency on devtools_ext and then bump that package when you break the extension interface.
But it's also fair that you just detect that an extension isn't compatible and thus shouldn't be loaded.

In you use-case, not loading the extension is probably better than having a version conflict.

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.
Comment on lines +220 to +221
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one caveat is that a profile mode flutter app is AOT compiled but findExtensions would still work if given the location of this app's package_config.json

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I just think it's important to point out that if you are writing a Flutter app, you can't really use findExtensions and have it magically working on your phone.
Because obviously the package_config.json and all the source code for the dependencies installed in pub-cache, aren't available on your phone, at-least not without some serious hacks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think most of my comments are because I'm reading from the perspective of having another tool (DevTools, which is running on the user's local machine) call findExtensions on a separate app (also running on the user's machine), and the warnings you are heading are when an app would be trying to call findExtensions itself.

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
34 changes: 34 additions & 0 deletions pkgs/extension_discovery/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.

include: package:dart_flutter_team_lints/analysis_options.yaml

# Uncomment the following section to specify additional rules.

linter:
rules:
- always_declare_return_types
- avoid_catches_without_on_clauses
- camel_case_types
- prefer_single_quotes
- unawaited_futures

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
26 changes: 26 additions & 0 deletions pkgs/extension_discovery/example/hello_world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# `hello_world` package

Example of a package that can be extended, and uses `extension_discovery` to
find extensions.

## Limitations

The `sayHello` function in this package can only be used in a project workspace,
where dependencies are resolved and you're running in JIT-mode.

The `sayHello` function uses `findExtensions` and, thus, cannot be called in a
compiled Flutter application or AOT-compiled executable. For this to work, we'd
need to augment this package with code-generation, such that the code-generation
uses `findExtensions` and compiles a `sayHello` function into your application.

## Extending the `hello_world` package

Other packages can extend this package by providing an `extension/hello_world/config.json` file the following form:

```js
// extension/hello_world/config.json
{
"language": "<language>",
"message": "<message>"
}
```
31 changes: 31 additions & 0 deletions pkgs/extension_discovery/example/hello_world/lib/hello_world.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. 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: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`.
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!');
}
}
10 changes: 10 additions & 0 deletions pkgs/extension_discovery/example/hello_world/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: hello_world
version: 1.0.0
publish_to: none # This is an example, don't publish!

dependencies:
extension_discovery: # ^1.0.0
path: ../../

environment:
sdk: ^3.0.0
8 changes: 8 additions & 0 deletions pkgs/extension_discovery/example/hello_world_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# `hello_world_app`

An application that uses the `hello_world` package and has a dependency on
`hello_world_german` such that when `hello_world` calls `findExtensions` it will
find the extension in `hello_world_german`.

**Notice**: This application only works when running from a project workspace.
See "runtime limitation" in the README for `package:extension_discovery`.
Loading