Skip to content

Commit

Permalink
Merge pull request #133 from fusion44/feat/analyzer_plugin
Browse files Browse the repository at this point in the history
Create jaspr_lints package
  • Loading branch information
Kilian Schulte authored Sep 12, 2024
2 parents 4f11fd3 + 6a91190 commit 5d8dddb
Show file tree
Hide file tree
Showing 32 changed files with 1,465 additions and 49 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,11 @@ Rather it embraces these differences to give the best of both worlds.
- **/examples**: Well-maintained and documented examples
- **/experiments**: Experimental apps or features, that are not part of the core framework (yet?) (may be broken).
- **/packages**:
- [**/jaspr**](https://github.com/schultek/jaspr/tree/main/packages/jaspr): The core framework package.
- **/jaspr**: The core framework package.
- **/jaspr_builder**: Code-generation builders for jaspr.
- **/jaspr_cli**: The command line interface of jaspr.
- **/jaspr_flutter_embed**: Flutter element embedding bindings for jaspr.
- **/jaspr_lints**: A collection of lints and assists for jaspr projects.
- **/jaspr_riverpod**: Riverpod implementation for jaspr.
- **/jaspr_router**: A router implementation for jaspr.
- **/jaspr_test**: A testing package for jaspr.
Expand Down
43 changes: 22 additions & 21 deletions apps/fluttercon/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ class App extends AsyncStatelessComponent {
final sessions = (sessionsJson as List).map((s) => SessionMapper.fromMap(s)).toList();

yield Router(
redirect: (context, state) {
if (state.location == '/') return '/day-1';
return null;
},
routes: [
for (var i = 1; i < 4; i++)
Route(
path: '/day-$i',
title: 'Fluttercon Berlin 2024',
builder: (context, state) => SchedulePage(
day: i,
sessions: sessions.where((s) => s.startsAt.day == i + 2).toList(),
),
redirect: (context, state) {
if (state.location == '/') return '/day-1';
return null;
},
routes: [
for (var i = 1; i < 4; i++)
Route(
path: '/day-$i',
title: 'Fluttercon Berlin 2024',
builder: (context, state) => SchedulePage(
day: i,
sessions: sessions.where((s) => s.startsAt.day == i + 2).toList(),
),
Route(path: '/favorites', title: 'Favorites', builder: (context, state) => FavoritesPage()),
for (var session in sessions)
Route(
path: '/${session.slug}',
title: session.title,
builder: (context, state) => SessionPage(session: session),
),
]);
),
Route(path: '/favorites', title: 'Favorites', builder: (context, state) => FavoritesPage()),
for (var session in sessions)
Route(
path: '/${session.slug}',
title: session.title,
builder: (context, state) => SessionPage(session: session),
),
],
);
}
}
2 changes: 0 additions & 2 deletions apps/fluttercon/lib/components/session_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,5 @@ class SessionCard extends StatelessComponent {
css('div').flexbox(),
css('button').box(position: Position.absolute(right: 10.px, top: 10.px)),
]),
...LikeButton.styles,
...Tag.styles,
];
}
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
{"title": "\uD83D\uDD79 Jaspr CLI", "href": "/get_started/cli"},
{"title": "\uD83D\uDCDF Rendering Modes", "href": "/get_started/modes"},
{"title": "\uD83D\uDCA7 Hydration", "href": "/get_started/hydration"},
{"title": "\uD83D\uDCE6 Project Structure", "href": "/get_started/project_structure"}
{"title": "\uD83D\uDCE6 Project Structure", "href": "/get_started/project_structure"},
{"title": "\uD83E\uDDF9 Linting", "href": "/get_started/linting"}
]
},
{
Expand Down
207 changes: 207 additions & 0 deletions docs/get_started/linting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
title: Linting
description: Jaspr comes with its own set of lint rules and code assists.
---

Jaspr has its own linting and analyzer package called `jaspr_lints`. This comes pre-configured
with every new project and enables the following set of lint rules and code assists:

## Lint Rules

<Card>
<Property name="prefer_html_methods" type="Fix available">
Prefer html methods like `div(...)` over `DomComponent(tag: 'div', ...)`.

**BAD:**
```dart
yield DomComponent(
tag: 'div',
children: [
DomComponent(
tag: 'p',
child: Text('Hello World'),
),
],
);
```

**GOOD:**
```dart
yield div([
p([text('Hello World')]),
]);
```
</Property>
</Card>

<Card>
<Property name="sort_children_properties_last" type="Fix available">
Sort children properties last in html component methods.

This improves readability and best represents actual html.

**BAD:**
```dart
yield div([
p([text('Hello World')], classes: 'text-red'),
], id: 'main');
```

**GOOD:**
```dart
yield div(id: 'main', [
p(classes: 'text-red', [text('Hello World')]),
]);
```
</Property>
</Card>

## Code Assists

<Card>
<Property name="Create StatelessComponent" type="Top level">
Creates a new `StatelessComponent` class.
</Property>
</Card>

<Card>
<Property name="Create StatefulComponent" type="Top level">
Creates a new `StatefulComponent` and respective `State` class.
</Property>
</Card>

<Card>
<Property name="Create InheritedComponent" type="Top level">
Creates a new `InheritedComponent` class.
</Property>
</Card>

<Card>
<Property name="Convert to StatefulComponent" type="Class level">
Converts a custom `StatelessComponent` into a `StatefulComponent` and respective `State` class.
</Property>
</Card>

<Card>
<Property name="Convert to AsyncStatelessComponent" type="Class level">
Converts a custom `StatelessComponent` into an `AsyncStatelessComponent`.
</Property>
</Card>

<Card>
<Property name="Remove this component" type="Component tree level">
Surgically removes the selected component from the component tree, making its
children the new direct children of its parent.
</Property>
</Card>

<Card>
<Property name="Wrap with html..." type="Component tree level">
Wraps the selected component with a new html component.

```dart
yield div([
p([ // [!code ++]
span([text('Hello World')]),
]), // [!code ++]
]);
```
</Property>
</Card>

<Card>
<Property name="Wrap with component..." type="Component tree level">
Wraps the selected component with a new component.

```dart
yield div([
MyComponent( // [!code ++]
child: span([text('Hello World')]),
), // [!code ++]
]);
```
</Property>
</Card>

<Card>
<Property name="Wrap with Builder" type="Component tree level">
Wraps the selected component with a `Builder`.

```dart
yield div([
Builder(builder: (context) sync* { // [!code ++]
yield span([text('Hello World')]);
}), // [!code ++]
]);
```
</Property>
</Card>

<Card>
<Property name="Extract component" type="Component tree level">
Extracts the selected component plus subtree into a new `StatelessComponent`.
</Property>
</Card>

<Card>
<Property name="Add styles" type="Component tree level">
Adds new css styles to the selected component.

This will either add a new class name:
```dart
yield div(
classes: '[...]' // [!code ++]
[],
);

/* ... */

@css // [!code ++]
static List<StyleRule> styles = [ // [!code ++]
css('.[...]').box(...), // [!code ++]
]; // [!code ++]
```

Or use an existing id or class name:
```dart
yield div(id: 'content', []);

/* ... */

@css // [!code ++]
static List<StyleRule> styles = [ // [!code ++]
css('#content').box(...), // [!code ++]
]; // [!code ++]
```
</Property>
</Card>

<Card>
<Property name="Convert to web-only import" type="Directive level">
Converts any import to a web-only import using the [@Import](/utils/at_import) annotation.

```dart
import 'dart:html'; // [!code --]
@Import.onWeb('dart:html', show: [#window]) // [!code ++]
import 'filename.imports.dart'; // [!code ++]
```

This will auto-detect all used members from the import and add them to the `show` parameter.
</Property>
</Card>

<Card>
<Property name="Convert to server-only import">
Converts any import to a server-only import using the [@Import](/utils/at_import) annotation.

```dart
import 'dart:io'; // [!code --]
@Import.onServer('dart:io', show: [#HttpRequest]) // [!code ++]
import 'filename.imports.dart'; // [!code ++]
```

This will auto-detect all used members from the import and add them to the `show` parameter.
</Property>
</Card>
4 changes: 4 additions & 0 deletions docs/utils/at_import.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ This will:
- setup conditional imports for `dart:html` on the web
- write stubs on the server for all imported members.

<Success>
There is a code-assist available from `jaspr_lints` that converts your imports automatically!
See [Linting](/get_started/linting).
</Success>
---

You can extend this to multiple imports and mix web and server imports like this:
Expand Down
6 changes: 6 additions & 0 deletions packages/jaspr/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## Unreleased minor
<<<<<<< feat/analyzer_plugin

- Include and setup `jaspr_lints` in newly created projects.
- Added `jaspr analyze` command to check all custom lints.
=======
- Added css variable for Unit, Color, Angle and FontFamily.
>>>>>>> main
## 0.15.0

Expand Down
6 changes: 6 additions & 0 deletions packages/jaspr/tool/generate_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ void main() {
var specFile = File('tool/data/html.json');
var specJson = jsonDecode(specFile.readAsStringSync()) as Map<String, dynamic>;

var allTags = <String>{};

for (var key in specJson.keys) {
var group = specJson[key] as Map<String, dynamic>;
var file = File('lib/src/components/html/$key.dart');
Expand All @@ -19,6 +21,7 @@ void main() {
continue;
}

allTags.add(tag);
content.write('\n${data['doc'].split('\n').map((t) => '/// $t\n').join()}');

var attrs = data['attributes'] as Map<String, dynamic>?;
Expand Down Expand Up @@ -206,4 +209,7 @@ void main() {

file.writeAsStringSync(content.toString());
}

var lintFile = File('../jaspr_lints/lib/src/all_html_tags.dart');
lintFile.writeAsStringSync('const allHtmlTags = {${allTags.map((t) => "'$t'").join(', ')}};\n');
}
2 changes: 2 additions & 0 deletions packages/jaspr_cli/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:cli_completion/cli_completion.dart';
import 'package:mason/mason.dart';
import 'package:pub_updater/pub_updater.dart';

import 'commands/analyze_command.dart';
import 'commands/base_command.dart';
import 'commands/build_command.dart';
import 'commands/clean_command.dart';
Expand Down Expand Up @@ -37,6 +38,7 @@ class JasprCommandRunner extends CompletionCommandRunner<CommandResult?> {
addCommand(CreateCommand());
addCommand(ServeCommand());
addCommand(BuildCommand());
addCommand(AnalyzeCommand());
addCommand(CleanCommand());
addCommand(UpdateCommand());
addCommand(DoctorCommand());
Expand Down
42 changes: 42 additions & 0 deletions packages/jaspr_cli/lib/src/commands/analyze_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:io';

import '../logging.dart';
import 'base_command.dart';

class AnalyzeCommand extends BaseCommand {
AnalyzeCommand({super.logger}) {
argParser.addFlag(
'fatal-infos',
help: 'Treat info level issues as fatal',
defaultsTo: true,
);
argParser.addFlag(
'fatal-warnings',
help: 'Treat warning level issues as fatal',
defaultsTo: true,
);
argParser.addFlag(
'fix',
help: 'Apply all possible fixes to the lint issues found.',
negatable: false,
);
}

@override
String get description => 'Report Jaspr specific lint warnings.';

@override
String get name => 'analyze';

@override
String get category => 'Tooling';

@override
Future<CommandResult?> run() async {
await super.run();

var process = await Process.start('dart', ['run', 'custom_lint', ...?argResults?.arguments]);

return CommandResult.running(watchProcess('custom_lint', process, tag: Tag.none), stop);
}
}
Loading

0 comments on commit 5d8dddb

Please sign in to comment.