Skip to content

Commit

Permalink
feat(dart_frog_cli): add dart_frog_new_brick (#623)
Browse files Browse the repository at this point in the history
  • Loading branch information
renancaraujo authored May 12, 2023
1 parent fbb8ca0 commit 8543fdb
Show file tree
Hide file tree
Showing 25 changed files with 1,315 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/dart_frog_new.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: dart_frog_new

on:
pull_request:
paths:
- ".github/workflows/dart_frog_new.yaml"
- "bricks/dart_frog_new/hooks/**"
push:
branches:
- main
paths:
- ".github/workflows/dart_frog_new.yaml"
- "bricks/dart_frog_new/hooks/**"

jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
with:
runs_on: macos-latest
working_directory: bricks/dart_frog_new/hooks
analyze_directories: .
21 changes: 21 additions & 0 deletions bricks/dart_frog_new/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Very Good Ventures

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
9 changes: 9 additions & 0 deletions bricks/dart_frog_new/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# dart_frog_new

[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason)

A dart frog brick to create routes and middleware.

_Generated by [mason][1] 🧱_

[1]: https://github.com/felangel/mason
3 changes: 3 additions & 0 deletions bricks/dart_frog_new/__brick__/{{filename}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:dart_frog/dart_frog.dart';

{{> route.dart}}
7 changes: 7 additions & 0 deletions bricks/dart_frog_new/__brick__/{{~ route.dart }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{^params}}Response onRequest(RequestContext context) {
{{/params}}{{#params.0}}Response onRequest(
RequestContext context,{{#params}}
String {{.}},{{/params}}
) {
{{/params.0}} return Response(body: 'Welcome to Dart Frog!');
}
20 changes: 20 additions & 0 deletions bricks/dart_frog_new/brick.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: dart_frog_new
description: A dart frog brick to create routes and middleware
version: 0.1.0+1

environment:
mason: ">=0.1.0-dev <0.1.0"

vars:
route_path:
type: string
description: The path for the desired route
prompt: What is the path for the desired route?
type:
type: enum
description: Whether to create a route or a middleware
prompt: Want to create a route or a middleware?
default: route
values:
- route
- middleware
4 changes: 4 additions & 0 deletions bricks/dart_frog_new/hooks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.dart_tool
.packages
pubspec.lock
build
5 changes: 5 additions & 0 deletions bricks/dart_frog_new/hooks/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include: package:very_good_analysis/analysis_options.4.0.0.yaml

linter:
rules:
public_member_api_docs: false
30 changes: 30 additions & 0 deletions bricks/dart_frog_new/hooks/lib/post_gen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:io' as io;

import 'package:dart_frog_new_hooks/src/exit_overrides.dart';
import 'package:mason/mason.dart';
import 'package:path/path.dart' as path;

void _defaultExit(int code) => ExitOverrides.current?.exit ?? io.exit;

Future<void> postGen(
HookContext context, {
io.Directory? directory,
void Function(int exitCode) exit = _defaultExit,
}) async {
final dirPath = context.vars['dir_path'] as String;
final currentDirectory = directory ?? io.Directory.current;

final containingDirectoryPath = path.relative(
io.Directory(path.join(currentDirectory.path, dirPath)).path,
);
final filename = context.vars['filename'] as String;
try {
io.Directory(containingDirectoryPath).createSync(recursive: true);
io.File(
path.join(currentDirectory.path, filename),
).renameSync('$containingDirectoryPath/$filename');
} catch (error) {
context.logger.err('$error');
return exit(1);
}
}
131 changes: 131 additions & 0 deletions bricks/dart_frog_new/hooks/lib/pre_gen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import 'dart:io' as io;

import 'package:dart_frog_gen/dart_frog_gen.dart';
import 'package:dart_frog_new_hooks/src/exit_overrides.dart';
import 'package:dart_frog_new_hooks/src/normalize_route_path.dart';
import 'package:dart_frog_new_hooks/src/parameter_syntax.dart';
import 'package:dart_frog_new_hooks/src/route_configuration_utils.dart';
import 'package:dart_frog_new_hooks/src/route_to_path.dart';

import 'package:mason/mason.dart';
import 'package:path/path.dart' as path;

typedef RouteConfigurationBuilder = RouteConfiguration Function(
io.Directory directory,
);

void _defaultExit(int code) => ExitOverrides.current?.exit ?? io.exit;

void preGen(
HookContext context, {
io.Directory? directory,
RouteConfigurationBuilder buildConfiguration = buildRouteConfiguration,
void Function(int exitCode) exit = _defaultExit,
}) {
// The dart frog server project directory
final projectDirectory = directory ?? io.Directory.current;

// Build the route configuration
final RouteConfiguration routeConfiguration;
try {
routeConfiguration = buildConfiguration(projectDirectory);
} catch (error) {
context.logger.err('$error');
return exit(1);
}

// Get the desired type of creation
final type = context.vars['type'] as String;

// Verify if current route configuration have conflicts and bail out if
// any are found
try {
routeConfiguration.validate();
} on FormatException catch (exception) {
context.logger.err('Failed to create $type: ${exception.message}');
return exit(1);
}

// The path in which the route will be created
final routePath = normalizeRoutePath(context.vars['route_path'] as String);

return _preGenRoute(
context,
routePath: routePath,
routeConfiguration: routeConfiguration,
projectDirectory: projectDirectory,
exit: exit,
);
}

void _preGenRoute(
HookContext context, {
required String routePath,
required RouteConfiguration routeConfiguration,
required io.Directory projectDirectory,
required void Function(int exitCode) exit,
}) {
final routesDirectoryPath = path.relative(
io.Directory(path.join(projectDirectory.path, 'routes')).path,
);

// Verify if the endpoint does already exist.
final endpointExists = routeConfiguration.endpoints.containsKey(routePath);
if (endpointExists) {
context.logger.err('Failed to create route: $routePath already exists.');
return exit(1);
}

// Verify if the given route already exists as directory.
final existsAsDirectory = io.Directory(
path.withoutExtension(
routeToPath(
routePath,
preamble: routesDirectoryPath,
).toBracketParameterSyntax,
),
).existsSync();

// If the route does not exist as directory, we must check if any of its
// ancestor routes exists as file routes to avoid rogue routes.
if (!existsAsDirectory) {
final fileRoute = routeConfiguration.containingFileRoute(routePath);

if (fileRoute != null) {
final filepath = path.normalize(
path.join(
routesDirectoryPath,
fileRoute.path,
),
);

io.Directory(path.withoutExtension(filepath)).createSync();

final newFilepath = filepath.replaceFirst('.dart', '/index.dart');
io.File(filepath).renameSync(newFilepath);
context.logger.detail(
'Renamed $filepath to $newFilepath to avoid rogue routes',
);
}
}

final routeFileName = routeToPath(
routePath,
preferIndex: existsAsDirectory,
preamble: routesDirectoryPath,
).toBracketParameterSyntax;

context.logger.detail('Creating route file: $routeFileName');

final List<String> parameterNames;
try {
parameterNames = routeFileName.getParameterNames();
} on FormatException catch (exception) {
context.logger.err('Failed to create route: ${exception.message}');
return exit(1);
}

context.vars['dir_path'] = path.dirname(routeFileName);
context.vars['filename'] = path.basename(routeFileName);
context.vars['params'] = parameterNames;
}
31 changes: 31 additions & 0 deletions bricks/dart_frog_new/hooks/lib/src/exit_overrides.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'dart:async';
import 'dart:io' as io;

const _asyncRunZoned = runZoned;

abstract class ExitOverrides {
static final _token = Object();

static ExitOverrides? get current {
return Zone.current[_token] as ExitOverrides?;
}

static R runZoned<R>(R Function() body, {void Function(int)? exit}) {
final overrides = _ExitOverridesScope(exit);
return _asyncRunZoned(body, zoneValues: {_token: overrides});
}

void Function(int exitCode) get exit => io.exit;
}

class _ExitOverridesScope extends ExitOverrides {
_ExitOverridesScope(this._exit);

final ExitOverrides? _previous = ExitOverrides.current;
final void Function(int exitCode)? _exit;

@override
void Function(int exitCode) get exit {
return _exit ?? _previous?.exit ?? super.exit;
}
}
31 changes: 31 additions & 0 deletions bricks/dart_frog_new/hooks/lib/src/normalize_route_path.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:dart_frog_new_hooks/src/parameter_syntax.dart';

String normalizeRoutePath(String routePath) {
final replaced = routePath.toDiamondParameterSyntax.replaceAll(r'\', '/');

final segments = replaced.split('/');

final normalizedSegments =
segments.fold(<String>[], (previousValue, segment) {
if (segment == '..') {
if (previousValue.length > 1) {
previousValue.removeLast();
}
} else if (segment.isNotEmpty && segment != '.') {
previousValue.add(segment.encodeSegment());
}
return previousValue;
});

return '/${normalizedSegments.join('/')}';
}

extension on String {
String encodeSegment() {
final encoded = Uri.encodeComponent(this);
if (hasDiamondParameter) {
return encoded.replaceAll('%3C', '<').replaceAll('%3E', '>');
}
return encoded;
}
}
39 changes: 39 additions & 0 deletions bricks/dart_frog_new/hooks/lib/src/parameter_syntax.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
extension ParameterSyntax on String {
/// Replaces [] for <>
String get toDiamondParameterSyntax {
return replaceAll('[', '<').replaceAll(']', '>');
}

/// Replaces <> for []
String get toBracketParameterSyntax {
return replaceAll('<', '[').replaceAll('>', ']');
}

/// Detect if the given string has a < and a > after it
bool get hasDiamondParameter {
final regexp = RegExp('<.*?>');
return regexp.hasMatch(this);
}

/// Get the route parameters from the given string.
List<String> getParameterNames() {
final regexp = RegExp(r'\[(.*?)\]');
final names = regexp
.allMatches(toBracketParameterSyntax)
.map((m) => m[0]?.replaceAll(RegExp(r'[\[\]]'), ''))
.where((el) => el != null)
.cast<String>();

final duplicates = names
.toSet()
.where((element) => names.where((el) => el == element).length > 1);
if (duplicates.isNotEmpty) {
final plural = duplicates.length > 1;
final message = 'Duplicate parameter name${plural ? 's' : ''} found: '
'${duplicates.join(', ')}';
throw FormatException(message);
}

return names.toList();
}
}
Loading

0 comments on commit 8543fdb

Please sign in to comment.