File Coverage

blib/lib/Dancer2/CLI/Gen.pm
Criterion Covered Total %
statement 36 182 19.7
branch 0 78 0.0
condition 0 30 0.0
subroutine 12 26 46.1
pod 0 2 0.0
total 48 318 15.0


line stmt bran cond sub pod time code
1             package Dancer2::CLI::Gen;
2             # ABSTRACT: Create new Dancer2 application
3             $Dancer2::CLI::Gen::VERSION = '2.0.1';
4 2     2   202969 use Moo;
  2         6731  
  2         16  
5 2     2   2862 use URI;
  2         13389  
  2         75  
6 2     2   1568 use HTTP::Tiny;
  2         115724  
  2         123  
7 2     2   805 use Path::Tiny;
  2         10944  
  2         169  
8 2     2   1154 use JSON::MaybeXS;
  2         18643  
  2         162  
9 2     2   1087 use Dancer2::Template::Tiny;
  2         12  
  2         105  
10 2     2   682 use Module::Runtime qw( use_module is_module_name );
  2         2273  
  2         21  
11             use CLI::Osprey
12 2     2   767 desc => 'Helper script to create new Dancer2 applications';
  2         5743  
  2         17  
13              
14             # For git integration
15 2     2   83416 use Symbol;
  2         6  
  2         182  
16 2     2   2798 use IPC::Open3 qw();
  2         7910  
  2         98  
17 2     2   18 use Try::Tiny;
  2         6  
  2         211  
18 2     2   1215 use File::Which;
  2         3411  
  2         7504  
19              
20             option application => (
21             is => 'ro',
22             short => 'a',
23             doc => 'application name',
24             format => 's',
25             format_doc => 'appname',
26             required => 1,
27             spacer_before => 1,
28             );
29              
30             option directory => (
31             is => 'ro',
32             short => 'd',
33             doc => 'application directory (default: same as application name)',
34             format => 's',
35             format_doc => 'directory',
36             required => 0,
37             default => sub { my $self = shift; return $self->application; },
38             );
39              
40             # This was causing conflict with Path::Tiny's path(), so renaming to avoid
41             # the overhead of making Path::Tiny an object.
42             option app_path => (
43             is => 'ro',
44             short => 'p',
45             option => 'path',
46             doc => 'application path (default: current directory)',
47             format => 's',
48             format_doc => 'directory',
49             required => 0,
50             default => '.',
51             );
52              
53             option overwrite => (
54             is => 'ro',
55             short => 'o',
56             doc => 'overwrite existing files',
57             required => 0,
58             default => 0,
59             );
60              
61             option no_check => (
62             is => 'ro',
63             short => 'x',
64             doc => "don't check latest Dancer2 version (default: check - requires internet)",
65             required => 0,
66             default => 0,
67             );
68              
69             option skel => (
70             is => 'ro',
71             short => 's',
72             doc => 'skeleton directory',
73             format => 's',
74             format_doc => 'directory',
75             required => 0,
76             default => sub{
77             my $self = shift;
78             path( $self->parent_command->_dist_dir, 'skel' );
79             },
80             );
81              
82             option skel_name => (
83             is => 'ro',
84             short => 'n',
85             doc => 'skeleton name',
86             format => 's',
87             format_doc => 'skelname',
88             required => 0,
89             default => 'default',
90             );
91              
92             option docker => (
93             is => 'ro',
94             short => 'c',
95             doc => 'create a dockerfile (container definition)',
96             required => 0,
97             default => 0,
98             );
99              
100             option git => (
101             is => 'ro',
102             short => 'g',
103             doc => 'init git repository',
104             required => 0,
105             default => 0,
106             );
107              
108             option remote => (
109             is => 'ro',
110             short => 'r',
111             doc => 'URI for git repository (implies -g)',
112             format => 's',
113             format_doc => 'URI',
114             required => 0,
115             );
116              
117             option no_package_files => (
118             is => 'ro',
119             doc => "don't create files needed for CPAN packaging",
120             required => 0,
121             default => 0,
122             );
123              
124             has _engine => (
125             is => 'ro',
126             default => sub {
127             return Dancer2::Template::Tiny->new( config => { start_tag => '[d2%', end_tag => '%2d]' } );
128             },
129             );
130              
131             # Last chance to validate args before we attempt to do something with them
132             sub BUILD {
133 0     0 0   my ( $self, $args ) = @_;
134              
135 0 0         $self->osprey_usage( 1, qq{
136             Invalid application name. Application names must not contain single colons,
137             dots, hyphens or start with a number.
138             }) unless is_module_name( $self->application );
139              
140 0 0         if ( my $remote = $self->remote ) {
141 0   0       my $scheme = URI->new( $remote )->scheme // $self->remote; # This feels dirty
142 0 0         $self->osprey_usage( 1, "'$remote' must be a valid URI to git repository")
143             unless $scheme =~ / ^ git \@ .+ : .+ \.git $ | ^ http /x;
144             }
145              
146 0           my $path = $self->app_path;
147 0 0         -d $path or $self->osprey_usage( 1, "path: directory '$path' does not exist" );
148 0 0         -w $path or $self->osprey_usage( 1, "path: directory '$path' is not writeable" );
149              
150 0 0         if ( my $skel = $self->skel ) {
151 0 0         -d $skel or $self->osprey_usage( 1, "skel: directory '$skel' not found" );
152             }
153             }
154              
155             sub run {
156 0     0 0   my $self = shift;
157 0 0         $self->_version_check unless $self->no_check;
158              
159 0           my $app_name = $self->application;
160 0           my $app_file = $self->parent_command->_get_app_file( $app_name );
161 0           my $app_path = $self->parent_command->_get_app_path( $self->app_path, $app_name );
162              
163 0 0         if( my $dir = $self->directory ) {
164 0           $app_path = path( $self->app_path, $dir );
165             }
166              
167 0           my $files_to_copy = $self->_build_file_list( $self->skel . '/' . $self->skel_name, $app_path );
168 0           foreach my $pair( @$files_to_copy ) {
169 0 0         if( $pair->[0] =~ m/lib\/AppFile.pm$/ ) {
170 0           $pair->[1] = path( $app_path, $app_file );
171 0           last;
172             }
173             }
174              
175 0 0         if( $self->docker ) {
176 0           push @$files_to_copy, [ path( $self->parent_command->_dist_dir, 'docker/Dockerfile' ), "$app_name/Dockerfile" ];
177             }
178              
179 0           my $vars = {
180             appname => $app_name,
181             appfile => $app_file->stringify,
182             apppath => $app_path,
183             appdir => File::Spec->rel2abs( $app_path ),
184             apppath => $app_path,
185             perl_interpreter => $self->parent_command->_get_perl_interpreter,
186             cleanfiles => $self->parent_command->_get_dashed_name( $app_name ),
187             dancer_version => $self->parent_command->_dancer2_version,
188             docker => $self->docker,
189             };
190              
191 0           $self->_copy_templates( $files_to_copy, $vars, $self->overwrite );
192 0 0         unless( $self->no_package_files ) {
193 0           $self->_create_manifest( $files_to_copy, $app_path );
194 0           $self->_add_to_manifest_skip( $app_path );
195             }
196              
197 0           $self->_check_git( $vars );
198 0           $self->_check_yaml;
199 0           $self->_how_to_run( $vars );
200             }
201              
202             sub _check_git {
203 0     0     my( $self, $vars ) = @_;
204              
205 0 0 0       if( my $remote = $self->remote or $self->git ) {
206 0           my $app_name = $vars->{ appname };
207 0           my $git_error = qq{
208             *****
209              
210             WARNING: Couldn't initialize a git repo despite being asked to do so.
211              
212             To resolve this, cd to your application directory and run the following
213             commands:
214              
215             git init
216             git add .
217             git commit -m"Initial commit of $app_name by Dancer2"
218             };
219              
220 0           my $git = which 'git';
221 0 0         -x $git or die "Can't execute git: $!";
222              
223             #my $dist_dir = $self->parent_command->_dist_dir;
224 0           my $app_path = $vars->{ apppath };
225 0           my $gitignore = path( $self->parent_command->_dist_dir, '.gitignore' );
226 0           path( $gitignore )->copy( $app_path );
227              
228 0 0         chdir File::Spec->rel2abs( $app_path ) or die "Can't cd to $app_path: $!";
229 0 0 0       if( _run_shell_cmd( 'git', 'init') != 0 or
      0        
230             _run_shell_cmd( 'git', 'add', '.') != 0 or
231             _run_shell_cmd( 'git', 'commit', "-m 'Initial commit of $app_name by Dancer2'" ) != 0 ) {
232 0           print $git_error;
233             }
234             else {
235 0 0 0       if( $self->remote &&
236             _run_shell_cmd( 'git', 'remote', 'add', 'origin', $self->remote ) != 0 ) {
237 0           print $git_error;
238 0           print " git remote add origin " . $self->remote . "\n";
239             }
240             }
241 0           print "\n*****\n";
242             }
243             }
244              
245             sub _check_yaml {
246 0 0   0     if ( ! eval { use_module( 'YAML' ); 1; } ) {
  0            
  0            
247 0           print qq{
248             *****
249              
250             WARNING: YAML.pm is not installed. This is not a full dependency, but is highly
251             recommended; in particular, the scaffolded Dancer app being created will not be
252             able to read settings from the config file without YAML.pm being installed.
253              
254             To resolve this, simply install YAML from CPAN, for instance using one of the
255             following commands:
256              
257             cpan YAML
258             perl -MCPAN -e 'install YAML'
259             curl -L https://cpanmin.us | perl - --sudo YAML
260              
261             *****
262             };
263             }
264             }
265              
266             sub _how_to_run {
267 0     0     my( $self, $vars ) = @_;
268              
269 0           my $app_path = $vars->{ apppath };
270 0           my $app_name = $vars->{ appname };
271              
272 0           print "\nYour new application is ready! To run it:\n";
273              
274 0 0         if( $vars->{ docker } ) {
275 0           my $image = lc $app_name;
276 0           print qq{
277              
278             cd $app_path
279             docker build -t ${image} .
280             docker run -d -p 5000:4000 --name $app_name ${image}
281              
282             where 5000 is the external port, and 4000 is the port your application
283             runs on inside of the container.
284              
285             (note: you may need to run the docker commands with sudo)
286              
287             You may also run your app without Docker:
288             };
289             }
290              
291 0           my $install_deps = '';
292 0 0         $install_deps = "\n cpanm --installdeps ."
293             if $self->skel_name ne 'default';
294              
295 0           print qq{
296             cd $app_path$install_deps
297             plackup bin/app.psgi
298              
299             To access your application, point your browser to http://localhost:5000/
300              
301             If you need community assistance, the following resources are available:
302             - Dancer website: https://perldancer.org
303             - GitHub: https://github.com/PerlDancer/Dancer2/
304             - Mailing list: https://lists.perldancer.org/mailman/listinfo/dancer-users
305             - IRC: irc.perl.org#dancer
306              
307             Happy Dancing!
308              
309             };
310             }
311              
312             # skel creation routines
313             sub _build_file_list {
314 0     0     my ( $self, $from, $to ) = @_;
315 0           $from =~ s{/+$}{};
316              
317 0           my @result;
318 0           my $iter = path( $from )->iterator({ recurse => 1 });
319 0           while( my $file = $iter->() ) {
320 0 0         warn "File not found: $file" unless $file->exists; # Paranoia
321 0 0         next if $file->basename =~ m{^\.git(/|$)};
322 0 0         next if $file->is_dir;
323              
324 0           my $filename = $file->relative( $from );
325 0           push @result, [ $file, path( $to, $filename )];
326             }
327 0           return \@result;
328             }
329              
330             sub _copy_templates {
331 0     0     my ( $self, $files, $vars, $overwrite ) = @_;
332 0           my $app_name = $vars->{ appname };
333              
334 0           foreach my $pair (@$files) {
335 0           my ( $from, $to ) = @{$pair};
  0            
336 0 0 0       next if $self->no_package_files && $from =~ /MANIFEST\.SKIP$/;
337 0 0 0       next if $self->no_package_files && $from =~ /Makefile.PL$/;
338              
339 0 0 0       if ( -f $to && !$overwrite ) {
340 0           print "! $to exists, overwrite? (or rerun this command with -o) [N/y/a]: ";
341 0           my $res = ; chomp($res);
  0            
342 0 0         $overwrite = 1 if $res eq 'a';
343 0 0 0       next unless ( $res eq 'y' ) or ( $res eq 'a' );
344             }
345              
346 0           $to =~ s/AppFile/$app_name/;
347 0           my $to_dir = path( $to )->parent;
348 0 0         if ( ! $to_dir->is_dir ) {
349 0           print "+ $to_dir\n";
350 0           $to_dir->mkpath;
351             }
352              
353             # Skeleton files whose names are prefixed with + need to be executable, but we must strip
354             # that from the name when copying them
355 0           my $to_file = path( $to )->basename;
356 0           my $ex = ( $to_file =~ s/^\+// );
357 0 0         $to = path( $to_dir, $to_file ) if $ex;
358              
359 0           print "+ $to\n";
360 0           my $content;
361             {
362 0           local $/;
  0            
363 0 0         open( my $fh, '<:raw', $from ) or die "unable to open file `$from' for reading: $!";
364 0           $content = <$fh>;
365 0           close $fh;
366             }
367              
368 0 0         if( $from !~ m/\.(db|ico|jpg|png|css|eot|map|swp|ttf|svg|woff|woff2|js)$/ ) {
369 0           $content = $self->_process_template($content, $vars);
370             }
371              
372 0           path( $to )->spew_raw( $content );
373 0 0         if( $ex ) {
374 0 0         $to->chmod( 0755 ) or warn "unable to change permissions for $to: $!";
375             }
376             }
377             }
378              
379             sub _create_manifest {
380 0     0     my ( $self, $files, $dir ) = @_;
381              
382 0           my $manifest_name = path( $dir, 'MANIFEST' );
383 0 0         open( my $manifest, '>', $manifest_name ) or die $!;
384 0           print $manifest "MANIFEST\n";
385              
386 0           foreach my $file( @{ $files } ) {
  0            
387 0           my $filename = path( $file->[1] )->relative( $dir );
388 0           my $basename = $filename->basename;
389 0           my $clean_basename = $basename;
390 0           $clean_basename =~ s/^\+//;
391 0           $filename =~ s/\Q$basename\E/$clean_basename/;
392 0           print {$manifest} "$filename\n";
  0            
393             }
394              
395 0           close $manifest;
396             }
397              
398             sub _add_to_manifest_skip {
399 0     0     my ( $self, $dir ) = @_;
400              
401 0           my $filename = path( $dir, 'MANIFEST.SKIP' );
402 0 0         open my $fh, '>>', $filename or die $!;
403 0           print {$fh} "^$dir-\n";
  0            
404 0           close $fh;
405             }
406              
407             sub _process_template {
408 0     0     my ( $self, $template, $tokens ) = @_;
409              
410 0           return $self->_engine->render( \$template, $tokens );
411             }
412              
413             sub _version_check {
414 0     0     my $self = shift;
415 0           my $version = $self->parent_command->_dancer2_version;
416 0 0         return if $version =~ m/_/;
417              
418 0           my $latest_version = 0;
419 0           my $resp = HTTP::Tiny->new( timeout => 5 )->get( 'https://fastapi.metacpan.org/release/Dancer2' );
420 0 0 0       if( $resp->{ success } && decode_json( $resp->{ content } )->{ version } =~ /(\d\.\d+)/ ) {
421 0           $latest_version = $1;
422 0 0         if ($latest_version gt $version) {
423 0           print qq{
424             The latest stable Dancer2 release is $latest_version. You are currently using $version.
425             Please check https://metacpan.org/pod/Dancer2/ for updates.
426              
427             };
428             }
429             } else {
430 0           warn "\nCouldn't determine latest version of Dancer2. Please check your internet
431             connection, or pass -x to gen to bypass this check in the future.\n\n";
432              
433             }
434             }
435              
436             # Shell out to run git
437             sub _run_shell_cmd {
438 0     0     my @cmds = @_;
439              
440             my $exit_status = try {
441 0     0     my $pid = IPC::Open3::open3(
442             my $stdin,
443             my $stdout,
444             my $stderr = Symbol::gensym,
445             @cmds,
446             );
447              
448 0           waitpid( $pid, 0 );
449 0           return $? >> 8;
450             } catch {
451 0     0     print STDERR "$_\n";
452 0           return 1;
453 0           };
454              
455 0           return $exit_status;
456             }
457              
458             1;
459              
460             __END__