• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ryoskzypu / Module-Starter-Plugin-MyGuts / 21772366193

07 Feb 2026 02:09AM UTC coverage: 96.341%. First build
21772366193

push

github

ryoskzypu
Add stub file in docs for expected-tree test to pass

7 of 8 new or added lines in 1 file covered. (87.5%)

158 of 164 relevant lines covered (96.34%)

2.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.34
/lib/Module/Starter/Plugin/MyGuts.pm
1
package Module::Starter::Plugin::MyGuts;
2

3
use v5.40.0;
3✔
4

5
use strict;
3✔
6
use warnings;
3✔
7
use utf8;
3✔
8
use version;
3✔
9
use open qw< :std :encoding(UTF-8) >;  # Encode/decode STDIN, STDOUT, STDERR, and filehandles to UTF-8.
3✔
10

11
use parent qw< Module::Starter::Simple >;
3✔
12

13
use File::Spec ();
3✔
14
use File::Path qw< make_path >;
3✔
15
use Carp       qw< confess >;
3✔
16

17
our $VERSION = 'v1.0.0';
18

19
# Initial distribution version (dotted)
20
my $DIST_VERSION = 'v1.0.0';           # Stable API initial release (SemVer)
21

22
sub new ( $class, @args )
3✔
23
{
3✔
24
    my $self = $class->SUPER::new(@args);
3✔
25

26
    if ( defined $self->{builder} ) {
3✔
27
        die 'Only ExtUtils::MakeMaker is supported' if $self->{builder}[0] ne 'ExtUtils::MakeMaker';
3✔
28
        die 'Only one builder is supported'         if scalar $self->{builder}->@* > 1;
2✔
29
    }
30

31
    return $self;
1✔
32
}
33

34
sub post_create_distro ($self)
35
{
1✔
36
    if ( defined $self->{github} ) {
1✔
37
        # Create GitHub workflows directory and its CI file.
38
        my $workflows = File::Spec->catdir( $self->{basedir}, '.github', 'workflows' );
1✔
39
        if ( make_path $workflows ) {
1✔
40
            $self->progress("Created $workflows");
1✔
41
            $self->create_CI($workflows);
1✔
42
        }
43
        else {
44
            warn "Failed to create GitHub workflows directory: $!";
×
45
        }
46

47
        # Create docs directory.
48
        #
49
        # NOTE:
50
        #   To reflect the 'Support and documentation' section, make sure to convert the
51
        #   distribution files that contain POD to Markdown (with pod2markdown) and put
52
        #   them in the docs directory.
53
        my $docs = File::Spec->catdir( $self->{basedir}, 'docs' );
1✔
54
        if ( mkdir $docs ) {
1✔
55
            $self->progress("Created $docs");
1✔
56

57
            # Create a stub file so docs is shown on GitHub.
58
            my @parts    = split /::/, $self->{main_module};
1✔
59
            my $filepart = ( pop @parts ) . '.md';
1✔
60
            my $fname    = File::Spec->catdir( $docs, $filepart );
1✔
61

62
            $self->create_file( $fname, '' );
1✔
63
            $self->progress("Created $fname");
1✔
64
        }
65
        else {
NEW
66
            warn "Failed to create docs directory: $!";
×
67
        }
68

69
        $self->create_README_md;
1✔
70
    }
71
}
72

73
# See:
74
#   https://docs.github.com/en/actions/get-started/understand-github-actions
75
#   https://perlmaven.com/what-is-ci
76
#   https://perlhacks.com/2024/01/github-actions-for-perl-development/
77
#   https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows
78
#   https://github.com/perl-actions/install-with-cpanm
79
#   https://github.com/FGasper/perl-github-action-tips
80
#   https://metacpan.org/pod/Devel::Cover::Report::Coveralls
81
#   https://github.com/ryoskzypu/github_workflows
82
sub create_CI ( $self, $fpath )
1✔
83
{
1✔
84
    my $fname    = File::Spec->catfile( $fpath, 'ci.yml' );
1✔
85
    my $workflow = q{ryoskzypu/github_workflows/.github/workflows/perl-test.yml@main};
1✔
86

87
    my $ci = <<~"END";
1✔
88
    name: 'CI'
89
    description: 'Call perl-test.yml on every push and pull request'
90

91
    on:
92
      push:
93
        branches:
94
          - '*'
95
        tags-ignore:
96
          - '*'
97
      pull_request:
98
      workflow_dispatch:
99

100
    jobs:
101
      call-perl-test:
102
        uses: $workflow
103
        with:
104
          since-perl: '$self->{minperl}'
105
          with-devel: true
106
          coverage: true
107
    END
108

109
    $self->create_file( $fname, $ci );
1✔
110
    $self->progress("Created $fname");
1✔
111
}
112

113
# See:
114
#   https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes
115
#   https://www.markdownguide.org/basic-syntax/
116
#   https://google.github.io/styleguide/docguide/style.html
117
#   https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax
118
sub create_README_md ($self)
119
{
1✔
120
    my $fname   = File::Spec->catfile( $self->{basedir}, 'README.md' );
1✔
121
    my $readme  = $self->README_guts('');
1✔
122
    my $license = $self->_get_license( POD => 1 );
1✔
123

124
    $readme = <<~"END";
1✔
125
        # $self->{main_module}
126

127
        $self->{bp}{readme_intro}
128
        ## Installation
129

130
        To download and install this module directly with [cpanminus](https://metacpan.org/pod/App::cpanminus):
131

132
        ```shell
133
        \$ cpanm $self->{res}{repository}.git
134
        ```
135

136
        To do it manually, run the following commands (after cloning the repository):
137

138
        ```shell
139
        \$ cd $self->{distro}
140
        \$ perl Makefile.PL
141
        \$ make
142
        \$ make test
143
        \$ make install
144
        ```
145

146
        ## Support and documentation
147

148
        You can find documentation for this module in [docs](docs/) or with the
149
        `perldoc` command (after installing):
150

151
        ```shell
152
        \$ perldoc $self->{main_module}
153
        ```
154

155
        You can also look for information at:
156

157
        - GitHub issue tracker (report bugs here)
158

159
            $self->{res}{bug_tracker}
160

161
        - Search CPAN
162

163
            https://metacpan.org/dist/$self->{distro}
164

165
        ## Copyright
166

167
        $license
168
        END
169

170
    $self->create_file( $fname, $readme );
1✔
171
    $self->progress("Created $fname");
1✔
172
}
173

174
# See:
175
#   https://perldoc.perl.org/perlmodstyle
176
#   https://perldoc.perl.org/perlpodstyle
177
#   https://pause.perl.org/pause/query?ACTION=pause_namingmodules
178
#   https://blogs.perl.org/users/neilb/2014/08/the-right-name-for-your-cpan-distribution.html
179
#   https://blogs.perl.org/users/neilb/2014/07/give-your-modules-a-good-abstract.html
180
#   https://www.neilb.org/2015/12/20/specify-perl-version.html
181
#   https://old.reddit.com/r/perl/comments/5i4vn9/version_numbers/
182
#   https://semver.org/
183
#   https://blogs.perl.org/users/dean/2022/08/please-relicense-from-perl-5-to-mit-or-apache-20-license.html
184
#   https://github.com/aws/mit-0
185
sub module_guts ( $self, $module, $rtname )
1✔
186
{
1✔
187
    # Remove true value at the end if minimum perl is >= v5.38.0.
188
    my $mod_true = version->parse( $self->{minperl} ) >= version->parse('v5.38.0');
1✔
189

190
    # NOTE:
191
    #   module_guts method is the first executed in the *_guts chain, thus some
192
    #   attributes should be set here. The boilerplate/metadata methods must be
193
    #   called here in order for them to access the attributes from Module::Starter::Simple::_create_module().
194
    $self->{author_full} = $self->{author}[0];
1✔
195
    $self->{author_name} = $self->{author_full} =~ s{ <.+\z}{}r;  # Strip email
1✔
196
    $self->_build_boilerplates;
1✔
197
    $self->{res} = $self->_get_resources;
1✔
198

199
    my $header  = "package $module;\n\n$self->{bp}{header}";
1✔
200
    my $license = $self->_get_license( POD => 1 );
1✔
201

202
    my $content = $header . <<~"END";
1✔
203
        our \$VERSION = '$DIST_VERSION';
204

205
        $self->{bp}{stub_function1}
206
        $self->{bp}{stub_function2}
207
        \=encoding UTF-8
208

209
        \=head1 NAME
210

211
        $module - $self->{bp}{abstract}
212

213
        \=head1 SYNOPSIS
214

215
        $self->{bp}{synopsis}
216
        \=head1 DESCRIPTION
217

218
        $self->{bp}{description}
219

220
        \=head1 EXPORTS
221

222
        $self->{bp}{exports}
223
        \=head1 SUBROUTINES/METHODS
224

225
        $self->{bp}{functions}
226
        \=head1 BUGS
227

228
        Report bugs at L<$self->{res}{bug_tracker}>.
229

230
        \=head1 AUTHOR
231

232
        $self->{author_full}
233

234
        \=head1 SEE ALSO
235

236
        $self->{bp}{see_also}
237
        \=head1 COPYRIGHT
238

239
        $license
240

241
        \=cut
242

243
        1;
244
        END
245

246
    $content =~ s{
1✔
247
        ^\n
248
        1;\n
249
        \z
250
    }
251
    {}mx if $mod_true;
252

253
    return $content;
1✔
254
}
255

256
# See:
257
#   https://archive.shadowcat.co.uk/blog/matt-s-trout/mstpan-11/
258
#   https://old.reddit.com/r/perl/comments/ad7vyq/how_to_write_perl_modules_for_cpan_the_modern_way/
259
#   https://old.reddit.com/r/perl/comments/13ib46n/distzilla_considered_annoying/
260
#   https://github.com/Perl-Toolchain-Gang/toolchain-site/blob/master/cpan-packaging.md
261
#   https://blogs.perl.org/users/neilb/2017/04/an-introduction-to-distribution-metadata.html
262
#   https://blogs.perl.org/users/neilb/2017/04/dependency-phases-in-cpan-distribution-metadata.html
263
#   https://blogs.perl.org/users/neilb/2017/05/specifying-dependencies-for-your-cpan-distribution.html
264
sub Makefile_PL_guts ( $self, $main_module, $main_pm_file )
1✔
265
{
1✔
266
    my $sl_name =
267
        $self->{license_record}
268
      ? $self->{license_record}->meta2_name
269
      : $self->{license};
1✔
270

271
    my $meta_merge = $self->Makefile_PL_meta_merge;
1✔
272

273
    # NOTE:
274
    #   EUMM 6.64 is the minimum version that supports CONFIGURE_REQUIRES, TEST_REQUIRES,
275
    #   and META_MERGE attributes.
276
    my $makefile = $self->{bp}{header} . <<~"END";
1✔
277
        use ExtUtils::MakeMaker;
278

279
        my %WriteMakefileArgs = (
280
            NAME             => '$main_module',
281
            AUTHOR           => q{$self->{author_full}},
282
            VERSION_FROM     => '$main_pm_file',
283
            ABSTRACT_FROM    => '$main_pm_file',
284
            LICENSE          => '$sl_name',
285
            MIN_PERL_VERSION => '$self->{minperl}',
286
            EXE_FILES        => [
287
                #'bin/prog',
288
            ],
289
            CONFIGURE_REQUIRES => {
290
                'ExtUtils::MakeMaker' => '6.64',
291
            },
292
            TEST_REQUIRES => {
293
                'Test2::V1' => '0',
294
            },
295
            PREREQ_PM => {
296
                #'ABC'              => '1.6',
297
                #'Foo::Bar::Module' => '5.0401',
298
            },
299
        $meta_merge);
300

301
        WriteMakefile(%WriteMakefileArgs);
302
        END
303

304
    # Do not declare a minimum perl version.
305
    if ( defined $self->{no_minperl} && $self->{no_minperl} ) {
1✔
306
        # Strip metadata info.
307
        $makefile =~ s{^\x{20}+MIN_PERL_VERSION => [^,]+,\n}{}m;
×
308
    }
309

310
    return $makefile;
1✔
311
}
312

313
# See:
314
#   https://metacpan.org/pod/ExtUtils::MakeMaker#META_MERGE
315
#   https://metacpan.org/pod/CPAN::Meta::Spec#Prereq-Spec
316
#   https://blogs.perl.org/users/neilb/2017/04/specifying-the-type-of-your-cpan-dependencies.html
317
#   https://perlmaven.com/how-to-add-link-to-version-control-system-of-a-cpan-distributions
318
#   https://metacpan.org/pod/CPAN::Meta::Spec#resources.
319
#   https://metacpan.org/about/metadata
320
#   https://libera.chat/guides/webchat
321
#   https://perlmaven.com/how-to-add-list-of-contributors-to-the-cpan-meta-files
322
sub Makefile_PL_meta_merge ($self)
323
{
1✔
324
    return <<~"END";
1✔
325
        META_MERGE => {
326
            'meta-spec' => { version => 2 },
327
            no_index    => {
328
                directory => [
329
                    qw<
330
                        eg
331
                        examples
332
                        share
333
                        t
334
                        xt
335
                    >
336
                ],
337
            },
338
            prereqs => {
339
                develop => {
340
                    recommends => {
341
                        'App::CPANTS::Lint'        => '0',
342
                        'Data::Printer'            => '0',
343
                        'Devel::Cover'             => '0',
344
                        'Perl::Critic'             => '0',
345
                        'Perl::Tidy'               => '0',
346
                        'Pod::Markdown'            => '0',
347
                        'Pod::Markdown::Githubert' => '0',
348
                    },
349
                    requires => {
350
                        'Test::CPAN::Changes' => '0',
351
                        'Test::Kwalitee'      => '0',
352
                        'Test::Perl::Critic'  => '0',
353
                        'Test::Pod'           => '0',
354
                        'Test::Pod::Coverage' => '0',
355
                        'Test::Spelling'      => '0',
356
                    },
357
                },
358
                #runtime => {
359
                #    recommends => {
360
                #        'Foo::Bar' => '0',
361
                #    },
362
                #    suggests => {
363
                #        'Foo::Bat' => '0',
364
                #    },
365
                #},
366
                #test => {
367
                #    recommends => {
368
                #        'Foo::Bar' => '0',
369
                #    },
370
                #    suggests => {
371
                #        'Foo::Bat' => '0',
372
                #    },
373
                #},
374
            },
375
            resources => {
376
                repository => {
377
                    type => 'git',
378
                    url  => '$self->{res}{repository}.git',
379
                    web  => '$self->{res}{repository}',
380
                },
381
                bugtracker => {
382
                    web => '$self->{res}{bug_tracker}',
383
                },
384
                #homepage => '$self->{res}{homepage}',
385
                #'x_IRC' => {
386
                #    url => 'irc://irc.libera.chat/#channel',
387
                #    web => 'https://web.libera.chat/?nick=Guest?#channel',
388
                #}
389
            },
390
            x_contributors => [
391
                q{$self->{author_full}},
392
            ],
393
        },
394
    END
395
}
396

397
# See:
398
#   https://neilb.org/2015/10/18/spotters-guide.html#text:~:text=Changes,-The
399
#   https://metacpan.org/dist/CPAN-Changes/view/lib/CPAN/Changes/Spec.pod
400
#   https://blogs.perl.org/users/grinnz/2018/04/a-guide-to-versions-in-perl.html
401
sub Changes_guts ($self)
402
{
1✔
403
    chomp( my $changelog = $self->{bp}{changelog} );
1✔
404

405
    return <<~"END";
1✔
406
        Revision history for $self->{main_module}
407

408
        $changelog
409
        END
410
}
411

412
# See:
413
#   https://neilb.org/2015/10/18/spotters-guide.html#text:~:text=README
414
sub README_guts ( $self, $build_instructions )
2✔
415
{
2✔
416
    my $bugs_header =
417
      defined $self->{github}
418
      ? 'GitHub issue tracker (report bugs here)'
2✔
419
      : q{CPAN's request tracker (report bugs here)};
420

421
    my $readme = <<~"END";
2✔
422
        $self->{main_module}
423

424
        $self->{bp}{readme_intro}
425

426
        INSTALLATION
427

428
        To download and install this module, use your favorite CPAN client:
429

430
            cpanm $self->{main_module}
431

432
        To do it manually, run the following commands (after downloading and unpacking
433
        the tarball):
434

435
            perl Makefile.PL
436
            make
437
            make test
438
            make install
439

440

441
        SUPPORT AND DOCUMENTATION
442

443
        After installing, you can find documentation for this module with the perldoc
444
        command:
445

446
            perldoc $self->{main_module}
447

448
        You can also look for information at:
449

450
            $bugs_header
451
                $self->{res}{bug_tracker}
452

453
            Search CPAN
454
                https://metacpan.org/dist/$self->{distro}
455

456

457
        COPYRIGHT
458

459
        $self->{bp}{license}
460
        END
461

462
    return $readme;
2✔
463
}
464

465
# See:
466
#   https://neilb.org/2015/10/18/spotters-guide.html#text:~:text=t,-%2F
467
#   https://metacpan.org/pod/Test2::Manual::Testing::Introduction
468
#   https://metacpan.org/pod/Test2::V1
469
sub t_guts ( $self, @modules )
1✔
470
{
1✔
471
    my %t_files;
1✔
472
    my $use_mod;
473
    my $shebang = '#!/usr/bin/env perl';
1✔
474
    my $header  = "$shebang\n\n$self->{bp}{header}";
1✔
475

476
    foreach my $mod (@modules) {
1✔
477
        $use_mod .= "use ok '$mod';\n";
1✔
478
    }
479
    chomp $use_mod;
1✔
480

481
    $t_files{'00-load.t'} = $header . <<~"END";
1✔
482
        use Test2::V1;
483

484
        $use_mod
485

486
        foreach my \$mod ( qw< @modules > ) {
487
            my \$mod_ver = '\$' . \$mod . '::VERSION';
488

489
            T2->diag(
490
                sprintf "Testing \$mod %s, Perl %s, %s",
491
                \$mod_ver, \$], \$^X,
492
            );
493
        }
494

495
        T2->done_testing;
496
        END
497

498
    return %t_files;
1✔
499
}
500

501
# See:
502
#   https://neilb.org/2015/10/18/spotters-guide.html#text:~:text=xt
503
sub xt_guts ( $self, @modules )
1✔
504
{
1✔
505
    my %xt_files;
1✔
506
    my $shebang = '#!/usr/bin/env perl';
1✔
507
    my $header  = "$shebang\n\n$self->{bp}{header}";
1✔
508

509
    # perlcritic
510
    # https://metacpan.org/pod/Test::Perl::Critic
511
    {
512
        $xt_files{'critic.t'} = $header . <<~'END';
1✔
513
        use Test2::Require::Module qw< Test::Perl::Critic >;
514
        use Test::Perl::Critic;
515

516
        my $EXE = 'bin';
517

518
        my @FILES = (
519
            qw<
520
                Makefile.PL
521
                lib
522
                t
523
                xt
524
            >
525
        );
526

527
        push @FILES, $EXE if -e $EXE && -d $EXE;
528

529
        all_critic_ok(@FILES);
530
        END
531
    }
532

533
    # Manifest tests
534
    # https://metacpan.org/pod/ExtUtils::Manifest
535
    {
536
        $xt_files{'manifest.t'} = $header . <<~'END';
1✔
537
            use Test2::V1 qw< is >;
538
            T2->plan(2);
539

540
            use ExtUtils::Manifest qw< manicheck filecheck >;
541

542
            is(
543
                [ manicheck() ], [],
544
                'manicheck() - missing files',
545
            );
546

547
            is(
548
                [ filecheck() ], [],
549
                'filecheck() - extra files',
550
            );
551
            END
552
    }
553

554
    # Changes tests
555
    # https://metacpan.org/pod/Test::CPAN::Changes
556
    {
557
        $xt_files{'cpan-changes.t'} = $header . <<~'END';
1✔
558
            use Test2::Require::Module qw< Test::CPAN::Changes >;
559
            use Test::CPAN::Changes;
560

561
            changes_ok();
562
            END
563
    }
564

565
    # POD tests
566
    #
567
    # https://metacpan.org/pod/Test::Pod
568
    # https://metacpan.org/pod/Test::Pod::Coverage
569
    # https://metacpan.org/pod/Test::Spelling
570
    {
571
        $xt_files{'pod-syntax.t'} = $header . <<~'END';
1✔
572
            use Test2::Require::Module qw< Test::Pod >;
573
            use Test::Pod;
574

575
            my $EXE = 'bin';
576

577
            my @DIRS = (
578
                qw<
579
                    lib
580
                >
581
            );
582

583
            push @DIRS, $EXE if -e $EXE && -d $EXE;
584

585
            all_pod_files_ok( all_pod_files(@DIRS) );
586
            END
587

588
        $xt_files{'pod-coverage.t'} = $header . <<~'END';
1✔
589
            use Test2::Require::Module qw< Test::Pod::Coverage >;
590
            use Test::Pod::Coverage;
591

592
            all_pod_coverage_ok();
593
            END
594

595
        $xt_files{'pod-spell.t'} = $header . <<~"_";
1✔
596
            use Test2::V1              qw< diag >;
597
            use Test2::Require::Module qw< Test::Spelling >;
598

599
            use Test::Spelling;
600
            use Pod::Wordlist;
601

602
            diag <<'END';
603
            NOTE:
604
              This test requires a spellchecker with an English dictionary installed, e.g. aspell.
605

606
            END
607

608
            add_stopwords(<DATA>);
609

610
            all_pod_files_spelling_ok(
611
                qw<
612
                    bin
613
                    script
614
                    lib
615
                >
616
            );
617

618
            __DATA__
619
            $self->{author_name}
620
            _
621
    }
622

623
    # Kwalitee tests
624
    #
625
    # https://metacpan.org/pod/App::CPANTS::Lint
626
    # https://metacpan.org/pod/Test::Kwalitee
627
    {
628
        $xt_files{'kwalitee.t'} = $header . <<~"_";
1✔
629
            use Test2::V1              qw< diag >;
630
            use Test2::Require::Module qw< Test::Kwalitee >;
631

632
            use Test::Kwalitee qw< kwalitee_ok >;
633

634
            diag <<'END';
635
            NOTE:
636
              This test must be done in the unpacked release tarball directory, which
637
              misses some kwalitee indicators.
638

639
              For a more complete test, install App::CPANTS::Lint and run it on the
640
              release tarball:
641

642
                \$ cpanm App::CPANTS::Lint
643
                \$ cpants_lint.pl --color --verbose $self->{distro}-$DIST_VERSION.tar.gz
644

645
            END
646

647
            kwalitee_ok();
648
            T2->done_testing;
649
            _
650
    }
651

652
    # Boilerplate tests
653
    {
654
        my $module_bp_tests;
1✔
655

656
        foreach my $mod (@modules) {
1✔
657
            my $file = $self->_module_to_pm_file($mod);
1✔
658
            $module_bp_tests .= "not_in_file_ok('$file');\n";
1✔
659
        }
660
        chomp $module_bp_tests;
1✔
661

662
        $xt_files{'boilerplate.t'} = "$shebang\n" . <<~'END';
1✔
663
            #
664
            # Test to ensure that no boilerplate text generated by Module::Starter::Plugin::MyGuts
665
            # is left in the distribution files.
666
            #
667
            # NOTE:
668
            #   To see verbose output in correct order, use yath or run:
669
            #     prove -l xt/boilerplate.t --merge -v
670

671
            END
672

673
        $xt_files{'boilerplate.t'} .= $self->{bp}{header} . <<~'END';
1✔
674
            use Test2::V1 qw<
675
                note
676
                diag
677
                pass
678
                fail
679
            >;
680

681
            use re qw< eval >;
682
            #use DDP output => 'stdout';
683

684
            sub not_in_file_ok
685
            {
686
                my $filename = shift;
687

688
                note("FILENAME: $filename\n\n");
689

690
                open my $fh, '<', $filename or die "Failed to open $filename for reading: $!";
691
                my $file = do { local $/ = undef; <$fh> };  # Slurp entire file.
692
                close $fh or die $!;
693

694
                my $desc;
695
                my @regex_type;
696

697
            END
698

699
        # Inject the boilerplates texts for their regex compilations.
700
        $xt_files{'boilerplate.t'} .= <<~"END";
1✔
701
                my \@readme_rgx = (
702
                    '\Q$self->{bp}{readme_intro}\E(?{ \$desc = "introduction" })',
703
                );
704

705
                my \@changes_rgx = (
706
                    '\Q$self->{bp}{changelog}\E(?{ \$desc = "changelog" })',
707
                );
708

709
                my \@modules_rgx = (
710
                    '\Q$self->{bp}{stub_function1}\E(?{ \$desc = "stub function1 definition" })',
711
                    '\Q$self->{bp}{stub_function2}\E(?{ \$desc = "stub function2 definition" })',
712
                    '[^ \\n]+ - \Q$self->{bp}{abstract}\E(?{ \$desc = "POD NAME" })',
713
                    '\Q$self->{bp}{synopsis}\E(?{ \$desc = "POD SYNOPSIS" })',
714
                    '\Q$self->{bp}{description}\E(?{ \$desc = "POD DESCRIPTION" })',
715
                    '\Q$self->{bp}{exports}\E(?{ \$desc = "POD EXPORTS" })',
716
                    '\Q$self->{bp}{functions}\E(?{ \$desc = "POD SUBROUTINES/METHODS" })',
717
                    '\Q$self->{bp}{see_also}\E(?{ \$desc = "POD SEE ALSO" })',
718
                );
719

720
            END
721

722
        $xt_files{'boilerplate.t'} .= <<~'END';
1✔
723
                foreach ($filename) {
724
                    if    (/\AREADME\z/)  { push @regex_type, @readme_rgx }
725
                    elsif (/\AChanges\z/) { push @regex_type, @changes_rgx }
726
                    elsif (/\.pm\z/)      { push @regex_type, @modules_rgx }
727
                    else                  { die "$filename is not supported" }
728
                }
729

730
                #p @regex_type;
731
                #note("\n");
732

733
                # Concat and compile the regexes.
734
                my $joined = join '|', @regex_type;
735
                my $regex  = qr{^(?> $joined )}mx;
736

737
                #note("REGEX:\n");
738
                #p $regex;
739
                #note("\n");
740

741
                my $c = 0;
742

743
                # Scan the file with its respective regex type.
744
                while (1) {
745
                    # Boilerplate
746
                    if ( $file =~ /\G$regex/gc ) {
747
                        my $end = '';
748
                        ++$c;
749

750
                        note("MATCH:\n$c '$&'\n");
751
                        fail("$filename contains $desc boilerplate text");
752

753
                        # Count lines of a multiline match.
754
                        my $nl = $& =~ tr{\n}{};
755
                        if ( $nl > 1 ) {
756
                            $nl  += $c - 1;
757
                            $end  = ',' . $nl;
758
                        }
759

760
                        diag("$desc appears on lines ${c}$end");
761
                        note("\n");
762

763
                        $c = $nl;
764
                    }
765
                    # Generic line (forwards scanner line-by-line).
766
                    elsif ( $file =~ /\G([^\n]*+)\n/gc ) {
767
                        ++$c;
768
                        #note("$c '$1'");
769
                    }
770

771
                    # End of file.
772
                    if ( $file =~ /\G\z/ ) {
773
                        pass("$filename contains no boilerplate text") unless defined $desc;
774
                        note("\n");
775

776
                        last;
777
                    }
778
                }
779
            }
780

781
            not_in_file_ok('README');
782
            not_in_file_ok('Changes');
783
            END
784

785
        $xt_files{'boilerplate.t'} .= <<~"END";
1✔
786
            $module_bp_tests
787

788
            T2->done_testing;
789
            END
790
    }
791

792
    return %xt_files;
1✔
793
}
794

795
# Ignore only build files relevant to Unix, EUMM, and the current tooling.
796
#
797
# References:
798
#   https://perlmaven.com/dont-keep-generated-files-in-version-control
799
sub ignores_guts ( $self, $type )
2✔
800
{
2✔
801
    my $guts = {
2✔
802
        # See:
803
        #   https://git-scm.com/docs/gitignore
804
        #   https://github.com/github/gitignore/blob/main/Perl.gitignore
805
        #   https://github.com/briandfoy/PerlPowerTools/blob/master/.gitignore
806
        generic => <<~"END",
807
            MANIFEST
808
            MANIFEST.bak
809
            META.*
810
            MYMETA.*
811

812
            # Junk
813
            *.o
814
            *.bs
815
            *.tar
816
            *.tgz
817
            *.gz
818
            *.zip
819
            *.tmp
820
            *.old
821
            *.bak
822
            *.rej
823
            *.orig
824

825
            # ExtUtils::MakeMaker
826
            blib/
827
            Makefile
828
            Makefile.old
829
            pm_to_blib
830

831
            # Devel::Cover
832
            cover_db/
833
            .last_cover_stats
834

835
            # Devel::NYTProf
836
            nytprof.out
837

838
            $self->{distro}-*
839
            END
840

841
        # See:
842
        #   https://metacpan.org/pod/ExtUtils::Manifest#MANIFEST.SKIP
843
        #   https://neilb.org/2015/10/18/spotters-guide.html#:~:text=MANIFEST.SKIP
844
        #   https://github.com/briandfoy/PerlPowerTools/blob/master/MANIFEST.SKIP
845
        manifest => <<~'END',
846
            # Root allowlist filter
847
            #
848
            # NOTE:
849
            #   Include only specific root dirs or files in the tarball; skip everything else.
850
            #   This keeps dirs like .github and Markdown files only in version control, e.g.
851
            #   README.md, docs directory (contains POD files converted to Markdown with pod2markdown).
852
            \A(?!(?>bin|script|examples|eg|lib|t|xt|share|data)/|(?>Makefile\.PL|README|LICENSE|MANIFEST|Changes|INSTALL|CONTRIBUTING|TODO|SECURITY|META\.(?>json|yml))\z)
853

854
            /MANIFEST(?>\.bak)?\z
855
            /(?>MY)?META\.(?>json|yml)\z
856

857
            # Junk
858
            \.o\z
859
            \.bs\z
860
            \.tar\z
861
            \.tgz\z
862
            \.gz\z
863
            \.zip\z
864
            \.tmp\z
865
            \.old\z
866
            \.bak\z
867
            \.rej\z
868
            \.orig\z
869

870
            # ExtUtils::MakeMaker
871
            /blib/
872
            /Makefile(?>\.old)?\z
873
            /pm_to_blib\z
874

875
            # Devel::Cover
876
            /cover_db/
877
            /\.last_cover_stats\z
878

879
            # Devel::NYTProf
880
            /nytprof\.out\z
881

882
            # git
883
            /\.git/
884
            /\.gitignore\z
885
            END
886
    };
887

888
    # Append regex distro pattern to MANIFEST.SKIP.
889
    $guts->{manifest} .= "\n/\Q$self->{distro}\E-" . '(?s:.+)\z' . "\n";
2✔
890

891
    $guts->{hg} = $guts->{cvs} = $guts->{git} = $guts->{generic};
2✔
892

893
    return $guts->{$type};
2✔
894
}
895

896
sub create_file ( $self, $fname, @content )
19✔
897
{
19✔
898
    if ( -f $fname ) {
19✔
899
        if ( !$self->{force} ) {
×
900
            warn "Will not overwrite '$fname' (--force option not enabled)";
×
901
            return;
×
902
        }
903
    }
904

905
    open my $fh, '>', $fname or confess "Can't create $fname: $!\n";
19✔
906
    print $fh @content;
19✔
907
    close $fh or die "Can't close $fname: $!\n";
19✔
908

909
    return;
19✔
910
}
911

912
# Register the default boilerplate texts.
913
sub _build_boilerplates ($self)
914
{
1✔
915
    # Use signatures if minimum perl is >= v5.36.0.
916
    my $use_sig = version->parse( $self->{minperl} ) >= version->parse('v5.36.0');
1✔
917

918
    # Modules
919
    {
920
        $self->{bp}{header} = $self->_get_header;
1✔
921

922
        $self->{bp}{exports} = <<~'END';
1✔
923
            A list of functions that can be exported. Delete this section if nothing is
924
            exported, such as for a purely object-oriented module.
925
            END
926

927
        $self->{bp}{stub_function1} = <<~'END';
1✔
928
            sub function1
929
            {
930
            }
931
            END
932
        $self->{bp}{stub_function1} =~ s{\Asub\ function1\K\n}{ ()\n} if $use_sig;
1✔
933

934
        $self->{bp}{stub_function2} = <<~'END';
1✔
935
            sub function2
936
            {
937
            }
938
            END
939
        $self->{bp}{stub_function2} =~ s{\Asub\ function2\K\n}{ ()\n} if $use_sig;
1✔
940

941
        $self->{bp}{abstract} = 'new abstract';
1✔
942

943
        $self->{bp}{synopsis} = <<~"END";
1✔
944
            Quick summary of what the module does.
945

946
            With brief examples:
947

948
                # Procedural
949

950
                use $self->{main_module} qw< function >;
951

952
                my \$foo = function(...);
953
                ...
954

955
                # OOP
956

957
                use $self->{main_module};
958

959
                my \$foo = $self->{main_module}->new;
960
                \$foo->method(...);
961
                ...
962
            END
963

964
        $self->{bp}{description} = 'Overview or extended description and discussion of the module.';
1✔
965

966
        $self->{bp}{functions} = <<~'END';
1✔
967
            =head2 function1
968

969
            =head2 function2
970
            END
971

972
        $self->{bp}{see_also} = <<~'END';
1✔
973
        =over 4
974

975
        =item *
976

977
        L<Some::Module>
978

979
        =item *
980

981
        L<https://some-reference.TLD>
982

983
        =back
984
        END
985
    }
986

987
    # Changes
988
    {
989
        $self->{bp}{changelog} = <<~"END";
1✔
990
            $DIST_VERSION    YYYY-MM-DD HH:MM:SSZ
991
                      - Initial release
992
            END
993
    }
994

995
    # README
996
    $self->{bp}{readme_intro} = $self->_README_intro;
1✔
997

998
    # LICENSE
999
    $self->{bp}{license} = $self->_get_license;
1✔
1000
}
1001

1002
# Returns a header boilerplate; accepts a minimum perl version.
1003
sub _get_header ( $self, $minperl //= $self->{minperl} )
1✔
1004
{
1✔
1005
    # Fatal warnings are bad, do not use it.
1006
    #my $warnings = sprintf 'warnings%s;', ( $self->{fatalize} ? q{ FATAL => 'all'} : '' );
1007
    my $warnings = 'warnings;';
1✔
1008

1009
    # Only declare a minimum perl version if the user wants it.
1010
    $minperl =
1011
      defined $self->{no_minperl} && $self->{no_minperl}
1012
      ? ''
1✔
1013
      : "use $minperl;\n\n";
1014

1015
    my $header = $minperl . <<~"END";
1✔
1016
        use strict;
1017
        use $warnings
1018

1019
        END
1020

1021
    return $header;
1✔
1022
}
1023

1024
# Returns resources metadata information.
1025
sub _get_resources ($self)
1026
{
1✔
1027
    my $author     = $self->{github} // $self->{author_name} =~ tr{ }{-}r;
1✔
1028
    my $homepage   = '';
1✔
1029
    my $repository = "https://github.com/$author/$self->{distro}";
1✔
1030
    my $gh_issues  = "$repository/issues";
1✔
1031
    my $bug_tracker =
1032
      defined $self->{github}
1033
      ? $gh_issues
1✔
1034
      : "https://rt.cpan.org/NoAuth/Bugs.html?Dist=$self->{distro}";
1035

1036
    return {
1037
        repository  => $repository,
1✔
1038
        bug_tracker => $bug_tracker,
1039
        homepage    => $homepage,
1040
    };
1041
}
1042

1043
# Returns LICENSE boilerplate; accepts 'POD' argument (if true, returns LICENSE in POD format).
1044
sub _get_license ( $self, %opts )
3✔
1045
{
3✔
1046
    my $current_year = $self->_thisyear;
3✔
1047
    my $name         = $self->{license_record}->spdx_expression // $self->{license};
3✔
1048

1049
    $self->{license_record}{holder} = $self->{author_name};
3✔
1050

1051
    chomp( my $license = <<~"END" );
3✔
1052
        Copyright © $current_year $self->{author_name}
1053
        $name License. See LICENSE for details.
1054
        END
1055

1056
    # Insert blank lines between lines (POD format).
1057
    $license =~ s{
1058
        ^[^\n]+\n
1059
        \K
1060
        (?=
1061
            [^\n]+
1062
            (?> \n | \z)
1063
        )
1064
    }
1065
    {\n}mgx if $opts{POD};
3✔
1066

1067
    return $license;
3✔
1068
}
1069

1070
=encoding UTF-8
1071

1072
=head1 NAME
1073

1074
Module::Starter::Plugin::MyGuts - module starter with opinionated settings
1075

1076
=head1 SYNOPSIS
1077

1078
In your F<~/.module-starter/config>:
1079

1080
  builder:      ExtUtils::MakeMaker
1081
  license:      MIT_0
1082
  genlicense:   1
1083
  ignores_type: git manifest
1084
  author:       author <author@email>
1085
  minperl:      v5.40.0
1086
  verbose:      1
1087
  plugins:      Module::Starter::Plugin::MyGuts
1088

1089
Then, run:
1090

1091
=for highlighter language=shell
1092

1093
  $ module-starter --module=Foo::Bar
1094

1095
Alternatively:
1096

1097
  $ module-starter \
1098
      --module=Foo::Bar \
1099
      --eumm \
1100
      --license=MIT_0 \
1101
      --genlicense \
1102
      --ignores=git,manifest \
1103
      --author='author <author@email>' \
1104
      --minperl=v5.40.0 \
1105
      --verbose \
1106
      --plugin=Module::Starter::Plugin::MyGuts
1107

1108
=head1 DESCRIPTION
1109

1110
This plugin is a subclass of L<Module::Starter::Simple> that replaces some of its
1111
B<*_guts> methods with my preferred settings, thus not intended for public usage.
1112
Inspired by L<Module::Starter::Plugin::Template>.
1113

1114
Note that only L<ExtUtils::MakeMaker> and single author are supported for simplicity.
1115

1116
=head1 METHODS
1117

1118
=head2 new(I<%args>)
1119

1120
Calls the C<new> C<SUPER> method.
1121

1122
=head2 post_create_distro
1123

1124
If C<github> is set, creates the .github/workflows directory and calls C<create_CI()>,
1125
creates the docs directory (intended to contain POD files from distribution converted
1126
to Markdown), and then calls C<create_README_md()>.
1127

1128
=head2 create_CI ( I<$self, $fpath> )
1129

1130
If C<github> is set, creates the distribution's F<ci.yml> file in .github/workflows
1131
directory.
1132

1133
=head2 create_README_md
1134

1135
If C<github> is set, creates the distribution's F<README.md> file.
1136

1137
=head2 *_guts
1138

1139
These L<Module::Starter::Simple> methods are subclassed to look like this:
1140

1141
=for highlighter language=perl
1142

1143
    sub module_guts ( $self, @args )
1144
    {
1145
        ...
1146
    }
1147

1148
=over 4
1149

1150
=item module_guts
1151

1152
=item Makefile_PL_guts
1153

1154
=item Makefile_PL_meta_merge
1155

1156
=item Changes_guts
1157

1158
=item README_guts
1159

1160
=item t_guts
1161

1162
=item xt_guts
1163

1164
=item ignores_guts
1165

1166
=back
1167

1168
=head2 create_file( I<$fname, @content_lines> )
1169

1170
Overrides C<Module::Starter::Simple::create_file()> so files are created with UTF-8
1171
encoding accordingly.
1172

1173
See L<Module::Starter::Simple/create_file(-$fname,-@content_lines-)>.
1174

1175
=head1 CONFIGURATION
1176

1177
=over 4
1178

1179
=item B<no_minperl>
1180

1181
If true, a minimum perl version is not declared in the files. Default: C<undef>.
1182

1183
=back
1184

1185
=head1 BUGS
1186

1187
Report bugs at L<https://github.com/ryoskzypu/Module-Starter-Plugin-MyGuts/issues>.
1188

1189
=head1 AUTHOR
1190

1191
ryoskzypu <ryoskzypu@proton.me>
1192

1193
=head1 SEE ALSO
1194

1195
=over 4
1196

1197
=item *
1198

1199
L<Module::Starter>
1200

1201
=item *
1202

1203
L<Module::Starter::Simple>
1204

1205
=item *
1206

1207
L<Module::Starter::Plugin::Template>
1208

1209
=item *
1210

1211
L<https://neilb.org/2015/09/05/cpan-glossary.html>
1212

1213
=item *
1214

1215
L<https://neilb.org/2015/10/18/spotters-guide.html>
1216

1217
=item *
1218

1219
L<https://github.com/Perl-Toolchain-Gang/toolchain-site/blob/master/cpan-packaging.md>
1220

1221
=back
1222

1223
=head1 COPYRIGHT
1224

1225
Copyright © 2026 ryoskzypu
1226

1227
MIT-0 License. See LICENSE for details.
1228

1229
=cut
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc