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

dragomano / sass-embedded-php / 17546383533

08 Sep 2025 09:35AM UTC coverage: 90.291% (-9.7%) from 100.0%
17546383533

push

github

dragomano
Update coverage.yml

93 of 103 relevant lines covered (90.29%)

6.61 hits per line

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

90.29
/src/Compiler.php
1
<?php declare(strict_types=1);
2

3
namespace Bugo\Sass;
4

5
use Symfony\Component\Process\Process;
6

7
use function array_merge;
8
use function base64_encode;
9
use function basename;
10
use function file_exists;
11
use function file_get_contents;
12
use function file_put_contents;
13
use function filemtime;
14
use function filter_var;
15
use function is_array;
16
use function is_dir;
17
use function json_decode;
18
use function json_encode;
19
use function parse_url;
20
use function pathinfo;
21
use function realpath;
22
use function strtolower;
23
use function strtoupper;
24
use function substr;
25
use function trim;
26

27
class Compiler implements CompilerInterface
28
{
29
    protected array $options = [];
30

31
    public function __construct(protected ?string $bridgePath = null, protected ?string $nodePath = null)
32
    {
33
        $this->bridgePath = $bridgePath ?? __DIR__ . '/../bin/bridge.js';
21✔
34
        $this->nodePath = $nodePath ?? $this->findNode();
21✔
35
        $this->checkEnvironment();
21✔
36
    }
37

38
    public function setOptions(array $options): static
39
    {
40
        $this->options = $options;
2✔
41

42
        return $this;
2✔
43
    }
44

45
    public function getOptions(): array
46
    {
47
        return $this->options;
1✔
48
    }
49

50
    public function compileString(string $source, array $options = []): string
51
    {
52
        if (trim($source) === '') {
8✔
53
            return '';
1✔
54
        }
55

56
        $options = array_merge($this->options, $options);
7✔
57

58
        return $this->compileSource($source, $options);
7✔
59
    }
60

61
    public function compileFile(string $filePath, array $options = []): string
62
    {
63
        if (! file_exists($filePath)) {
6✔
64
            throw new Exception("File not found: $filePath");
1✔
65
        }
66

67
        $content = $this->readFile($filePath);
5✔
68
        if ($content === false) {
5✔
69
            throw new Exception("Unable to read file: $filePath");
1✔
70
        }
71

72
        if (trim($content) === '') {
4✔
73
            return '';
1✔
74
        }
75

76
        $options = array_merge($this->options, $options);
3✔
77

78
        if (! isset($options['url'])) {
3✔
79
            $options['url'] = 'file://' . realpath($filePath);
3✔
80
        }
81

82
        return $this->compileSource($content, $options);
3✔
83
    }
84

85
    public function compileFileAndSave(string $inputPath, string $outputPath, array $options = []): bool
86
    {
87
        if (! file_exists($inputPath)) {
3✔
88
            throw new Exception("Source file not found: $inputPath");
1✔
89
        }
90

91
        $inputMtime = filemtime($inputPath);
2✔
92
        $outputMtime = file_exists($outputPath) ? filemtime($outputPath) : 0;
2✔
93

94
        if ($inputMtime > $outputMtime) {
2✔
95
            if (! empty($options['sourceMap']) && empty($options['sourceMapPath'])) {
2✔
96
                $options['sourceMapPath'] = $outputPath;
1✔
97
            }
98

99
            $css = $this->compileFile($inputPath, $options);
2✔
100
            file_put_contents($outputPath, $css);
2✔
101

102
            return true;
2✔
103
        }
104

105
        return false;
1✔
106
    }
107

108
    protected function compileSource(string $source, array $options): string
109
    {
110
        $payload = [
10✔
111
            'source' => $source,
10✔
112
            'options' => $options,
10✔
113
            'url' => $options['url'] ?? null,
10✔
114
        ];
10✔
115

116
        return $this->runCompile($payload);
10✔
117
    }
118

119
    protected function runCompile(array $payload): string
120
    {
121
        $cmd = [$this->nodePath, $this->bridgePath, '--stdin'];
10✔
122

123
        $process = $this->createProcess($cmd);
10✔
124
        $process->setInput(json_encode($payload));
10✔
125
        $process->run();
10✔
126

127
        $out = trim($process->getOutput());
10✔
128
        if ($out === '') {
10✔
129
            $err = trim($process->getErrorOutput());
1✔
130
            throw new Exception('Sass process failed: ' . ($err ?: 'unknown error'));
1✔
131
        }
132

133
        $data = json_decode($out, true);
9✔
134
        if (! is_array($data)) {
9✔
135
            throw new Exception('Invalid response from sass bridge');
1✔
136
        }
137

138
        if (! empty($data['error'])) {
8✔
139
            throw new Exception('Sass parsing error: ' . $data['error']);
1✔
140
        }
141

142
        $css = $data['css'] ?? '';
7✔
143

144
        if (! empty($data['sourceMap'])) {
7✔
145
            $css .= $this->processSourceMap($data['sourceMap'], $payload['options']);
2✔
146
        }
147

148
        return $css;
7✔
149
    }
150

151
    protected function processSourceMap(array $sourceMap, array $options): string
152
    {
153
        if (! empty($options['sourceMapPath'])) {
2✔
154
            $mapFile = (string) $options['sourceMapPath'];
1✔
155

156
            $isUrl = filter_var($mapFile, FILTER_VALIDATE_URL) !== false;
1✔
157
            if ($isUrl) {
1✔
158
                $sourceMappingUrl = $mapFile;
×
159
            } else {
160
                if (is_dir($mapFile)) {
1✔
161
                    $sourceFilename = $this->getSourceFilenameFromUrl($options['url'] ?? '');
×
162
                    $mapFile .= DIRECTORY_SEPARATOR . $sourceFilename . '.map';
×
163
                } elseif (strtolower(substr($mapFile, -4)) !== '.map') {
1✔
164
                    $mapFile .= '.map';
1✔
165
                }
166

167
                file_put_contents($mapFile, json_encode($sourceMap));
1✔
168
                $sourceMappingUrl = basename($mapFile);
1✔
169
            }
170

171
            return "\n/*# sourceMappingURL=" . $sourceMappingUrl . " */";
1✔
172
        } else {
173
            $mapData = json_encode($sourceMap);
1✔
174
            $encodedMap = base64_encode($mapData);
1✔
175
            return "\n/*# sourceMappingURL=data:application/json;base64," . $encodedMap . " */";
1✔
176
        }
177
    }
178

179
    protected function getSourceFilenameFromUrl(string $url): string
180
    {
181
        if ($url === '') {
×
182
            return 'style';
×
183
        }
184

185
        $parsed = parse_url($url);
×
186
        $path = $parsed['path'] ?? '';
×
187
        $basename = basename($path);
×
188
        $info = pathinfo($basename);
×
189

190
        return $info['filename'] ?: 'style';
×
191
    }
192

193
    protected function readFile(string $path): string|false
194
    {
195
        return file_get_contents($path);
4✔
196
    }
197

198
    protected function createProcess(array $command): Process
199
    {
200
        return new Process($command);
21✔
201
    }
202

203
    protected function findNode(): string
204
    {
205
        $candidates = ['node'];
21✔
206
        if ($this->isWindows()) {
21✔
207
            $candidates[] = 'C:\\Program Files\\nodejs\\node.exe';
1✔
208
            $candidates[] = 'C:\\Program Files (x86)\\nodejs\\node.exe';
1✔
209
        } else {
210
            $candidates[] = '/usr/local/bin/node';
21✔
211
            $candidates[] = '/usr/bin/node';
21✔
212
            $candidates[] = '/opt/homebrew/bin/node';
21✔
213
        }
214

215
        foreach ($candidates as $node) {
21✔
216
            $process = $this->createProcess([$node, '--version']);
21✔
217
            $process->run();
21✔
218
            if ($process->isSuccessful()) {
21✔
219
                return $node;
21✔
220
            }
221
        }
222

223
        throw new Exception(
1✔
224
            "Node.js not found. Please install Node.js >= 18 and make sure it's in PATH, " .
1✔
225
            "or pass its full path to your Compiler constructor."
1✔
226
        );
1✔
227
    }
228

229
    protected function isWindows(): bool
230
    {
231
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
21✔
232
    }
233

234
    protected function checkEnvironment(): void
235
    {
236
        if (! file_exists($this->bridgePath)) {
21✔
237
            throw new Exception("bridge.js not found at $this->bridgePath");
1✔
238
        }
239

240
        $nodeModules = $this->getPackageRoot() . '/node_modules/sass-embedded';
21✔
241
        if (! is_dir($nodeModules)) {
21✔
242
            throw new Exception("sass-embedded not found. Run `npm install` in {$this->getPackageRoot()}.");
1✔
243
        }
244
    }
245

246
    protected function getPackageRoot(): string
247
    {
248
        return realpath(__DIR__ . '/../');
21✔
249
    }
250
}
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