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

ryoskzypu / Module-Starter-Plugin-MyGuts / 23615527215

26 Mar 2026 08:06PM UTC coverage: 96.364% (+0.02%) from 96.341%
23615527215

push

github

ryoskzypu
Make 02-expected-tree.t pass

159 of 165 relevant lines covered (96.36%)

2.08 hits per line

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

96.36
/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
51
        #   the distribution files that contain POD to Markdown (with pod2gfm) and
52
        #   put 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 {
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://metacpan.org/pod/version
181
#   https://metacpan.org/pod/CPAN::Meta::Spec#Dotted-integer-versions
182
#   https://www.neilb.org/2015/12/20/specify-perl-version.html
183
#   https://old.reddit.com/r/perl/comments/5i4vn9/version_numbers/
184
#   https://semver.org/
185
#   https://blogs.perl.org/users/dean/2022/08/please-relicense-from-perl-5-to-mit-or-apache-20-license.html
186
#   https://github.com/aws/mit-0
187
sub module_guts ( $self, $module, $rtname )
1✔
188
{
1✔
189
    # Remove true value at the end if minimum perl is >= v5.38.0.
190
    my $mod_true = version->parse( $self->{minperl} ) >= version->parse('v5.38.0');
1✔
191

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

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

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

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

211
        \=head1 NAME
212

213
        $module - $self->{bp}{abstract}
214

215
        \=head1 SYNOPSIS
216

217
        $self->{bp}{synopsis}
218
        \=head1 DESCRIPTION
219

220
        $self->{bp}{description}
221

222
        \=head1 EXPORTS
223

224
        $self->{bp}{exports}
225
        \=head1 SUBROUTINES/METHODS
226

227
        $self->{bp}{functions}
228
        \=head1 BUGS
229

230
        Report bugs at L<$self->{res}{bug_tracker}>.
231

232
        \=head1 AUTHOR
233

234
        $self->{author_full}
235

236
        \=head1 SEE ALSO
237

238
        $self->{bp}{see_also}
239
        \=head1 COPYRIGHT
240

241
        $license
242

243
        \=cut
244

245
        1;
246
        END
247

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

255
    return $content;
1✔
256
}
257

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

273
    my $meta_merge = $self->Makefile_PL_meta_merge;
1✔
274

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

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

303
        WriteMakefile(%WriteMakefileArgs);
304
        END
305

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

312
    return $makefile;
1✔
313
}
314

315
# See:
316
#   https://metacpan.org/pod/ExtUtils::MakeMaker#META_MERGE
317
#   https://metacpan.org/pod/CPAN::Meta::Spec#Prereq-Spec
318
#   https://blogs.perl.org/users/neilb/2017/04/specifying-the-type-of-your-cpan-dependencies.html
319
#   https://perlmaven.com/how-to-add-link-to-version-control-system-of-a-cpan-distributions
320
#   https://metacpan.org/pod/CPAN::Meta::Spec#resources.
321
#   https://metacpan.org/about/metadata
322
#   https://libera.chat/guides/webchat
323
#   https://perlmaven.com/how-to-add-list-of-contributors-to-the-cpan-meta-files
324
sub Makefile_PL_meta_merge ($self)
325
{
1✔
326
    my $spdx_exp = $self->{license_record}->spdx_expression // '';
1✔
327

328
    return <<~"END";
1✔
329
        META_MERGE => {
330
            'meta-spec' => { version => 2 },
331
            no_index    => {
332
                directory => [
333
                    qw<
334
                        eg
335
                        examples
336
                        share
337
                        xt
338
                    >
339
                ],
340
            },
341
            prereqs => {
342
                develop => {
343
                    recommends => {
344
                        'App::CPANTS::Lint' => '0',
345
                        'Data::Printer'     => '0',
346
                        'Devel::Cover'      => '0',
347
                        'Perl::Critic'      => '0',
348
                        'Perl::Tidy'        => '0',
349
                    },
350
                    requires => {
351
                        'Test::CPAN::Changes' => '0',
352
                        'Test::Kwalitee'      => '0',
353
                        'Test::Perl::Critic'  => '0',
354
                        'Test::Pod'           => '0',
355
                        'Test::Pod::Coverage' => '0',
356
                        'Test::Spelling'      => '0',
357
                    },
358
                },
359
                #runtime => {
360
                #    recommends => {
361
                #        'Foo::Bar' => '0',
362
                #    },
363
                #    suggests => {
364
                #        'Foo::Bat' => '0',
365
                #    },
366
                #},
367
                #test => {
368
                #    recommends => {
369
                #        'Foo::Bar' => '0',
370
                #    },
371
                #    suggests => {
372
                #        'Foo::Bat' => '0',
373
                #    },
374
                #},
375
            },
376
            resources => {
377
                repository => {
378
                    type => 'git',
379
                    url  => '$self->{res}{repository}.git',
380
                    web  => '$self->{res}{repository}',
381
                },
382
                bugtracker => {
383
                    web => '$self->{res}{bug_tracker}',
384
                },
385
                #homepage => '$self->{res}{homepage}',
386
                #'x_IRC' => {
387
                #    url => 'irc://irc.libera.chat/#channel',
388
                #    web => 'https://web.libera.chat/?nick=Guest?#channel',
389
                #}
390
            },
391
            x_contributors => [
392
                q{$self->{author_full}},
393
            ],
394
            x_spdx_expression => '$spdx_exp',
395
        },
396
    END
397
}
398

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

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

410
        $changelog
411
        END
412
}
413

414
# NOTE:
415
#   For consistency, at least make sure to replace the README intro with the POD
416
#   description from main module (convert with pod2text).
417
#
418
# See:
419
#   https://neilb.org/2015/10/18/spotters-guide.html#text:~:text=README
420
sub README_guts ( $self, $build_instructions )
2✔
421
{
2✔
422
    my $bugs_header =
423
      defined $self->{github}
424
      ? 'GitHub issue tracker (report bugs here)'
2✔
425
      : q{CPAN's request tracker (report bugs here)};
426

427
    my $readme = <<~"END";
2✔
428
        $self->{main_module}
429

430
        $self->{bp}{readme_intro}
431

432
        INSTALLATION
433

434
        To download and install this module, use your favorite CPAN client:
435

436
            cpanm $self->{main_module}
437

438
        To do it manually, run the following commands (after downloading and unpacking
439
        the tarball):
440

441
            perl Makefile.PL
442
            make
443
            make test
444
            make install
445

446

447
        SUPPORT AND DOCUMENTATION
448

449
        After installing, you can find documentation for this module with the perldoc
450
        command:
451

452
            perldoc $self->{main_module}
453

454
        You can also look for information at:
455

456
            $bugs_header
457
                $self->{res}{bug_tracker}
458

459
            Search CPAN
460
                https://metacpan.org/dist/$self->{distro}
461

462

463
        COPYRIGHT
464

465
        $self->{bp}{license}
466
        END
467

468
    return $readme;
2✔
469
}
470

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

482
    foreach my $mod (@modules) {
1✔
483
        $use_mod .= "use ok '$mod';\n";
1✔
484
    }
485
    chomp $use_mod;
1✔
486

487
    $t_files{'00-load.t'} = $header . <<~"END";
1✔
488
        use Test2::V1;
489

490
        $use_mod
491

492
        foreach my \$mod ( qw< @modules > ) {
493
            my \$mod_ver = '\$' . \$mod . '::VERSION';
494

495
            T2->diag(
496
                sprintf "Testing \$mod %s, Perl %s, %s",
497
                \$mod_ver, \$], \$^X,
498
            );
499
        }
500

501
        T2->done_testing;
502
        END
503

504
    return %t_files;
1✔
505
}
506

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

515
    # perlcritic
516
    # https://metacpan.org/pod/Test::Perl::Critic
517
    {
518
        $xt_files{'critic.t'} = $header . <<~'END';
1✔
519
        use Test2::Require::Module qw< Test::Perl::Critic >;
520
        use Test::Perl::Critic;
521

522
        my $EXE = 'bin';
523

524
        my @FILES = (
525
            qw<
526
                Makefile.PL
527
                lib
528
                t
529
                xt
530
            >
531
        );
532

533
        push @FILES, $EXE if -e $EXE && -d $EXE;
534

535
        all_critic_ok(@FILES);
536
        END
537
    }
538

539
    # Manifest tests
540
    # https://metacpan.org/pod/ExtUtils::Manifest
541
    {
542
        $xt_files{'manifest.t'} = $header . <<~'END';
1✔
543
            use Test2::V1 qw< is >;
544
            T2->plan(2);
545

546
            use ExtUtils::Manifest qw< manicheck filecheck >;
547

548
            is(
549
                [ manicheck() ], [],
550
                'manicheck() - missing files',
551
            );
552

553
            is(
554
                [ filecheck() ], [],
555
                'filecheck() - extra files',
556
            );
557
            END
558
    }
559

560
    # Changes tests
561
    # https://metacpan.org/pod/Test::CPAN::Changes
562
    {
563
        $xt_files{'cpan-changes.t'} = $header . <<~'END';
1✔
564
            use Test2::Require::Module qw< Test::CPAN::Changes >;
565
            use Test::CPAN::Changes;
566

567
            changes_ok();
568
            END
569
    }
570

571
    # POD tests
572
    #
573
    # https://metacpan.org/pod/Test::Pod
574
    # https://metacpan.org/pod/Test::Pod::Coverage
575
    # https://metacpan.org/pod/Test::Spelling
576
    {
577
        $xt_files{'pod-syntax.t'} = $header . <<~'END';
1✔
578
            use Test2::Require::Module qw< Test::Pod >;
579
            use Test::Pod;
580

581
            my $EXE = 'bin';
582

583
            my @DIRS = (
584
                qw<
585
                    lib
586
                >
587
            );
588

589
            push @DIRS, $EXE if -e $EXE && -d $EXE;
590

591
            all_pod_files_ok( all_pod_files(@DIRS) );
592
            END
593

594
        $xt_files{'pod-coverage.t'} = $header . <<~'END';
1✔
595
            use Test2::Require::Module qw< Test::Pod::Coverage >;
596
            use Test::Pod::Coverage;
597

598
            all_pod_coverage_ok();
599
            END
600

601
        $xt_files{'pod-spell.t'} = $header . <<~"_";
1✔
602
            use Test2::V1              qw< diag >;
603
            use Test2::Require::Module qw< Test::Spelling >;
604

605
            use Test::Spelling;
606
            use Pod::Wordlist;
607

608
            diag <<'END';
609
            NOTE:
610
              This test requires a spellchecker with an English dictionary installed, e.g. aspell.
611

612
            END
613

614
            add_stopwords(<DATA>);
615

616
            all_pod_files_spelling_ok(
617
                qw<
618
                    bin
619
                    script
620
                    lib
621
                >
622
            );
623

624
            __DATA__
625
            $self->{author_name}
626
            _
627
    }
628

629
    # Kwalitee tests
630
    #
631
    # https://metacpan.org/pod/App::CPANTS::Lint
632
    # https://metacpan.org/pod/Test::Kwalitee
633
    {
634
        $xt_files{'kwalitee.t'} = $header . <<~"_";
1✔
635
            use Test2::V1              qw< diag >;
636
            use Test2::Require::Module qw< Test::Kwalitee >;
637

638
            use Test::Kwalitee qw< kwalitee_ok >;
639

640
            diag <<'END';
641
            NOTE:
642
              This test must be done in the unpacked release tarball directory, which
643
              misses some kwalitee indicators.
644

645
              For a more complete test, install App::CPANTS::Lint and run it on the
646
              release tarball:
647

648
                \$ cpanm App::CPANTS::Lint
649
                \$ cpants_lint.pl --color --verbose $self->{distro}-$DIST_VERSION.tar.gz
650

651
            END
652

653
            kwalitee_ok();
654
            T2->done_testing;
655
            _
656
    }
657

658
    # Boilerplate tests
659
    {
660
        my $module_bp_tests;
1✔
661

662
        foreach my $mod (@modules) {
1✔
663
            my $file = $self->_module_to_pm_file($mod);
1✔
664
            $module_bp_tests .= "not_in_file_ok('$file');\n";
1✔
665
        }
666
        chomp $module_bp_tests;
1✔
667

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

677
            END
678

679
        $xt_files{'boilerplate.t'} .= $self->{bp}{header} . <<~'END';
1✔
680
            use Test2::V1 qw<
681
                note
682
                diag
683
                pass
684
                fail
685
            >;
686

687
            use re qw< eval >;
688
            #use DDP output => 'stdout';
689

690
            sub not_in_file_ok
691
            {
692
                my $filename = shift;
693

694
                note("FILENAME: $filename\n\n");
695

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

700
                my $desc;
701
                my @regex_type;
702

703
            END
704

705
        # Inject the boilerplates texts for their regex compilations.
706
        $xt_files{'boilerplate.t'} .= <<~"END";
1✔
707
                my \@readme_rgx = (
708
                    '\Q$self->{bp}{readme_intro}\E(?{ \$desc = "introduction" })',
709
                );
710

711
                my \@changes_rgx = (
712
                    '\Q$self->{bp}{changelog}\E(?{ \$desc = "changelog" })',
713
                );
714

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

726
            END
727

728
        $xt_files{'boilerplate.t'} .= <<~'END';
1✔
729
                foreach ($filename) {
730
                    if    (/\AREADME\z/)  { push @regex_type, @readme_rgx }
731
                    elsif (/\AChanges\z/) { push @regex_type, @changes_rgx }
732
                    elsif (/\.pm\z/)      { push @regex_type, @modules_rgx }
733
                    else                  { die "$filename is not supported" }
734
                }
735

736
                #p @regex_type;
737
                #note("\n");
738

739
                # Concat and compile the regexes.
740
                my $joined = join '|', @regex_type;
741
                my $regex  = qr{^(?> $joined )}mx;
742

743
                #note("REGEX:\n");
744
                #p $regex;
745
                #note("\n");
746

747
                my $c = 0;
748

749
                # Scan the file with its respective regex type.
750
                while (1) {
751
                    # Boilerplate
752
                    if ( $file =~ /\G$regex/gc ) {
753
                        my $end = '';
754
                        ++$c;
755

756
                        note("MATCH:\n$c '$&'\n");
757
                        fail("$filename contains $desc boilerplate text");
758

759
                        # Count lines of a multiline match.
760
                        my $nl = $& =~ tr{\n}{};
761
                        if ( $nl > 1 ) {
762
                            $nl  += $c - 1;
763
                            $end  = ',' . $nl;
764
                        }
765

766
                        diag("$desc appears on lines ${c}$end");
767
                        note("\n");
768

769
                        $c = $nl;
770
                    }
771
                    # Generic line (forwards scanner line-by-line).
772
                    elsif ( $file =~ /\G([^\n]*+)\n/gc ) {
773
                        ++$c;
774
                        #note("$c '$1'");
775
                    }
776

777
                    # End of file.
778
                    if ( $file =~ /\G\z/ ) {
779
                        pass("$filename contains no boilerplate text") unless defined $desc;
780
                        note("\n");
781

782
                        last;
783
                    }
784
                }
785
            }
786

787
            not_in_file_ok('README');
788
            not_in_file_ok('Changes');
789
            END
790

791
        $xt_files{'boilerplate.t'} .= <<~"END";
1✔
792
            $module_bp_tests
793

794
            T2->done_testing;
795
            END
796
    }
797

798
    return %xt_files;
1✔
799
}
800

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

818
            # Junk
819
            *.o
820
            *.bs
821
            *.tar
822
            *.tgz
823
            *.gz
824
            *.zip
825
            *.tmp
826
            *.old
827
            *.bak
828
            *.rej
829
            *.orig
830

831
            # ExtUtils::MakeMaker
832
            blib/
833
            Makefile
834
            Makefile.old
835
            pm_to_blib
836

837
            # Devel::Cover
838
            cover_db/
839
            .last_cover_stats
840

841
            # Devel::NYTProf
842
            nytprof.out
843

844
            $self->{distro}-*
845
            END
846

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

860
            /MANIFEST(?>\.bak)?\z
861
            /(?>MY)?META\.(?>json|yml)\z
862

863
            # Junk
864
            \.o\z
865
            \.bs\z
866
            \.tar\z
867
            \.tgz\z
868
            \.gz\z
869
            \.zip\z
870
            \.tmp\z
871
            \.old\z
872
            \.bak\z
873
            \.rej\z
874
            \.orig\z
875

876
            # ExtUtils::MakeMaker
877
            /blib/
878
            /Makefile(?>\.old)?\z
879
            /pm_to_blib\z
880

881
            # Devel::Cover
882
            /cover_db/
883
            /\.last_cover_stats\z
884

885
            # Devel::NYTProf
886
            /nytprof\.out\z
887

888
            # git
889
            /\.git/
890
            /\.gitignore\z
891
            END
892
    };
893

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

897
    $guts->{hg} = $guts->{cvs} = $guts->{git} = $guts->{generic};
2✔
898

899
    return $guts->{$type};
2✔
900
}
901

902
sub create_file ( $self, $fname, @content )
19✔
903
{
19✔
904
    if ( -f $fname ) {
19✔
905
        if ( !$self->{force} ) {
×
906
            warn "Will not overwrite '$fname' (--force option not enabled)";
×
907
            return;
×
908
        }
909
    }
910

911
    open my $fh, '>', $fname or confess "Can't create $fname: $!\n";
19✔
912
    print $fh @content;
19✔
913
    close $fh or die "Can't close $fname: $!\n";
19✔
914

915
    return;
19✔
916
}
917

918
# Register the default boilerplate texts.
919
sub _build_boilerplates ($self)
920
{
1✔
921
    # Use signatures if minimum perl is >= v5.36.0.
922
    my $use_sig = version->parse( $self->{minperl} ) >= version->parse('v5.36.0');
1✔
923

924
    # Modules
925
    {
926
        $self->{bp}{header} = $self->_get_header;
1✔
927

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

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

940
        $self->{bp}{stub_function2} = <<~'END';
1✔
941
            sub function2
942
            {
943
            }
944
            END
945
        $self->{bp}{stub_function2} =~ s{\Asub\ function2\K\n}{ ()\n} if $use_sig;
1✔
946

947
        $self->{bp}{abstract} = 'new abstract';
1✔
948

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

952
            With brief examples:
953

954
                # Procedural
955

956
                use $self->{main_module} qw< function >;
957

958
                my \$foo = function(...);
959
                ...
960

961
                # OOP
962

963
                use $self->{main_module};
964

965
                my \$foo = $self->{main_module}->new;
966
                \$foo->method(...);
967
                ...
968
            END
969

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

972
        $self->{bp}{functions} = <<~'END';
1✔
973
            =head2 function1
974

975
            =head2 function2
976
            END
977

978
        $self->{bp}{see_also} = <<~'END';
1✔
979
        =over 4
980

981
        =item *
982

983
        L<Some::Module>
984

985
        =item *
986

987
        L<https://some-reference.TLD>
988

989
        =back
990
        END
991
    }
992

993
    # Changes
994
    {
995
        $self->{bp}{changelog} = <<~"END";
1✔
996
            $DIST_VERSION    YYYY-MM-DD HH:MM:SSZ
997
                      - Initial release
998
            END
999
    }
1000

1001
    # README
1002
    $self->{bp}{readme_intro} = $self->_README_intro;
1✔
1003

1004
    # LICENSE
1005
    $self->{bp}{license} = $self->_get_license;
1✔
1006
}
1007

1008
# Returns a header boilerplate; accepts a minimum perl version.
1009
sub _get_header ( $self, $minperl //= $self->{minperl} )
1✔
1010
{
1✔
1011
    # Fatal warnings are bad, do not use it.
1012
    #my $warnings = sprintf 'warnings%s;', ( $self->{fatalize} ? q{ FATAL => 'all'} : '' );
1013
    my $warnings = 'warnings;';
1✔
1014

1015
    # Only declare a minimum perl version if the user wants it.
1016
    $minperl =
1017
      defined $self->{no_minperl} && $self->{no_minperl}
1018
      ? ''
1✔
1019
      : "use $minperl;\n\n";
1020

1021
    my $header = $minperl . <<~"END";
1✔
1022
        use strict;
1023
        use $warnings
1024

1025
        END
1026

1027
    return $header;
1✔
1028
}
1029

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

1042
    return {
1043
        repository  => $repository,
1✔
1044
        bug_tracker => $bug_tracker,
1045
        homepage    => $homepage,
1046
    };
1047
}
1048

1049
# Returns LICENSE boilerplate; accepts 'POD' argument (if true, returns LICENSE in POD format).
1050
sub _get_license ( $self, %opts )
3✔
1051
{
3✔
1052
    my $current_year = $self->_thisyear;
3✔
1053
    my $name         = $self->{license_record}->spdx_expression // $self->{license};
3✔
1054

1055
    $self->{license_record}{holder} = $self->{author_name};
3✔
1056

1057
    chomp( my $license = <<~"END" );
3✔
1058
        Copyright © $current_year $self->{author_name}
1059
        $name License. See LICENSE for details.
1060
        END
1061

1062
    # Insert blank lines between lines (POD format).
1063
    $license =~ s{
1064
        ^[^\n]+\n
1065
        \K
1066
        (?=
1067
            [^\n]+
1068
            (?> \n | \z)
1069
        )
1070
    }
1071
    {\n}mgx if $opts{POD};
3✔
1072

1073
    return $license;
3✔
1074
}
1075

1076
=encoding UTF-8
1077

1078
=head1 NAME
1079

1080
Module::Starter::Plugin::MyGuts - module starter with opinionated settings
1081

1082
=head1 SYNOPSIS
1083

1084
In your F<~/.module-starter/config>:
1085

1086
  builder:      ExtUtils::MakeMaker
1087
  license:      MIT_0
1088
  genlicense:   1
1089
  ignores_type: git manifest
1090
  author:       author <author@email>
1091
  minperl:      v5.40.0
1092
  verbose:      1
1093
  plugins:      Module::Starter::Plugin::MyGuts
1094

1095
Then, run:
1096

1097
=for highlighter language=shell
1098

1099
  $ module-starter --module=Foo::Bar
1100

1101
Alternatively:
1102

1103
  $ module-starter \
1104
      --module=Foo::Bar \
1105
      --eumm \
1106
      --license=MIT_0 \
1107
      --genlicense \
1108
      --ignores=git,manifest \
1109
      --author='author <author@email>' \
1110
      --minperl=v5.40.0 \
1111
      --verbose \
1112
      --plugin=Module::Starter::Plugin::MyGuts
1113

1114
=head1 DESCRIPTION
1115

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

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

1122
=head1 METHODS
1123

1124
=head2 new(I<%args>)
1125

1126
Calls the C<new> C<SUPER> method.
1127

1128
=head2 post_create_distro
1129

1130
If C<github> is set, creates the .github/workflows directory and calls C<create_CI()>,
1131
creates the docs directory (intended to contain POD files from distribution converted
1132
to Markdown), and then calls C<create_README_md()>.
1133

1134
=head2 create_CI ( I<$self, $fpath> )
1135

1136
If C<github> is set, creates the distribution's F<ci.yml> file in .github/workflows
1137
directory.
1138

1139
=head2 create_README_md
1140

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

1143
=head2 *_guts
1144

1145
These L<Module::Starter::Simple> methods are subclassed to look like this:
1146

1147
=for highlighter language=perl
1148

1149
    sub module_guts ( $self, @args )
1150
    {
1151
        ...
1152
    }
1153

1154
=over 4
1155

1156
=item module_guts
1157

1158
=item Makefile_PL_guts
1159

1160
=item Makefile_PL_meta_merge
1161

1162
=item Changes_guts
1163

1164
=item README_guts
1165

1166
=item t_guts
1167

1168
=item xt_guts
1169

1170
=item ignores_guts
1171

1172
=back
1173

1174
=head2 create_file( I<$fname, @content_lines> )
1175

1176
Overrides C<Module::Starter::Simple::create_file()> so files are created with UTF-8
1177
encoding accordingly.
1178

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

1181
=head1 CONFIGURATION
1182

1183
=over 4
1184

1185
=item B<no_minperl>
1186

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

1189
=back
1190

1191
=head1 BUGS
1192

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

1195
=head1 AUTHOR
1196

1197
ryoskzypu <ryoskzypu@proton.me>
1198

1199
=head1 SEE ALSO
1200

1201
=over 4
1202

1203
=item *
1204

1205
L<Module::Starter>
1206

1207
=item *
1208

1209
L<Module::Starter::Simple>
1210

1211
=item *
1212

1213
L<Module::Starter::Plugin::Template>
1214

1215
=item *
1216

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

1219
=item *
1220

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

1223
=item *
1224

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

1227
=back
1228

1229
=head1 COPYRIGHT
1230

1231
Copyright © 2026 ryoskzypu
1232

1233
MIT-0 License. See LICENSE for details.
1234

1235
=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