Skip to content

Commit 44b39a1

Browse files
authored
feat(dart_frig_cli): add new command to the cli (#625)
1 parent f10004e commit 44b39a1

File tree

12 files changed

+915
-21
lines changed

12 files changed

+915
-21
lines changed

packages/dart_frog_cli/lib/src/command.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ abstract class DartFrogCommand extends Command<int> {
2020
@visibleForTesting
2121
String? testUsage;
2222

23+
/// Current working directory used for testing purposes only.
24+
Directory? testCwd;
25+
2326
/// [ArgResults] for the current command.
2427
ArgResults get results => testArgResults ?? argResults!;
2528

@@ -32,5 +35,5 @@ abstract class DartFrogCommand extends Command<int> {
3235
Logger? _logger;
3336

3437
/// Return the current working directory.
35-
Directory get cwd => Directory.current;
38+
Directory get cwd => testCwd ?? Directory.current;
3639
}

packages/dart_frog_cli/lib/src/command_runner.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class DartFrogCommandRunner extends CompletionCommandRunner<int> {
4242
addCommand(CreateCommand(logger: _logger));
4343
addCommand(DevCommand(logger: _logger));
4444
addCommand(UpdateCommand(logger: _logger));
45+
addCommand(NewCommand(logger: _logger));
4546
}
4647

4748
final Logger _logger;

packages/dart_frog_cli/lib/src/commands/build/templates/dart_frog_prod_server_bundle.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dart_frog_cli/lib/src/commands/commands.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:mason/mason.dart';
33
export 'build/build.dart';
44
export 'create/create.dart';
55
export 'dev/dev.dart';
6+
export 'new/new.dart';
67

78
/// A method which returns a [Future<MasonGenerator>] given a [MasonBundle].
89
typedef GeneratorBuilder = Future<MasonGenerator> Function(MasonBundle);

packages/dart_frog_cli/lib/src/commands/dev/templates/dart_frog_dev_server_bundle.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import 'dart:io';
2+
3+
import 'package:args/args.dart';
4+
import 'package:args/command_runner.dart';
5+
import 'package:dart_frog_cli/src/command.dart';
6+
import 'package:dart_frog_cli/src/commands/commands.dart';
7+
import 'package:dart_frog_cli/src/commands/new/templates/dart_frog_new_bundle.dart';
8+
import 'package:mason/mason.dart';
9+
import 'package:meta/meta.dart';
10+
import 'package:path/path.dart' as path;
11+
12+
/// {@template new_command}
13+
/// `dart_frog new <route|middleware> "path/to/route"`
14+
///
15+
/// Creates a new route or middleware for dart_frog.
16+
/// {@endtemplate}
17+
class NewCommand extends DartFrogCommand {
18+
/// {@macro new_command}
19+
NewCommand({
20+
super.logger,
21+
GeneratorBuilder? generator,
22+
}) : _generator = generator ?? MasonGenerator.fromBundle {
23+
addSubcommand(newRouteCommand = _NewSubCommand('route'));
24+
addSubcommand(newMiddlewareCommand = _NewSubCommand('middleware'));
25+
}
26+
27+
final GeneratorBuilder _generator;
28+
29+
@override
30+
String get description => 'Create a new route or middleware for dart_frog';
31+
32+
@override
33+
String get name => 'new';
34+
35+
@override
36+
final String invocation = 'dart_frog new <route|middleware> "path/to/route"';
37+
38+
/// Subcommand for creating a new route.
39+
@visibleForTesting
40+
late final DartFrogCommand newRouteCommand;
41+
42+
/// Subcommand for creating a new middleware.
43+
@visibleForTesting
44+
late final DartFrogCommand newMiddlewareCommand;
45+
}
46+
47+
class _NewSubCommand extends DartFrogCommand {
48+
_NewSubCommand(this.name);
49+
50+
@override
51+
String get description => 'Create a new $name for dart_frog';
52+
53+
@override
54+
final String name;
55+
56+
@override
57+
NewCommand get parent => super.parent! as NewCommand;
58+
59+
@override
60+
late final String invocation = 'dart_frog new $name "path/to/route"';
61+
62+
@visibleForTesting
63+
@override
64+
// ignore: invalid_use_of_visible_for_testing_member
65+
ArgResults? get testArgResults => parent.testArgResults;
66+
67+
@override
68+
// ignore: invalid_use_of_visible_for_testing_member
69+
String? get testUsage => parent.testUsage;
70+
71+
String get _routePath {
72+
final rest = results.rest;
73+
if (rest.isEmpty) {
74+
throw UsageException(
75+
'Provide a route path for the new $name',
76+
usageString,
77+
);
78+
}
79+
final routeName = rest.first;
80+
if (routeName.isEmpty) {
81+
throw UsageException(
82+
'Route path must not be empty',
83+
usageString,
84+
);
85+
}
86+
87+
final segments =
88+
routeName.split('/').skipWhile((element) => element.isEmpty);
89+
90+
for (final segment in segments) {
91+
if (segment.isEmpty) {
92+
throw UsageException(
93+
'Route path cannot contain empty segments',
94+
'',
95+
);
96+
}
97+
if (segment.contains(r'$')) {
98+
throw UsageException(
99+
'Route path cannot contain dollar signs',
100+
'',
101+
);
102+
}
103+
if (segment.contains(RegExp(r'[^a-zA-Z\d_\[\]]'))) {
104+
throw UsageException(
105+
'Route path segments must be valid Dart identifiers',
106+
'',
107+
);
108+
}
109+
}
110+
111+
return routeName;
112+
}
113+
114+
@override
115+
Future<int> run() async {
116+
final routePath = _routePath;
117+
118+
final generator = await parent._generator(dartFrogNewBundle);
119+
120+
final vars = <String, dynamic>{
121+
'route_path': routePath,
122+
'type': name,
123+
};
124+
125+
final routesDirectory = Directory(path.join(cwd.path, 'routes'));
126+
if (!routesDirectory.existsSync()) {
127+
throw UsageException(
128+
'No "routes" directory found in the current directory. '
129+
'Make sure to run this command on a dart_frog project.',
130+
usageString,
131+
);
132+
}
133+
134+
await generator.hooks.preGen(
135+
vars: vars,
136+
workingDirectory: cwd.path,
137+
onVarsChanged: vars.addAll,
138+
logger: logger,
139+
);
140+
141+
if (!vars.containsKey('dir_path')) {
142+
return ExitCode.software.code;
143+
}
144+
145+
final generateProgress = logger.progress('Creating $name $routePath');
146+
147+
await generator.generate(DirectoryGeneratorTarget(cwd), vars: vars);
148+
149+
await generator.hooks.postGen(
150+
vars: vars,
151+
workingDirectory: cwd.path,
152+
logger: logger,
153+
);
154+
generateProgress.complete();
155+
156+
return ExitCode.success.code;
157+
}
158+
}

0 commit comments

Comments
 (0)