mirror of
https://github.com/flutter/packages.git
synced 2025-05-30 21:17:42 +08:00
[flutter_plugin_tools] Reimplements the excerpt system inline in the tool, rather than relying on a separate package. (#4417)
* Allows excerpts to come from any package, not just examples. * Fixes a bug in the excerpting logic that was causing a stray `}` to appear in one example. * Removes the need for `build.excerpt.yaml` files. * Remove the dependency on build_runner for excerpts. * Reduces the time to generate the excerpts from about 10 minutes to about 5 seconds. * Almost certainly fixes https://github.com/flutter/flutter/issues/107180 (untested). The new logic is not quite backwards compatible; the `path-base` feature now specifies a real path to the actual source directories, rather than a path into the invisible generated `excerpts/` directory with its special structure. Also, a number of features from the previous package that were not actually used in this repository are no longer supported (such as having multiple section names per `#docregion` pragma).
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "site-shared"]
|
||||
path = site-shared
|
||||
url = https://github.com/dart-lang/site-shared
|
@ -1,6 +1,6 @@
|
||||
# Camera Plugin
|
||||
|
||||
<?code-excerpt path-base="excerpts/packages/camera_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dev/packages/camera)
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,6 +1,6 @@
|
||||
# file_selector
|
||||
|
||||
<?code-excerpt path-base="excerpts/packages/file_selector_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dartlang.org/packages/file_selector)
|
||||
|
||||
@ -36,7 +36,7 @@ Please also take a look at our [example][example] app.
|
||||
#### Open a single file
|
||||
|
||||
<?code-excerpt "open_image_page.dart (SingleOpen)"?>
|
||||
``` dart
|
||||
```dart
|
||||
const XTypeGroup typeGroup = XTypeGroup(
|
||||
label: 'images',
|
||||
extensions: <String>['jpg', 'png'],
|
||||
@ -48,7 +48,7 @@ final XFile? file =
|
||||
#### Open multiple files at once
|
||||
|
||||
<?code-excerpt "open_multiple_images_page.dart (MultiOpen)"?>
|
||||
``` dart
|
||||
```dart
|
||||
const XTypeGroup jpgsTypeGroup = XTypeGroup(
|
||||
label: 'JPEGs',
|
||||
extensions: <String>['jpg', 'jpeg'],
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/flutter_adaptive_scaffold_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
# Adaptive Scaffold
|
||||
|
||||
@ -35,75 +35,74 @@ animation should use AdaptiveLayout.
|
||||
|
||||
<?code-excerpt "adaptive_scaffold_demo.dart (Example)"?>
|
||||
```dart
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Define the children to display within the body at different breakpoints.
|
||||
final List<Widget> children = <Widget>[
|
||||
for (int i = 0; i < 10; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
color: const Color.fromARGB(255, 255, 201, 197),
|
||||
height: 400,
|
||||
),
|
||||
)
|
||||
];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Define the children to display within the body at different breakpoints.
|
||||
final List<Widget> children = <Widget>[
|
||||
for (int i = 0; i < 10; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
color: const Color.fromARGB(255, 255, 201, 197),
|
||||
height: 400,
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
return AdaptiveScaffold(
|
||||
// An option to override the default breakpoints used for small, medium,
|
||||
// and large.
|
||||
smallBreakpoint: const WidthPlatformBreakpoint(end: 700),
|
||||
mediumBreakpoint: const WidthPlatformBreakpoint(begin: 700, end: 1000),
|
||||
largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000),
|
||||
useDrawer: false,
|
||||
selectedIndex: _selectedTab,
|
||||
onSelectedIndexChange: (int index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
destinations: const <NavigationDestination>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.inbox_outlined),
|
||||
selectedIcon: Icon(Icons.inbox),
|
||||
label: 'Inbox',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.article_outlined),
|
||||
selectedIcon: Icon(Icons.article),
|
||||
label: 'Articles',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_outlined),
|
||||
selectedIcon: Icon(Icons.chat),
|
||||
label: 'Chat',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.video_call_outlined),
|
||||
selectedIcon: Icon(Icons.video_call),
|
||||
label: 'Video',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Inbox',
|
||||
),
|
||||
],
|
||||
body: (_) => GridView.count(crossAxisCount: 2, children: children),
|
||||
smallBody: (_) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (_, int idx) => children[idx],
|
||||
return AdaptiveScaffold(
|
||||
// An option to override the default breakpoints used for small, medium,
|
||||
// and large.
|
||||
smallBreakpoint: const WidthPlatformBreakpoint(end: 700),
|
||||
mediumBreakpoint: const WidthPlatformBreakpoint(begin: 700, end: 1000),
|
||||
largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000),
|
||||
useDrawer: false,
|
||||
selectedIndex: _selectedTab,
|
||||
onSelectedIndexChange: (int index) {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
destinations: const <NavigationDestination>[
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.inbox_outlined),
|
||||
selectedIcon: Icon(Icons.inbox),
|
||||
label: 'Inbox',
|
||||
),
|
||||
// Define a default secondaryBody.
|
||||
secondaryBody: (_) => Container(
|
||||
color: const Color.fromARGB(255, 234, 158, 192),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.article_outlined),
|
||||
selectedIcon: Icon(Icons.article),
|
||||
label: 'Articles',
|
||||
),
|
||||
// Override the default secondaryBody during the smallBreakpoint to be
|
||||
// empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
|
||||
// overridden.
|
||||
smallSecondaryBody: AdaptiveScaffold.emptyBuilder,
|
||||
);
|
||||
}
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_outlined),
|
||||
selectedIcon: Icon(Icons.chat),
|
||||
label: 'Chat',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.video_call_outlined),
|
||||
selectedIcon: Icon(Icons.video_call),
|
||||
label: 'Video',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Inbox',
|
||||
),
|
||||
],
|
||||
body: (_) => GridView.count(crossAxisCount: 2, children: children),
|
||||
smallBody: (_) => ListView.builder(
|
||||
itemCount: children.length,
|
||||
itemBuilder: (_, int idx) => children[idx],
|
||||
),
|
||||
// Define a default secondaryBody.
|
||||
secondaryBody: (_) => Container(
|
||||
color: const Color.fromARGB(255, 234, 158, 192),
|
||||
),
|
||||
// Override the default secondaryBody during the smallBreakpoint to be
|
||||
// empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
|
||||
// overridden.
|
||||
smallSecondaryBody: AdaptiveScaffold.emptyBuilder,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
||||
|
@ -124,5 +124,5 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
smallSecondaryBody: AdaptiveScaffold.emptyBuilder,
|
||||
);
|
||||
}
|
||||
// #enddocregion
|
||||
// #enddocregion Example
|
||||
}
|
||||
|
@ -17,16 +17,16 @@ const String _data = '''
|
||||
''';
|
||||
|
||||
const String _notes = '''
|
||||
# Shrink wrap demo
|
||||
# Shrink wrap demo
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This example demonstrates how `MarkdownBody`'s `shrinkWrap` property works.
|
||||
|
||||
- If `shrinkWrap` is `true`, `MarkdownBody` will take the minimum height that
|
||||
- If `shrinkWrap` is `true`, `MarkdownBody` will take the minimum height that
|
||||
wraps its content.
|
||||
- If `shrinkWrap` is `false`, `MarkdownBody` will expand to the maximum allowed
|
||||
- If `shrinkWrap` is `false`, `MarkdownBody` will expand to the maximum allowed
|
||||
height.
|
||||
''';
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
<?code-excerpt path-base="excerpts/packages/google_identity_services_web_example"?>
|
||||
|
||||
# google_identity_services_web
|
||||
|
||||
A JS-interop layer for Google Identity's Sign In With Google SDK.
|
||||
@ -29,7 +27,7 @@ script tag [as recommended](https://developers.google.com/identity/gsi/web/guide
|
||||
Place the `script` tag in the `<head>` of your site, next to the script tag that
|
||||
loads `flutter.js`, so the browser can downloaded both in parallel:
|
||||
|
||||
<?code-excerpt "../../web/index-with-script-tag.html (script-tag)"?>
|
||||
<?code-excerpt "example/web/index-with-script-tag.html (script-tag)"?>
|
||||
```html
|
||||
<head>
|
||||
<!-- ··· -->
|
||||
@ -46,7 +44,7 @@ An alternative way, that downloads the SDK on demand, is to use the
|
||||
**`loadWebSdk`** function provided by the library. A simple location to embed
|
||||
this in a Flutter Web only app can be the `main.dart`:
|
||||
|
||||
<?code-excerpt "main.dart (use-loader)"?>
|
||||
<?code-excerpt "example/lib/main.dart (use-loader)"?>
|
||||
```dart
|
||||
import 'package:google_identity_services_web/loader.dart' as gis;
|
||||
// ···
|
||||
|
@ -1,19 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
- web/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
||||
generate_for:
|
||||
- '**/*.dart'
|
||||
- '**/*.html'
|
@ -1,6 +1,6 @@
|
||||
# Google Maps for Flutter
|
||||
|
||||
<?code-excerpt path-base="excerpts/packages/google_maps_flutter_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dev/packages/google_maps_flutter)
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
# - '**/build/**'
|
||||
# builders:
|
||||
# code_excerpter|code_excerpter:
|
||||
# enabled: true
|
@ -1,6 +1,6 @@
|
||||
# google\_maps\_flutter\_android
|
||||
|
||||
<?code-excerpt path-base="excerpts/packages/google_maps_flutter_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
The Android implementation of [`google_maps_flutter`][1].
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,5 +1,5 @@
|
||||
# Image Picker plugin for Flutter
|
||||
<?code-excerpt path-base="excerpts/packages/image_picker_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dev/packages/image_picker)
|
||||
|
||||
@ -122,7 +122,7 @@ implementations allow delegating to a camera handler by setting a
|
||||
`cameraDelegate` before using `image_picker`, such as in `main()`:
|
||||
|
||||
<?code-excerpt "readme_excerpts.dart (CameraDelegate)"?>
|
||||
``` dart
|
||||
```dart
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
// ···
|
||||
class MyCameraDelegate extends ImagePickerCameraDelegate {
|
||||
@ -167,7 +167,7 @@ add a filesystem access
|
||||
### Example
|
||||
|
||||
<?code-excerpt "readme_excerpts.dart (Pick)"?>
|
||||
``` dart
|
||||
```dart
|
||||
final ImagePicker picker = ImagePicker();
|
||||
// Pick an image.
|
||||
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/image_picker_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
# image\_picker\_android
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/in_app_purchase_android_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
# Migration Guide from 0.2.x to 0.3.0
|
||||
|
||||
Starting November 2023, Android Billing Client V4 is no longer supported,
|
||||
|
@ -1,6 +1,6 @@
|
||||
# local_auth
|
||||
|
||||
<?code-excerpt path-base="excerpts/packages/local_auth_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
This Flutter plugin provides means to perform local, on-device authentication of
|
||||
the user.
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,5 +1,5 @@
|
||||
# path_provider
|
||||
<?code-excerpt path-base="excerpts/packages/path_provider_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dev/packages/path_provider)
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/pigeon_example"?>
|
||||
<?code-excerpt path-base="app"?>
|
||||
# Pigeon Examples
|
||||
|
||||
The examples here will cover basic usage. For a more thorough set of examples,
|
||||
@ -11,7 +11,7 @@ Begin by configuring pigeon at the top of the `.dart` input file.
|
||||
In actual use, you would include only the languages
|
||||
needed for your project.
|
||||
|
||||
<?code-excerpt "../../app/pigeons/messages.dart (config)"?>
|
||||
<?code-excerpt "pigeons/messages.dart (config)"?>
|
||||
```dart
|
||||
@ConfigurePigeon(PigeonOptions(
|
||||
dartOut: 'lib/src/messages.g.dart',
|
||||
@ -49,7 +49,7 @@ host platform from Flutter.
|
||||
This is the Pigeon file that describes the interface that will be used to call
|
||||
from Flutter to the host-platform.
|
||||
|
||||
<?code-excerpt "../../app/pigeons/messages.dart (host-definitions)"?>
|
||||
<?code-excerpt "pigeons/messages.dart (host-definitions)"?>
|
||||
```dart
|
||||
enum Code { one, two }
|
||||
|
||||
@ -80,8 +80,8 @@ abstract class ExampleHostApi {
|
||||
This is the code that will use the generated Dart code to make calls from Flutter to
|
||||
the host platform.
|
||||
|
||||
<?code-excerpt "../../app/lib/main.dart (main-dart)"?>
|
||||
```dart
|
||||
<?code-excerpt "lib/main.dart (main-dart)"?>
|
||||
```dart
|
||||
final ExampleHostApi _api = ExampleHostApi();
|
||||
|
||||
/// Calls host method `add` with provided arguments.
|
||||
@ -115,7 +115,7 @@ Future<bool> sendMessage(String messageText) {
|
||||
|
||||
This is the code that will use the generated Swift code to receive calls from Flutter.
|
||||
packages/pigeon/example/app/ios/Runner/AppDelegate.swift
|
||||
<?code-excerpt "../../app/ios/Runner/AppDelegate.swift (swift-class)"?>
|
||||
<?code-excerpt "ios/Runner/AppDelegate.swift (swift-class)"?>
|
||||
```swift
|
||||
// This extension of Error is required to do use FlutterError in any Swift code.
|
||||
extension FlutterError: Error {}
|
||||
@ -143,7 +143,7 @@ private class PigeonApiImplementation: ExampleHostApi {
|
||||
```
|
||||
|
||||
### Kotlin
|
||||
<?code-excerpt "../../app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt (kotlin-class)"?>
|
||||
<?code-excerpt "android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt (kotlin-class)"?>
|
||||
```kotlin
|
||||
private class PigeonApiImplementation: ExampleHostApi {
|
||||
override fun getHostLanguage(): String {
|
||||
@ -168,7 +168,7 @@ private class PigeonApiImplementation: ExampleHostApi {
|
||||
```
|
||||
|
||||
### C++
|
||||
<?code-excerpt "../../app/windows/runner/flutter_window.cpp (cpp-class)"?>
|
||||
<?code-excerpt "windows/runner/flutter_window.cpp (cpp-class)"?>
|
||||
```c++
|
||||
class PigeonApiImplementation : public ExampleHostApi {
|
||||
public:
|
||||
@ -200,7 +200,7 @@ app from the host platform.
|
||||
|
||||
### Dart input
|
||||
|
||||
<?code-excerpt "../../app/pigeons/messages.dart (flutter-definitions)"?>
|
||||
<?code-excerpt "pigeons/messages.dart (flutter-definitions)"?>
|
||||
```dart
|
||||
@FlutterApi()
|
||||
abstract class MessageFlutterApi {
|
||||
@ -213,8 +213,8 @@ abstract class MessageFlutterApi {
|
||||
This is the code that will use the generated Dart code to handle calls made to
|
||||
Flutter from the host platform.
|
||||
|
||||
<?code-excerpt "../../app/lib/main.dart (main-dart-flutter)"?>
|
||||
```dart
|
||||
<?code-excerpt "lib/main.dart (main-dart-flutter)"?>
|
||||
```dart
|
||||
class _ExampleFlutterApi implements MessageFlutterApi {
|
||||
@override
|
||||
String flutterMethod(String? aString) {
|
||||
@ -227,7 +227,7 @@ class _ExampleFlutterApi implements MessageFlutterApi {
|
||||
|
||||
### Swift
|
||||
|
||||
<?code-excerpt "../../app/ios/Runner/AppDelegate.swift (swift-class-flutter)"?>
|
||||
<?code-excerpt "ios/Runner/AppDelegate.swift (swift-class-flutter)"?>
|
||||
```swift
|
||||
private class PigeonFlutterApi {
|
||||
var flutterAPI: MessageFlutterApi
|
||||
@ -246,7 +246,7 @@ private class PigeonFlutterApi {
|
||||
|
||||
### Kotlin
|
||||
|
||||
<?code-excerpt "../../app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt (kotlin-class-flutter)"?>
|
||||
<?code-excerpt "android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt (kotlin-class-flutter)"?>
|
||||
```kotlin
|
||||
private class PigeonFlutterApi {
|
||||
|
||||
@ -266,7 +266,7 @@ private class PigeonFlutterApi {
|
||||
|
||||
### C++
|
||||
|
||||
<?code-excerpt "../../app/windows/runner/flutter_window.cpp (cpp-method-flutter)"?>
|
||||
<?code-excerpt "windows/runner/flutter_window.cpp (cpp-method-flutter)"?>
|
||||
```c++
|
||||
void TestPlugin::CallFlutterMethod(
|
||||
String aString, std::function<void(ErrorOr<int64_t> reply)> result) {
|
||||
|
@ -1,23 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
- pigeons/**
|
||||
- app/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
||||
generate_for:
|
||||
- '**.dart'
|
||||
- '**.swift'
|
||||
- '**.kt'
|
||||
- '**.cpp'
|
||||
- '**.h'
|
@ -1,5 +1,5 @@
|
||||
# Shared preferences plugin
|
||||
<?code-excerpt path-base="excerpts/packages/shared_preferences_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dev/packages/shared_preferences)
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/standard_message_codec_examples"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
An efficient and schemaless binary format used by the Flutter SDK.
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/url_launcher_example"?>
|
||||
<?code-excerpt path-base="example"?>
|
||||
|
||||
# url_launcher
|
||||
|
||||
@ -16,7 +16,7 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil
|
||||
|
||||
### Example
|
||||
|
||||
<?code-excerpt "basic.dart (basic-example)"?>
|
||||
<?code-excerpt "lib/basic.dart (basic-example)"?>
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@ -71,7 +71,7 @@ element must be added to your manifest as a child of the root element.
|
||||
|
||||
Example:
|
||||
|
||||
<?code-excerpt "../../android/app/src/main/AndroidManifest.xml (android-queries)" plaster="none"?>
|
||||
<?code-excerpt "android/app/src/main/AndroidManifest.xml (android-queries)" plaster="none"?>
|
||||
```xml
|
||||
<!-- Provide required visibility configuration for API level 30 and above -->
|
||||
<queries>
|
||||
@ -141,7 +141,7 @@ due to [a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri`
|
||||
encodes query parameters. Using `queryParameters` will result in spaces being
|
||||
converted to `+` in many cases.
|
||||
|
||||
<?code-excerpt "encoding.dart (encode-query-parameters)"?>
|
||||
<?code-excerpt "lib/encoding.dart (encode-query-parameters)"?>
|
||||
```dart
|
||||
String? encodeQueryParameters(Map<String, String> params) {
|
||||
return params.entries
|
||||
@ -163,7 +163,7 @@ String? encodeQueryParameters(Map<String, String> params) {
|
||||
|
||||
Encoding for `sms` is slightly different:
|
||||
|
||||
<?code-excerpt "encoding.dart (sms)"?>
|
||||
<?code-excerpt "lib/encoding.dart (sms)"?>
|
||||
```dart
|
||||
final Uri smsLaunchUri = Uri(
|
||||
scheme: 'sms',
|
||||
@ -192,7 +192,7 @@ We recommend checking first whether the directory or file exists before calling
|
||||
|
||||
Example:
|
||||
|
||||
<?code-excerpt "files.dart (file)"?>
|
||||
<?code-excerpt "lib/files.dart (file)"?>
|
||||
```dart
|
||||
final String filePath = testFile.absolute.path;
|
||||
final Uri uri = Uri.file(filePath);
|
||||
|
@ -1,20 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
- android/app/src/main/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
- 'android/app/src/main/res/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
||||
generate_for:
|
||||
- '**/*.dart'
|
||||
- android/**/*.xml
|
@ -1,4 +1,4 @@
|
||||
<?code-excerpt path-base="excerpts/packages/video_player_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
# Video Player plugin for Flutter
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
- android/app/src/main/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
- 'android/app/src/main/res/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
||||
generate_for:
|
||||
- '**/*.dart'
|
||||
- android/**/*.xml
|
@ -154,7 +154,7 @@ WEBVTT
|
||||
00:05.200 --> 00:06.000 align:start size:50%
|
||||
<v Roger Bingham><i>You know I'm so excited my glasses are falling off here.</i>
|
||||
|
||||
00:00:06.050 --> 00:00:06.150
|
||||
00:00:06.050 --> 00:00:06.150
|
||||
<v Roger Bingham><i>I have a different time!</i>
|
||||
|
||||
00:06.200 --> 00:06.900
|
||||
|
@ -1,6 +1,6 @@
|
||||
# WebView for Flutter
|
||||
|
||||
<?code-excerpt path-base="excerpts/packages/webview_flutter_example"?>
|
||||
<?code-excerpt path-base="example/lib"?>
|
||||
|
||||
[](https://pub.dev/packages/webview_flutter)
|
||||
|
||||
@ -227,4 +227,4 @@ Below is a non-exhaustive list of changes to the API:
|
||||
[WebViewController]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html
|
||||
[WebViewWidget]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html
|
||||
[NavigationDelegate]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/NavigationDelegate-class.html
|
||||
[WebViewCookieManager]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewCookieManager-class.html
|
||||
[WebViewCookieManager]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewCookieManager-class.html
|
||||
|
@ -1,15 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
include:
|
||||
- lib/**
|
||||
# Some default includes that aren't really used here but will prevent
|
||||
# false-negative warnings:
|
||||
- $package$
|
||||
- lib/$lib$
|
||||
exclude:
|
||||
- '**/.*/**'
|
||||
- '**/build/**'
|
||||
builders:
|
||||
code_excerpter|code_excerpter:
|
||||
enabled: true
|
@ -19,12 +19,6 @@ dart run script/tool/bin/flutter_plugin_tools.dart <args>
|
||||
|
||||
Many commands require the Flutter-bundled version of Dart to be the first `dart` in the path.
|
||||
|
||||
### Extra Setup
|
||||
|
||||
When updating sample code excerpts (`update-excerpts`) for the README.md files,
|
||||
there is some [extra setup for
|
||||
submodules](#update-readmemd-from-example-sources) that is necessary.
|
||||
|
||||
## Commands
|
||||
|
||||
Run with `--help` for a full list of commands and arguments, but the
|
||||
@ -94,14 +88,16 @@ dart run script/tool/bin/flutter_plugin_tools.dart native-test --windows --packa
|
||||
|
||||
### Update README.md from Example Sources
|
||||
|
||||
`update-excerpts` requires sources that are in a submodule. If you didn't clone
|
||||
with submodules, you will need to `git submodule update --init --recursive`
|
||||
before running this command.
|
||||
|
||||
```sh
|
||||
# Update all .md files for all packages:
|
||||
dart run script/tool/bin/flutter_plugin_tools.dart update-excerpts
|
||||
|
||||
# Update the .md files only for one package:
|
||||
dart run script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages package_name
|
||||
```
|
||||
|
||||
_See also: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code_
|
||||
|
||||
### Update CHANGELOG and Version
|
||||
|
||||
`update-release-info` will automatically update the version and `CHANGELOG.md`
|
||||
@ -178,13 +174,3 @@ _everything_, including untracked or uncommitted files in version control.
|
||||
`publish` will first check the status of the local
|
||||
directory and refuse to publish if there are any mismatched files with version
|
||||
control present.
|
||||
|
||||
## Updating the Tool
|
||||
|
||||
For flutter/packages, just changing the source here is all that's needed.
|
||||
|
||||
For changes that are relevant to flutter/packages, you will also need to:
|
||||
- Update the tool's pubspec.yaml and CHANGELOG
|
||||
- Publish the tool
|
||||
- Update the pinned version in
|
||||
[flutter/packages](https://github.com/flutter/packages/blob/main/.cirrus.yml)
|
||||
|
@ -172,20 +172,6 @@ class ReadmeCheckCommand extends PackageLoopingCommand {
|
||||
errorSummary = 'Missing language identifier for code block';
|
||||
}
|
||||
|
||||
// If any blocks use code excerpts, make sure excerpting is configured
|
||||
// for the package.
|
||||
if (readmeLines.any((String line) => line.startsWith(excerptTagStart))) {
|
||||
const String buildRunnerConfigFile = 'build.excerpt.yaml';
|
||||
if (!mainPackage.getExamples().any((RepositoryPackage example) =>
|
||||
example.directory.childFile(buildRunnerConfigFile).existsSync())) {
|
||||
printError('code-excerpt tag found, but the package is not configured '
|
||||
'for excerpting. Follow the instructions at\n'
|
||||
'$_instructionWikiUrl\n'
|
||||
'for setting up a build.excerpt.yaml file.');
|
||||
errorSummary ??= 'Missing code-excerpt configuration';
|
||||
}
|
||||
}
|
||||
|
||||
if (missingExcerptLines.isNotEmpty) {
|
||||
for (final int lineNumber in missingExcerptLines) {
|
||||
printError('${indentation}Dart code block at line $lineNumber is not '
|
||||
@ -193,8 +179,7 @@ class ReadmeCheckCommand extends PackageLoopingCommand {
|
||||
}
|
||||
printError(
|
||||
'\n${indentation}For each block listed above, add <?code-excerpt ...> '
|
||||
'tag on the previous line, and ensure that a build.excerpt.yaml is '
|
||||
'configured for the source example as explained at\n'
|
||||
'tag on the previous line, as explained at\n'
|
||||
'$_instructionWikiUrl');
|
||||
errorSummary ??= 'Missing code-excerpt management for code block';
|
||||
}
|
||||
|
@ -2,18 +2,20 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
import 'package:yaml_edit/yaml_edit.dart';
|
||||
|
||||
import 'common/output_utils.dart';
|
||||
import 'common/package_looping_command.dart';
|
||||
import 'common/pub_utils.dart';
|
||||
import 'common/repository_package.dart';
|
||||
|
||||
class _UpdateResult {
|
||||
const _UpdateResult(this.changed, this.errors);
|
||||
final bool changed;
|
||||
final List<String> errors;
|
||||
}
|
||||
|
||||
enum _ExcerptParseMode { normal, pragma, injecting }
|
||||
|
||||
/// A command to update .md code excerpts from code files.
|
||||
class UpdateExcerptsCommand extends PackageLoopingCommand {
|
||||
/// Creates a excerpt updater command instance.
|
||||
@ -23,232 +25,295 @@ class UpdateExcerptsCommand extends PackageLoopingCommand {
|
||||
super.platform,
|
||||
super.gitDir,
|
||||
}) {
|
||||
argParser.addFlag(_failOnChangeFlag, hide: true);
|
||||
argParser.addFlag(_noCleanupFlag,
|
||||
help: 'Skips the step of cleaning up the excerpt extraction output. '
|
||||
'This can be useful when debugging extraction or checking paths to '
|
||||
'reference in snippets.');
|
||||
argParser.addFlag(
|
||||
_failOnChangeFlag,
|
||||
help: 'Fail if the command does anything. '
|
||||
'(Used in CI to ensure excerpts are up to date.)',
|
||||
);
|
||||
}
|
||||
|
||||
static const String _failOnChangeFlag = 'fail-on-change';
|
||||
static const String _noCleanupFlag = 'no-cleanup';
|
||||
|
||||
static const String _buildRunnerConfigName = 'excerpt';
|
||||
// The name of the build_runner configuration file that will be in an example
|
||||
// directory if the package is set up to use `code-excerpt`.
|
||||
static const String _buildRunnerConfigFile =
|
||||
'build.$_buildRunnerConfigName.yaml';
|
||||
|
||||
/// The relative directory path to put the extracted excerpt yaml files.
|
||||
@visibleForTesting
|
||||
static const String excerptOutputDir = 'excerpts';
|
||||
|
||||
// The filename to store the pre-modification copy of the pubspec.
|
||||
static const String _originalPubspecFilename =
|
||||
'pubspec.plugin_tools_original.yaml';
|
||||
|
||||
@override
|
||||
final String name = 'update-excerpts';
|
||||
|
||||
@override
|
||||
final String description = 'Updates code excerpts in .md files, based '
|
||||
'on code from code files, via code-excerpt';
|
||||
'on code from code files, via <?code-excerpt?> pragmas.';
|
||||
|
||||
@override
|
||||
Future<PackageResult> runForPackage(RepositoryPackage package) async {
|
||||
final Iterable<RepositoryPackage> configuredExamples = package
|
||||
.getExamples()
|
||||
.where((RepositoryPackage example) =>
|
||||
example.directory.childFile(_buildRunnerConfigFile).existsSync());
|
||||
|
||||
if (configuredExamples.isEmpty) {
|
||||
return PackageResult.skip(
|
||||
'No $_buildRunnerConfigFile found in example(s).');
|
||||
}
|
||||
|
||||
final Directory repoRoot =
|
||||
packagesDir.fileSystem.directory((await gitDir).path);
|
||||
|
||||
for (final RepositoryPackage example in configuredExamples) {
|
||||
_addSubmoduleDependencies(example, repoRoot: repoRoot);
|
||||
|
||||
try {
|
||||
// Ensure that dependencies are available.
|
||||
if (!await runPubGet(example, processRunner, platform)) {
|
||||
return PackageResult.fail(
|
||||
<String>['Unable to get script dependencies']);
|
||||
}
|
||||
|
||||
// Update the excerpts.
|
||||
if (!await _extractSnippets(example)) {
|
||||
return PackageResult.fail(<String>['Unable to extract excerpts']);
|
||||
}
|
||||
if (!await _injectSnippets(example, targetPackage: package)) {
|
||||
return PackageResult.fail(<String>['Unable to inject excerpts']);
|
||||
}
|
||||
if (!await _injectSnippets(example, targetPackage: example)) {
|
||||
return PackageResult.fail(
|
||||
<String>['Unable to inject example excerpts']);
|
||||
}
|
||||
} finally {
|
||||
// Clean up the pubspec changes and extracted excerpts directory.
|
||||
_undoPubspecChanges(example);
|
||||
final Directory excerptDirectory =
|
||||
example.directory.childDirectory(excerptOutputDir);
|
||||
if (excerptDirectory.existsSync()) {
|
||||
if (getBoolArg(_noCleanupFlag)) {
|
||||
final String relativeDir =
|
||||
getRelativePosixPath(excerptDirectory, from: package.directory);
|
||||
print(
|
||||
'\n\nSKIPPING CLEANUP: Extraction output is in $relativeDir/');
|
||||
} else {
|
||||
excerptDirectory.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
final List<File> changedFiles = <File>[];
|
||||
final List<String> errors = <String>[];
|
||||
final List<File> markdownFiles = package.directory
|
||||
.listSync(recursive: true)
|
||||
.where((FileSystemEntity entity) {
|
||||
return entity is File &&
|
||||
entity.basename != 'CHANGELOG.md' &&
|
||||
entity.basename.toLowerCase().endsWith('.md');
|
||||
})
|
||||
.cast<File>()
|
||||
.toList();
|
||||
for (final File file in markdownFiles) {
|
||||
final _UpdateResult result = _updateExcerptsIn(file);
|
||||
if (result.changed) {
|
||||
changedFiles.add(file);
|
||||
}
|
||||
if (result.errors.isNotEmpty) {
|
||||
errors.addAll(result.errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (getBoolArg(_failOnChangeFlag)) {
|
||||
final String? stateError = await _validateRepositoryState(package);
|
||||
if (stateError != null) {
|
||||
printError('One or more .md files are out of sync with their source '
|
||||
'excerpts.\n\n'
|
||||
'If you edited code in a .md file directly, you should instead '
|
||||
'edit the example source files. If you edited source files, run '
|
||||
'the repository tooling\'s "$name" command on this package, and '
|
||||
'update your PR with the resulting changes.\n\n'
|
||||
'For more information, see '
|
||||
'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code');
|
||||
return PackageResult.fail(<String>[stateError]);
|
||||
}
|
||||
if (errors.isNotEmpty) {
|
||||
printError('Injecting excerpts failed:');
|
||||
printError(errors.join('\n'));
|
||||
return PackageResult.fail();
|
||||
}
|
||||
|
||||
if (getBoolArg(_failOnChangeFlag) && changedFiles.isNotEmpty) {
|
||||
printError(
|
||||
'The following files have out of date excerpts:\n'
|
||||
' ${changedFiles.map((File file) => file.path).join("\n ")}\n'
|
||||
'\n'
|
||||
'If you edited code in a .md file directly, you should instead edit the '
|
||||
'files that contain the sources of the excerpts.\n'
|
||||
'If you did edit those source files, run the repository tooling\'s "$name" '
|
||||
'command on this package, and update your PR with the resulting changes.\n'
|
||||
'\n'
|
||||
'For more information, see '
|
||||
'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code',
|
||||
);
|
||||
return PackageResult.fail();
|
||||
}
|
||||
|
||||
return PackageResult.success();
|
||||
}
|
||||
|
||||
/// Runs the extraction step to create the excerpt files for the given
|
||||
/// example, returning true on success.
|
||||
Future<bool> _extractSnippets(RepositoryPackage example) async {
|
||||
final int exitCode = await processRunner.runAndStream(
|
||||
'dart',
|
||||
<String>[
|
||||
'run',
|
||||
'build_runner',
|
||||
'build',
|
||||
'--config',
|
||||
_buildRunnerConfigName,
|
||||
'--output',
|
||||
excerptOutputDir,
|
||||
'--delete-conflicting-outputs',
|
||||
],
|
||||
workingDir: example.directory);
|
||||
return exitCode == 0;
|
||||
static const String _pragma = '<?code-excerpt';
|
||||
static final RegExp _basePattern =
|
||||
RegExp(r'^ *<\?code-excerpt path-base="([^"]+)"\?>$');
|
||||
static final RegExp _injectPattern = RegExp(
|
||||
r'^ *<\?code-excerpt "(?<path>[^ ]+) \((?<section>[^)]+)\)"(?: plaster="(?<plaster>[^"]*)")?\?>$',
|
||||
);
|
||||
|
||||
_UpdateResult _updateExcerptsIn(File file) {
|
||||
bool detectedChange = false;
|
||||
final List<String> errors = <String>[];
|
||||
Directory pathBase = file.parent;
|
||||
final StringBuffer output = StringBuffer();
|
||||
final StringBuffer existingBlock = StringBuffer();
|
||||
String? language;
|
||||
String? excerpt;
|
||||
_ExcerptParseMode mode = _ExcerptParseMode.normal;
|
||||
int lineNumber = 0;
|
||||
for (final String line in file.readAsLinesSync()) {
|
||||
lineNumber += 1;
|
||||
switch (mode) {
|
||||
case _ExcerptParseMode.normal:
|
||||
if (line.contains(_pragma)) {
|
||||
RegExpMatch? match = _basePattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
pathBase =
|
||||
file.parent.childDirectory(path.normalize(match.group(1)!));
|
||||
} else {
|
||||
match = _injectPattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final String excerptPath =
|
||||
path.normalize(match.namedGroup('path')!);
|
||||
final File excerptSourceFile = pathBase.childFile(excerptPath);
|
||||
final String extension = path.extension(excerptSourceFile.path);
|
||||
switch (extension) {
|
||||
case '':
|
||||
language = 'txt';
|
||||
break;
|
||||
case '.kt':
|
||||
language = 'kotlin';
|
||||
break;
|
||||
case '.cc':
|
||||
case '.cpp':
|
||||
language = 'c++';
|
||||
break;
|
||||
default:
|
||||
language = extension.substring(1);
|
||||
break;
|
||||
}
|
||||
final String section = match.namedGroup('section')!;
|
||||
final String plaster = match.namedGroup('plaster') ?? '···';
|
||||
if (!excerptSourceFile.existsSync()) {
|
||||
errors.add(
|
||||
'${file.path}:$lineNumber: specified file "$excerptPath" (resolved to "${excerptSourceFile.path}") does not exist');
|
||||
} else {
|
||||
excerpt = _extractExcerpt(
|
||||
excerptSourceFile, section, plaster, language, errors);
|
||||
}
|
||||
mode = _ExcerptParseMode.pragma;
|
||||
} else {
|
||||
errors.add(
|
||||
'${file.path}:$lineNumber: $_pragma?> pragma does not match expected syntax or is not alone on the line');
|
||||
}
|
||||
}
|
||||
}
|
||||
output.writeln(line);
|
||||
break;
|
||||
case _ExcerptParseMode.pragma:
|
||||
if (!line.startsWith('```')) {
|
||||
errors.add(
|
||||
'${file.path}:$lineNumber: expected code block but did not find one');
|
||||
mode = _ExcerptParseMode.normal;
|
||||
} else {
|
||||
if (line.startsWith('``` ')) {
|
||||
errors.add(
|
||||
'${file.path}:$lineNumber: code block was followed by a space character instead of the language (expected "$language")');
|
||||
mode = _ExcerptParseMode.injecting;
|
||||
} else if (line != '```$language' && line != '```rfwtxt' && line != '```json') {
|
||||
// We special-case rfwtxt and json because the rfw package extracts such sections from Dart files.
|
||||
// If we get more special cases we should think about a more general solution.
|
||||
errors.add(
|
||||
'${file.path}:$lineNumber: code block has wrong language');
|
||||
}
|
||||
mode = _ExcerptParseMode.injecting;
|
||||
}
|
||||
output.writeln(line);
|
||||
break;
|
||||
case _ExcerptParseMode.injecting:
|
||||
if (line == '```') {
|
||||
if (existingBlock.toString() != excerpt) {
|
||||
detectedChange = true;
|
||||
}
|
||||
output.write(excerpt);
|
||||
output.writeln(line);
|
||||
mode = _ExcerptParseMode.normal;
|
||||
language = null;
|
||||
excerpt = null;
|
||||
existingBlock.clear();
|
||||
} else {
|
||||
existingBlock.writeln(line);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (detectedChange) {
|
||||
if (errors.isNotEmpty) {
|
||||
errors.add('${file.path}: skipped updating file due to errors');
|
||||
} else {
|
||||
try {
|
||||
file.writeAsStringSync(output.toString());
|
||||
} catch (e) {
|
||||
errors.add(
|
||||
'${file.path}: failed to update file (${e.runtimeType}: $e)');
|
||||
}
|
||||
}
|
||||
}
|
||||
return _UpdateResult(detectedChange, errors);
|
||||
}
|
||||
|
||||
/// Runs the injection step to update [targetPackage]'s top-level .md files
|
||||
/// with the latest excerpts from [example], returning true on success.
|
||||
Future<bool> _injectSnippets(
|
||||
RepositoryPackage example, {
|
||||
required RepositoryPackage targetPackage,
|
||||
}) async {
|
||||
final List<String> relativeMdPaths = targetPackage.directory
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((File f) =>
|
||||
f.basename.toLowerCase().endsWith('.md') &&
|
||||
// Exclude CHANGELOG since it should never have excerpts.
|
||||
f.basename != 'CHANGELOG.md')
|
||||
.map((File f) => getRelativePosixPath(f, from: example.directory))
|
||||
.toList();
|
||||
if (relativeMdPaths.isEmpty) {
|
||||
return true;
|
||||
String _extractExcerpt(File excerptSourceFile, String section,
|
||||
String plasterInside, String language, List<String> errors) {
|
||||
final List<String> buffer = <String>[];
|
||||
bool extracting = false;
|
||||
int lineNumber = 0;
|
||||
int maxLength = 0;
|
||||
bool found = false;
|
||||
String prefix = '';
|
||||
String suffix = '';
|
||||
String padding = '';
|
||||
switch (language) {
|
||||
case 'cc':
|
||||
case 'c++':
|
||||
case 'dart':
|
||||
case 'js':
|
||||
case 'kotlin':
|
||||
case 'rfwtxt':
|
||||
case 'swift':
|
||||
prefix = '// ';
|
||||
break;
|
||||
case 'css':
|
||||
prefix = '/* ';
|
||||
suffix = ' */';
|
||||
break;
|
||||
case 'html':
|
||||
case 'xml':
|
||||
prefix = '<!--';
|
||||
suffix = '-->';
|
||||
padding = ' ';
|
||||
break;
|
||||
case 'yaml':
|
||||
prefix = '# ';
|
||||
break;
|
||||
}
|
||||
final int exitCode = await processRunner.runAndStream(
|
||||
'dart',
|
||||
<String>[
|
||||
'run',
|
||||
'code_excerpt_updater',
|
||||
'--write-in-place',
|
||||
'--yaml',
|
||||
'--no-escape-ng-interpolation',
|
||||
...relativeMdPaths,
|
||||
],
|
||||
workingDir: example.directory);
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
/// Adds `code_excerpter` and `code_excerpt_updater` to [package]'s
|
||||
/// `dev_dependencies` using path-based references to the submodule copies.
|
||||
///
|
||||
/// This is done on the fly rather than being checked in so that:
|
||||
/// - Just building examples don't require everyone to check out submodules.
|
||||
/// - Examples can be analyzed/built even on versions of Flutter that these
|
||||
/// submodules do not support.
|
||||
void _addSubmoduleDependencies(RepositoryPackage package,
|
||||
{required Directory repoRoot}) {
|
||||
final String pubspecContents = package.pubspecFile.readAsStringSync();
|
||||
// Save aside a copy of the current pubspec state. This allows restoration
|
||||
// to the previous state regardless of its git status at the time the script
|
||||
// ran.
|
||||
package.directory
|
||||
.childFile(_originalPubspecFilename)
|
||||
.writeAsStringSync(pubspecContents);
|
||||
|
||||
// Update the actual pubspec.
|
||||
final YamlEditor editablePubspec = YamlEditor(pubspecContents);
|
||||
const String devDependenciesKey = 'dev_dependencies';
|
||||
final YamlNode root = editablePubspec.parseAt(<String>[]);
|
||||
// Ensure that there's a `dev_dependencies` entry to update.
|
||||
if ((root as YamlMap)[devDependenciesKey] == null) {
|
||||
editablePubspec.update(<String>['dev_dependencies'], YamlMap());
|
||||
final String startRegionMarker = '$prefix#docregion $section$suffix';
|
||||
final String endRegionMarker = '$prefix#enddocregion $section$suffix';
|
||||
final String plaster = '$prefix$padding$plasterInside$padding$suffix';
|
||||
int? indentation;
|
||||
for (final String excerptLine in excerptSourceFile.readAsLinesSync()) {
|
||||
final String trimmedLine = excerptLine.trimLeft();
|
||||
lineNumber += 1;
|
||||
if (extracting) {
|
||||
if (trimmedLine == endRegionMarker) {
|
||||
extracting = false;
|
||||
indentation = excerptLine.length - trimmedLine.length;
|
||||
} else {
|
||||
if (trimmedLine == startRegionMarker) {
|
||||
errors.add(
|
||||
'${excerptSourceFile.path}:$lineNumber: saw "$startRegionMarker" pragma while already in a "$section" doc region');
|
||||
}
|
||||
if (excerptLine.length > maxLength) {
|
||||
maxLength = excerptLine.length;
|
||||
}
|
||||
if (!excerptLine.contains('$prefix#docregion ') &&
|
||||
!excerptLine.contains('$prefix#enddocregion ')) {
|
||||
buffer.add(excerptLine);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (trimmedLine == startRegionMarker) {
|
||||
found = true;
|
||||
extracting = true;
|
||||
if (buffer.isNotEmpty && plasterInside != 'none') {
|
||||
assert(indentation != null);
|
||||
buffer.add('${" " * indentation!}$plaster');
|
||||
indentation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final Set<String> submoduleDependencies = <String>{
|
||||
'code_excerpter',
|
||||
'code_excerpt_updater',
|
||||
};
|
||||
final String relativeRootPath =
|
||||
getRelativePosixPath(repoRoot, from: package.directory);
|
||||
for (final String dependency in submoduleDependencies) {
|
||||
editablePubspec.update(<String>[
|
||||
devDependenciesKey,
|
||||
dependency
|
||||
], <String, String>{
|
||||
'path': '$relativeRootPath/site-shared/packages/$dependency'
|
||||
});
|
||||
if (extracting) {
|
||||
errors
|
||||
.add('${excerptSourceFile.path}: missing "$endRegionMarker" pragma');
|
||||
}
|
||||
package.pubspecFile.writeAsStringSync(editablePubspec.toString());
|
||||
}
|
||||
|
||||
/// Restores the version of the pubspec that was present before running
|
||||
/// [_addSubmoduleDependencies].
|
||||
void _undoPubspecChanges(RepositoryPackage package) {
|
||||
package.directory
|
||||
.childFile(_originalPubspecFilename)
|
||||
.renameSync(package.pubspecFile.path);
|
||||
}
|
||||
|
||||
/// Checks the git state, returning an error string if any .md files have
|
||||
/// changed.
|
||||
Future<String?> _validateRepositoryState(RepositoryPackage package) async {
|
||||
final io.ProcessResult checkFiles = await processRunner.run(
|
||||
'git',
|
||||
<String>['ls-files', '--modified'],
|
||||
workingDir: package.directory,
|
||||
logOnError: true,
|
||||
);
|
||||
if (checkFiles.exitCode != 0) {
|
||||
return 'Unable to determine local file state';
|
||||
if (!found) {
|
||||
errors.add(
|
||||
'${excerptSourceFile.path}: did not find a "$startRegionMarker" pragma');
|
||||
return '';
|
||||
}
|
||||
|
||||
final String stdout = checkFiles.stdout as String;
|
||||
final List<String> changedFiles = stdout.trim().split('\n');
|
||||
final Iterable<String> changedMDFiles =
|
||||
changedFiles.where((String filePath) => filePath.endsWith('.md'));
|
||||
if (changedMDFiles.isNotEmpty) {
|
||||
return 'Snippets are out of sync in the following files: '
|
||||
'${changedMDFiles.join(', ')}';
|
||||
if (buffer.isEmpty) {
|
||||
errors.add('${excerptSourceFile.path}: region "$section" is empty');
|
||||
return '';
|
||||
}
|
||||
|
||||
return null;
|
||||
int indent = maxLength;
|
||||
for (final String line in buffer) {
|
||||
if (indent == 0) {
|
||||
break;
|
||||
}
|
||||
if (line.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
for (int index = 0; index < line.length; index += 1) {
|
||||
if (line[index] != ' ') {
|
||||
if (index < indent) {
|
||||
indent = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final StringBuffer excerpt = StringBuffer();
|
||||
for (final String line in buffer) {
|
||||
if (line.isEmpty) {
|
||||
excerpt.writeln();
|
||||
} else {
|
||||
excerpt.writeln(line.substring(indent));
|
||||
}
|
||||
}
|
||||
return excerpt.toString();
|
||||
}
|
||||
}
|
||||
|
@ -648,7 +648,6 @@ A B C
|
||||
final RepositoryPackage package = createFakePackage(
|
||||
'a_package',
|
||||
packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath],
|
||||
);
|
||||
|
||||
package.readmeFile.writeAsStringSync('''
|
||||
@ -672,40 +671,6 @@ A B C
|
||||
);
|
||||
});
|
||||
|
||||
test('fails when excerpts are used but the package is not configured',
|
||||
() async {
|
||||
final RepositoryPackage package =
|
||||
createFakePackage('a_package', packagesDir);
|
||||
|
||||
package.readmeFile.writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A B C
|
||||
```
|
||||
''');
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['readme-check', '--require-excerpts'],
|
||||
errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('code-excerpt tag found, but the package is not configured '
|
||||
'for excerpting. Follow the instructions at\n'
|
||||
'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages\n'
|
||||
'for setting up a build.excerpt.yaml file.'),
|
||||
contains('Missing code-excerpt configuration'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('fails on missing excerpt tag when requested', () async {
|
||||
final RepositoryPackage package =
|
||||
createFakePackage('a_package', packagesDir);
|
||||
|
@ -7,328 +7,92 @@ import 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_plugin_tools/src/common/core.dart';
|
||||
import 'package:flutter_plugin_tools/src/update_excerpts_command.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'common/package_command_test.mocks.dart';
|
||||
import 'mocks.dart';
|
||||
import 'util.dart';
|
||||
|
||||
void main() {
|
||||
void runAllTests(MockPlatform platform) {
|
||||
late FileSystem fileSystem;
|
||||
late Directory packagesDir;
|
||||
late RecordingProcessRunner processRunner;
|
||||
late CommandRunner<void> runner;
|
||||
|
||||
setUp(() {
|
||||
fileSystem = MemoryFileSystem();
|
||||
fileSystem = MemoryFileSystem(
|
||||
style: platform.isWindows
|
||||
? FileSystemStyle.windows
|
||||
: FileSystemStyle.posix);
|
||||
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
|
||||
final MockGitDir gitDir = MockGitDir();
|
||||
when(gitDir.path).thenReturn(packagesDir.parent.path);
|
||||
processRunner = RecordingProcessRunner();
|
||||
final UpdateExcerptsCommand command = UpdateExcerptsCommand(
|
||||
packagesDir,
|
||||
processRunner: processRunner,
|
||||
platform: MockPlatform(),
|
||||
gitDir: gitDir,
|
||||
runner = CommandRunner<void>('', '')
|
||||
..addCommand(UpdateExcerptsCommand(
|
||||
packagesDir,
|
||||
platform: platform,
|
||||
processRunner: RecordingProcessRunner(),
|
||||
gitDir: MockGitDir(),
|
||||
));
|
||||
});
|
||||
|
||||
Future<void> testInjection(String before, String source, String after,
|
||||
{bool failOnChange = false}) async {
|
||||
final RepositoryPackage package =
|
||||
createFakePackage('a_package', packagesDir);
|
||||
package.readmeFile.writeAsStringSync(before);
|
||||
package.directory.childFile('main.dart').writeAsStringSync(source);
|
||||
Object? errorObject;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner,
|
||||
<String>[
|
||||
'update-excerpts',
|
||||
if (failOnChange) '--fail-on-change',
|
||||
],
|
||||
errorHandler: (Object error) {
|
||||
errorObject = error;
|
||||
},
|
||||
);
|
||||
if (errorObject != null) {
|
||||
fail('Failed: $errorObject\n\nOutput from excerpt command:\n$output');
|
||||
}
|
||||
expect(package.readmeFile.readAsStringSync(), after);
|
||||
}
|
||||
|
||||
runner = CommandRunner<void>(
|
||||
'update_excerpts_command', 'Test for update_excerpts_command');
|
||||
runner.addCommand(command);
|
||||
});
|
||||
test('succeeds when nothing has changed', () async {
|
||||
const String readme = '''
|
||||
Example:
|
||||
|
||||
test('runs pub get before running scripts', () async {
|
||||
final RepositoryPackage package = createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
final Directory example = getExampleDir(package);
|
||||
|
||||
await runCapturingPrint(runner, <String>['update-excerpts']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
containsAll(<ProcessCall>[
|
||||
ProcessCall('flutter', const <String>['pub', 'get'], example.path),
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'build_runner',
|
||||
'build',
|
||||
'--config',
|
||||
'excerpt',
|
||||
'--output',
|
||||
UpdateExcerptsCommand.excerptOutputDir,
|
||||
'--delete-conflicting-outputs',
|
||||
],
|
||||
example.path),
|
||||
]));
|
||||
});
|
||||
|
||||
test('runs when config is present', () async {
|
||||
final RepositoryPackage package = createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
final Directory example = getExampleDir(package);
|
||||
|
||||
final List<String> output =
|
||||
await runCapturingPrint(runner, <String>['update-excerpts']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
containsAll(<ProcessCall>[
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'build_runner',
|
||||
'build',
|
||||
'--config',
|
||||
'excerpt',
|
||||
'--output',
|
||||
UpdateExcerptsCommand.excerptOutputDir,
|
||||
'--delete-conflicting-outputs',
|
||||
],
|
||||
example.path),
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'code_excerpt_updater',
|
||||
'--write-in-place',
|
||||
'--yaml',
|
||||
'--no-escape-ng-interpolation',
|
||||
'../README.md',
|
||||
],
|
||||
example.path),
|
||||
]));
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Ran for 1 package(s)'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('updates example readme when config is present', () async {
|
||||
final RepositoryPackage package = createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath, 'example/README.md']);
|
||||
final Directory example = getExampleDir(package);
|
||||
|
||||
final List<String> output =
|
||||
await runCapturingPrint(runner, <String>['update-excerpts']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
containsAll(<ProcessCall>[
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'build_runner',
|
||||
'build',
|
||||
'--config',
|
||||
'excerpt',
|
||||
'--output',
|
||||
UpdateExcerptsCommand.excerptOutputDir,
|
||||
'--delete-conflicting-outputs',
|
||||
],
|
||||
example.path),
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'code_excerpt_updater',
|
||||
'--write-in-place',
|
||||
'--yaml',
|
||||
'--no-escape-ng-interpolation',
|
||||
'README.md',
|
||||
],
|
||||
example.path),
|
||||
]));
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Ran for 1 package(s)'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('includes all top-level .md files', () async {
|
||||
const String otherMdFileName = 'another_file.md';
|
||||
final RepositoryPackage package = createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath, otherMdFileName]);
|
||||
final Directory example = getExampleDir(package);
|
||||
|
||||
final List<String> output =
|
||||
await runCapturingPrint(runner, <String>['update-excerpts']);
|
||||
|
||||
expect(
|
||||
processRunner.recordedCalls,
|
||||
containsAll(<ProcessCall>[
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'build_runner',
|
||||
'build',
|
||||
'--config',
|
||||
'excerpt',
|
||||
'--output',
|
||||
UpdateExcerptsCommand.excerptOutputDir,
|
||||
'--delete-conflicting-outputs',
|
||||
],
|
||||
example.path),
|
||||
ProcessCall(
|
||||
'dart',
|
||||
const <String>[
|
||||
'run',
|
||||
'code_excerpt_updater',
|
||||
'--write-in-place',
|
||||
'--yaml',
|
||||
'--no-escape-ng-interpolation',
|
||||
'../README.md',
|
||||
'../$otherMdFileName',
|
||||
],
|
||||
example.path),
|
||||
]));
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Ran for 1 package(s)'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('skips when no config is present', () async {
|
||||
createFakePlugin('a_package', packagesDir);
|
||||
|
||||
final List<String> output =
|
||||
await runCapturingPrint(runner, <String>['update-excerpts']);
|
||||
|
||||
expect(processRunner.recordedCalls, isEmpty);
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Skipped 1 package(s)'),
|
||||
]));
|
||||
});
|
||||
|
||||
test('restores pubspec even if running the script fails', () async {
|
||||
final RepositoryPackage package = createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
|
||||
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get'])
|
||||
];
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
// Check that it's definitely a failure in a step between making the changes
|
||||
// and restoring the original.
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('The following packages had errors:'),
|
||||
contains('a_package:\n'
|
||||
' Unable to get script dependencies')
|
||||
]));
|
||||
|
||||
final String examplePubspecContent =
|
||||
package.getExamples().first.pubspecFile.readAsStringSync();
|
||||
expect(examplePubspecContent, isNot(contains('code_excerpter')));
|
||||
expect(examplePubspecContent, isNot(contains('code_excerpt_updater')));
|
||||
});
|
||||
|
||||
test('fails if pub get fails', () async {
|
||||
createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
|
||||
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['pub', 'get'])
|
||||
];
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('The following packages had errors:'),
|
||||
contains('a_package:\n'
|
||||
' Unable to get script dependencies')
|
||||
]));
|
||||
});
|
||||
|
||||
test('fails if extraction fails', () async {
|
||||
createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
|
||||
processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(exitCode: 1), <String>['run', 'build_runner'])
|
||||
];
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('The following packages had errors:'),
|
||||
contains('a_package:\n'
|
||||
' Unable to extract excerpts')
|
||||
]));
|
||||
});
|
||||
|
||||
test('fails if injection fails', () async {
|
||||
createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
|
||||
processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(), <String>['run', 'build_runner']),
|
||||
FakeProcessInfo(
|
||||
MockProcess(exitCode: 1), <String>['run', 'code_excerpt_updater']),
|
||||
];
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts'], errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('The following packages had errors:'),
|
||||
contains('a_package:\n'
|
||||
' Unable to inject excerpts')
|
||||
]));
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A B C
|
||||
```
|
||||
''';
|
||||
const String source = '''
|
||||
FAIL
|
||||
// #docregion SomeSection
|
||||
A B C
|
||||
// #enddocregion SomeSection
|
||||
FAIL
|
||||
''';
|
||||
await testInjection(readme, source, readme);
|
||||
});
|
||||
|
||||
test('fails if example injection fails', () async {
|
||||
createFakePlugin('a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath, 'example/README.md']);
|
||||
final RepositoryPackage package =
|
||||
createFakePackage('a_package', packagesDir);
|
||||
package.readmeFile.writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(), <String>['run', 'build_runner']),
|
||||
FakeProcessInfo(MockProcess(), <String>['run', 'code_excerpt_updater']),
|
||||
FakeProcessInfo(
|
||||
MockProcess(exitCode: 1), <String>['run', 'code_excerpt_updater']),
|
||||
];
|
||||
<?code-excerpt "main.dart (UnknownSection)"?>
|
||||
```dart
|
||||
A B C
|
||||
```
|
||||
''');
|
||||
package.directory.childFile('main.dart').writeAsStringSync('''
|
||||
FAIL
|
||||
// #docregion SomeSection
|
||||
A B C
|
||||
// #enddocregion SomeSection
|
||||
FAIL
|
||||
''');
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
@ -338,22 +102,61 @@ void main() {
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('The following packages had errors:'),
|
||||
contains('a_package:\n'
|
||||
' Unable to inject example excerpts')
|
||||
]));
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Injecting excerpts failed:'),
|
||||
contains(
|
||||
'main.dart: did not find a "// #docregion UnknownSection" pragma'),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('updates files', () async {
|
||||
await testInjection(
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
X Y Z
|
||||
```
|
||||
''',
|
||||
'''
|
||||
FAIL
|
||||
// #docregion SomeSection
|
||||
A B C
|
||||
// #enddocregion SomeSection
|
||||
FAIL
|
||||
''',
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A B C
|
||||
```
|
||||
''',
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if READMEs are changed with --fail-on-change', () async {
|
||||
createFakePlugin('a_plugin', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
final RepositoryPackage package =
|
||||
createFakePackage('a_package', packagesDir);
|
||||
package.readmeFile.writeAsStringSync('''
|
||||
Example:
|
||||
|
||||
const String changedFilePath = 'README.md';
|
||||
processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(stdout: changedFilePath)),
|
||||
];
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
X Y Z
|
||||
```
|
||||
''');
|
||||
package.directory.childFile('main.dart').writeAsStringSync('''
|
||||
FAIL
|
||||
// #docregion SomeSection
|
||||
A B C
|
||||
// #enddocregion SomeSection
|
||||
FAIL
|
||||
''');
|
||||
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
@ -364,101 +167,259 @@ void main() {
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains(
|
||||
'One or more .md files are out of sync with their source excerpts'),
|
||||
contains('Snippets are out of sync in the following files: '
|
||||
'$changedFilePath'),
|
||||
]));
|
||||
output.join('\n'),
|
||||
contains('The following files have out of date excerpts:'),
|
||||
);
|
||||
});
|
||||
|
||||
test('passes if unrelated files are changed with --fail-on-change', () async {
|
||||
createFakePlugin('a_plugin', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
test('does not fail if READMEs are not changed with --fail-on-change',
|
||||
() async {
|
||||
const String readme = '''
|
||||
Example:
|
||||
|
||||
const String changedFilePath = 'packages/a_plugin/linux/CMakeLists.txt';
|
||||
processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(stdout: changedFilePath)),
|
||||
];
|
||||
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts', '--fail-on-change']);
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Ran for 1 package(s)'),
|
||||
]));
|
||||
<?code-excerpt "main.dart (aa)"?>
|
||||
```dart
|
||||
A
|
||||
```
|
||||
<?code-excerpt "main.dart (bb)"?>
|
||||
```dart
|
||||
B
|
||||
```
|
||||
''';
|
||||
await testInjection(
|
||||
readme,
|
||||
'''
|
||||
// #docregion aa
|
||||
A
|
||||
// #enddocregion aa
|
||||
// #docregion bb
|
||||
B
|
||||
// #enddocregion bb
|
||||
''',
|
||||
readme,
|
||||
failOnChange: true,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if git ls-files fails', () async {
|
||||
createFakePlugin('a_plugin', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
test('indents the plaster', () async {
|
||||
await testInjection(
|
||||
'''
|
||||
Example:
|
||||
|
||||
processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(exitCode: 1))
|
||||
];
|
||||
Error? commandError;
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts', '--fail-on-change'],
|
||||
errorHandler: (Error e) {
|
||||
commandError = e;
|
||||
});
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
```
|
||||
''',
|
||||
'''
|
||||
// #docregion SomeSection
|
||||
A
|
||||
// #enddocregion SomeSection
|
||||
// #docregion SomeSection
|
||||
B
|
||||
// #enddocregion SomeSection
|
||||
''',
|
||||
'''
|
||||
Example:
|
||||
|
||||
expect(commandError, isA<ToolExit>());
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Unable to determine local file state'),
|
||||
]));
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A
|
||||
// ···
|
||||
B
|
||||
```
|
||||
''',
|
||||
);
|
||||
});
|
||||
|
||||
test('cleans up excerpt output by default', () async {
|
||||
final RepositoryPackage package = createFakePackage(
|
||||
'a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
// Simulate the creation of the output directory.
|
||||
final Directory excerptOutputDir = package
|
||||
.getExamples()
|
||||
.first
|
||||
.directory
|
||||
.childDirectory(UpdateExcerptsCommand.excerptOutputDir);
|
||||
excerptOutputDir.createSync(recursive: true);
|
||||
test('does not unindent blocks if plaster will not unindent', () async {
|
||||
await testInjection(
|
||||
'''
|
||||
Example:
|
||||
|
||||
const String changedFilePath = 'packages/a_plugin/linux/CMakeLists.txt';
|
||||
processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(stdout: changedFilePath)),
|
||||
];
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
```
|
||||
''',
|
||||
'''
|
||||
// #docregion SomeSection
|
||||
A
|
||||
// #enddocregion SomeSection
|
||||
// #docregion SomeSection
|
||||
B
|
||||
// #enddocregion SomeSection
|
||||
''',
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A
|
||||
// ···
|
||||
B
|
||||
```
|
||||
''',
|
||||
);
|
||||
});
|
||||
|
||||
test('unindents blocks', () async {
|
||||
await testInjection(
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
```
|
||||
''',
|
||||
'''
|
||||
// #docregion SomeSection
|
||||
A
|
||||
// #enddocregion SomeSection
|
||||
// #docregion SomeSection
|
||||
B
|
||||
// #enddocregion SomeSection
|
||||
''',
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A
|
||||
// ···
|
||||
B
|
||||
```
|
||||
''',
|
||||
);
|
||||
});
|
||||
|
||||
test('unindents blocks and plaster', () async {
|
||||
await testInjection(
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
```
|
||||
''',
|
||||
'''
|
||||
// #docregion SomeSection
|
||||
A
|
||||
// #enddocregion SomeSection
|
||||
// #docregion SomeSection
|
||||
B
|
||||
// #enddocregion SomeSection
|
||||
''',
|
||||
'''
|
||||
Example:
|
||||
|
||||
<?code-excerpt "main.dart (SomeSection)"?>
|
||||
```dart
|
||||
A
|
||||
// ···
|
||||
B
|
||||
```
|
||||
''',
|
||||
);
|
||||
});
|
||||
|
||||
test('relative path bases', () async {
|
||||
final RepositoryPackage package =
|
||||
createFakePackage('a_package', packagesDir);
|
||||
package.readmeFile.writeAsStringSync('''
|
||||
<?code-excerpt "main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt "test/main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt "test/test/main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt path-base="test"?>
|
||||
<?code-excerpt "main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt "../main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt "test/main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt path-base="/packages/a_package"?>
|
||||
<?code-excerpt "main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
<?code-excerpt "test/main.dart (a)"?>
|
||||
```dart
|
||||
```
|
||||
''');
|
||||
package.directory.childFile('main.dart').writeAsStringSync('''
|
||||
// #docregion a
|
||||
X
|
||||
// #enddocregion a
|
||||
''');
|
||||
package.directory.childDirectory('test').createSync();
|
||||
package.directory
|
||||
.childDirectory('test')
|
||||
.childFile('main.dart')
|
||||
.writeAsStringSync('''
|
||||
// #docregion a
|
||||
Y
|
||||
// #enddocregion a
|
||||
''');
|
||||
package.directory
|
||||
.childDirectory('test')
|
||||
.childDirectory('test')
|
||||
.createSync();
|
||||
package.directory
|
||||
.childDirectory('test')
|
||||
.childDirectory('test')
|
||||
.childFile('main.dart')
|
||||
.writeAsStringSync('''
|
||||
// #docregion a
|
||||
Z
|
||||
// #enddocregion a
|
||||
''');
|
||||
await runCapturingPrint(runner, <String>['update-excerpts']);
|
||||
|
||||
expect(excerptOutputDir.existsSync(), false);
|
||||
});
|
||||
|
||||
test('cleans up excerpt output by default', () async {
|
||||
final RepositoryPackage package = createFakePackage(
|
||||
'a_package', packagesDir,
|
||||
extraFiles: <String>[kReadmeExcerptConfigPath]);
|
||||
// Simulate the creation of the output directory.
|
||||
const String outputDirName = UpdateExcerptsCommand.excerptOutputDir;
|
||||
final Directory excerptOutputDir =
|
||||
package.getExamples().first.directory.childDirectory(outputDirName);
|
||||
excerptOutputDir.createSync(recursive: true);
|
||||
|
||||
const String changedFilePath = 'packages/a_plugin/linux/CMakeLists.txt';
|
||||
processRunner.mockProcessesForExecutable['git'] = <FakeProcessInfo>[
|
||||
FakeProcessInfo(MockProcess(stdout: changedFilePath)),
|
||||
];
|
||||
|
||||
final List<String> output = await runCapturingPrint(
|
||||
runner, <String>['update-excerpts', '--no-cleanup']);
|
||||
|
||||
expect(
|
||||
output,
|
||||
containsAllInOrder(<Matcher>[
|
||||
contains('Extraction output is in example/$outputDirName/'),
|
||||
]));
|
||||
expect(excerptOutputDir.existsSync(), true);
|
||||
expect(package.readmeFile.readAsStringSync(), '''
|
||||
<?code-excerpt "main.dart (a)"?>
|
||||
```dart
|
||||
X
|
||||
```
|
||||
<?code-excerpt "test/main.dart (a)"?>
|
||||
```dart
|
||||
Y
|
||||
```
|
||||
<?code-excerpt "test/test/main.dart (a)"?>
|
||||
```dart
|
||||
Z
|
||||
```
|
||||
<?code-excerpt path-base="test"?>
|
||||
<?code-excerpt "main.dart (a)"?>
|
||||
```dart
|
||||
Y
|
||||
```
|
||||
<?code-excerpt "../main.dart (a)"?>
|
||||
```dart
|
||||
X
|
||||
```
|
||||
<?code-excerpt "test/main.dart (a)"?>
|
||||
```dart
|
||||
Z
|
||||
```
|
||||
<?code-excerpt path-base="/packages/a_package"?>
|
||||
<?code-excerpt "main.dart (a)"?>
|
||||
```dart
|
||||
X
|
||||
```
|
||||
<?code-excerpt "test/main.dart (a)"?>
|
||||
```dart
|
||||
Y
|
||||
```
|
||||
''');
|
||||
});
|
||||
}
|
||||
|
||||
void main() {
|
||||
runAllTests(MockPlatform());
|
||||
runAllTests(MockPlatform(isWindows: true));
|
||||
}
|
||||
|
@ -23,13 +23,6 @@ import 'mocks.dart';
|
||||
|
||||
export 'package:flutter_plugin_tools/src/common/repository_package.dart';
|
||||
|
||||
/// The relative path from a package to the file that is used to enable
|
||||
/// README excerpting for a package.
|
||||
// This is a shared constant to ensure that both readme-check and
|
||||
// update-excerpt are looking for the same file, so that readme-check can't
|
||||
// get out of sync with what actually drives excerpting.
|
||||
const String kReadmeExcerptConfigPath = 'example/build.excerpt.yaml';
|
||||
|
||||
const String _defaultDartConstraint = '>=2.14.0 <4.0.0';
|
||||
const String _defaultFlutterConstraint = '>=2.5.0';
|
||||
|
||||
|
Submodule site-shared deleted from 8c92e5bdfd
Reference in New Issue
Block a user