Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
HydeBuildStaticSiteCommand
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
10 / 10
27
100.00% covered (success)
100.00%
1 / 1
 handle
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 runPreBuildActions
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 runPostBuildActions
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 printFinishMessage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 purge
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getModelPluralName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 runNodeCommand
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 canGenerateSitemap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canGenerateFeed
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 canGenerateSearch
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Hyde\Framework\Commands;
4
5use Exception;
6use Hyde\Framework\Actions\CreatesDefaultDirectories;
7use Hyde\Framework\Concerns\Internal\BuildActionRunner;
8use Hyde\Framework\Concerns\Internal\TransfersMediaAssetsForBuildCommands;
9use Hyde\Framework\Helpers\Features;
10use Hyde\Framework\Hyde;
11use Hyde\Framework\Models\BladePage;
12use Hyde\Framework\Models\DocumentationPage;
13use Hyde\Framework\Models\MarkdownPage;
14use Hyde\Framework\Models\MarkdownPost;
15use Hyde\Framework\Services\CollectionService;
16use Hyde\Framework\Services\DiscoveryService;
17use Hyde\Framework\Services\RssFeedService;
18use Hyde\Framework\Services\SitemapService;
19use Illuminate\Support\Facades\Artisan;
20use Illuminate\Support\Facades\Config;
21use Illuminate\Support\Facades\File;
22use LaravelZero\Framework\Commands\Command;
23
24/**
25 * Hyde Command to run the Build Process.
26 *
27 * @see \Tests\Feature\Commands\BuildStaticSiteCommandTest
28 */
29class HydeBuildStaticSiteCommand extends Command
30{
31    use BuildActionRunner;
32    use TransfersMediaAssetsForBuildCommands;
33
34    /**
35     * The signature of the command.
36     *
37     * @var string
38     */
39    protected $signature = 'build 
40        {--run-dev : Run the NPM dev script after build}
41        {--run-prod : Run the NPM prod script after build}
42        {--pretty : Deprecated option, use --run-prettier instead}
43        {--run-prettier : Format the output using NPM Prettier}
44        {--pretty-urls : Should links in output use pretty URLs?}
45        {--no-api : Disable API calls, for example, Torchlight}';
46
47    /**
48     * The description of the command.
49     *
50     * @var string
51     */
52    protected $description = 'Build the static site';
53
54    /**
55     * Execute the console command.
56     *
57     * @return int
58     *
59     * @throws Exception
60     */
61    public function handle(): int
62    {
63        $time_start = microtime(true);
64
65        $this->title('Building your static site!');
66
67        $this->runPreBuildActions();
68
69        $this->purge();
70
71        $this->transferMediaAssets();
72
73        if (Features::hasBladePages()) {
74            $this->runBuildAction(BladePage::class);
75        }
76
77        if (Features::hasMarkdownPages()) {
78            $this->runBuildAction(MarkdownPage::class);
79        }
80
81        if (Features::hasBlogPosts()) {
82            $this->runBuildAction(MarkdownPost::class);
83        }
84
85        if (Features::hasDocumentationPages()) {
86            $this->runBuildAction(DocumentationPage::class);
87        }
88
89        $this->runPostBuildActions();
90
91        $this->printFinishMessage($time_start);
92
93        return 0;
94    }
95
96    /** @internal */
97    protected function runPreBuildActions(): void
98    {
99        if ($this->option('no-api')) {
100            $this->info('Disabling external API calls');
101            $this->newLine();
102            $config = config('hyde.features');
103            unset($config[array_search('torchlight', $config)]);
104            Config::set(['hyde.features' => $config]);
105        }
106
107        if ($this->option('pretty-urls')) {
108            $this->info('Generating site with pretty URLs');
109            $this->newLine();
110            Config::set(['hyde.pretty_urls' => true]);
111        }
112    }
113
114    /**
115     * Run any post-build actions.
116     *
117     * @return void
118     */
119    public function runPostBuildActions(): void
120    {
121        if ($this->option('run-prettier') || $this->option('pretty')) {
122            if ($this->option('pretty')) {
123                $this->warn('<error>Warning:</> The --pretty option is deprecated, use --run-prettier instead');
124            }
125            $this->runNodeCommand(
126                'npx prettier '.Hyde::pathToRelative(Hyde::getSiteOutputPath()).'/ --write --bracket-same-line',
127                'Prettifying code!',
128                'prettify code'
129            );
130        }
131
132        if ($this->option('run-dev')) {
133            $this->runNodeCommand('npm run dev', 'Building frontend assets for development!');
134        }
135
136        if ($this->option('run-prod')) {
137            $this->runNodeCommand('npm run prod', 'Building frontend assets for production!');
138        }
139
140        if ($this->canGenerateSitemap()) {
141            Artisan::call('build:sitemap', outputBuffer: $this->output);
142        }
143
144        if ($this->canGenerateFeed()) {
145            Artisan::call('build:rss', outputBuffer: $this->output);
146        }
147
148        if ($this->canGenerateSearch()) {
149            Artisan::call('build:search', outputBuffer: $this->output);
150        }
151    }
152
153    /** @internal */
154    protected function printFinishMessage(float $time_start): void
155    {
156        $time_end = microtime(true);
157        $execution_time = ($time_end - $time_start);
158        $this->info('All done! Finished in '.number_format(
159                $execution_time,
160                2
161            ).' seconds. ('.number_format(($execution_time * 1000), 2).'ms)');
162
163        $this->info('Congratulations! ðŸŽ‰ Your static site has been built!');
164        $this->line(
165            'Your new homepage is stored here -> '.
166            DiscoveryService::createClickableFilepath(Hyde::getSiteOutputPath('index.html'))
167        );
168    }
169
170    /**
171     * Clear the entire output directory before running the build.
172     *
173     * @return void
174     */
175    public function purge(): void
176    {
177        $this->warn('Removing all files from build directory.');
178
179        File::deleteDirectory(Hyde::getSiteOutputPath());
180        mkdir(Hyde::getSiteOutputPath());
181
182        $this->line('<fg=gray> > Directory purged');
183
184        $this->line(' > Recreating directories');
185        (new CreatesDefaultDirectories)->__invoke();
186
187        $this->line('</>');
188    }
189
190    /** @internal */
191    protected function getModelPluralName(string $model): string
192    {
193        return preg_replace('/([a-z])([A-Z])/', '$1 $2', class_basename($model)).'s';
194    }
195
196    /* @internal */
197    private function runNodeCommand(string $command, string $message, ?string $actionMessage = null): void
198    {
199        $this->info($message.' This may take a second.');
200
201        if (app()->environment() === 'testing') {
202            $command = 'echo '.$command;
203        }
204        $output = shell_exec($command);
205
206        $this->line(
207            $output ?? '<fg=red>Could not '.($actionMessage ?? 'run script').'! Is NPM installed?</>'
208        );
209    }
210
211    protected function canGenerateSitemap(): bool
212    {
213        return SitemapService::canGenerateSitemap();
214    }
215
216    protected function canGenerateFeed(): bool
217    {
218        return RssFeedService::canGenerateFeed()
219            && count(CollectionService::getMarkdownPostList()) > 0;
220    }
221
222    protected function canGenerateSearch(): bool
223    {
224        return Features::hasDocumentationSearch()
225            && count(CollectionService::getDocumentationPageList()) > 0;
226    }
227}