Skip to content

Commit 74559f0

Browse files
committed
Upload files
1 parent 22a94f3 commit 74559f0

File tree

14 files changed

+733
-0
lines changed

14 files changed

+733
-0
lines changed

.editorconfig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
indent_style = space
7+
indent_size = space
8+
tab_width = 4
9+
trim_trailing_whitespace = true
10+
charset = utf-8
11+
12+
[*.{js,css,json}]
13+
indent_style = space
14+
indent_size = 2

.github/workflows/coverage.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: PHP Tests
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v2
11+
12+
- name: Setup PHP
13+
uses: shivammathur/setup-php@v2
14+
with:
15+
php-version: '8.2'
16+
17+
- name: Install dependencies
18+
run: composer install
19+
20+
- name: Run tests
21+
run: vendor/bin/pest --colors=always --coverage-clover build/logs/clover.xml
22+
23+
- name: Upload coverage results to Coveralls
24+
env:
25+
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26+
run: |
27+
composer global require php-coveralls/php-coveralls
28+
php-coveralls --coverage_clover=build/logs/clover.xml -v

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.idea/
2+
.phpunit.result.cache
3+
.phpunit.cache/
4+
.vscode/
5+
composer.lock
6+
package-lock.json
7+
coverage.xml
8+
coverage/
9+
vendor/
10+
node_modules/

bin/bridge.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env node
2+
import { compileString } from "sass-embedded";
3+
4+
let stdin = "";
5+
process.stdin.setEncoding("utf8");
6+
process.stdin.on("data", chunk => stdin += chunk);
7+
process.stdin.on("end", () => {
8+
try {
9+
const payload = JSON.parse(stdin || "{}");
10+
const source = String(payload.source || "");
11+
const options = payload.options || {};
12+
const url = payload.url ? new URL(String(payload.url)) : undefined;
13+
const compileOpts = {};
14+
15+
if (url) compileOpts.url = url;
16+
17+
if (options.syntax === 'sass' || options.syntax === 'indented') {
18+
compileOpts.syntax = 'indented';
19+
}
20+
21+
if (options.minimize || ('compressed' in options && options.compressed) || options.style === 'compressed') {
22+
compileOpts.style = 'compressed';
23+
}
24+
25+
if (options.sourceMap) {
26+
compileOpts.sourceMap = options.sourceMap;
27+
28+
if (options.sourceMapIncludeSources) {
29+
compileOpts.sourceMapIncludeSources = options.sourceMapIncludeSources;
30+
}
31+
}
32+
33+
const result = compileString(source, compileOpts);
34+
35+
process.stdout.write(JSON.stringify({
36+
css: result.css,
37+
sourceMap: result.sourceMap || null,
38+
}));
39+
} catch (err) {
40+
process.stdout.write(JSON.stringify({ error: String(err?.message || err) }));
41+
}
42+
});

composer.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "bugo/sass-embedded-php",
3+
"description": "PHP wrapper for sass-embedded (npm) with dependency autoloading",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Bugo",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"require": {
13+
"php": ">=8.2",
14+
"symfony/process": "^7.3"
15+
},
16+
"require-dev": {
17+
"mockery/mockery": "^2.0.x-dev",
18+
"pestphp/pest": "^3.8",
19+
"rector/rector": "^2.0"
20+
},
21+
"autoload": {
22+
"psr-4": {
23+
"Bugo\\Sass\\": "src/"
24+
}
25+
},
26+
"scripts": {
27+
"check": "vendor/bin/rector process --dry-run --clear-cache",
28+
"tests": "vendor/bin/pest --colors=always",
29+
"tests-coverage": "vendor/bin/pest --colors=always --coverage --min=90",
30+
"tests-coverage-clover": "vendor/bin/pest --colors=always --min=90 --coverage-clover coverage.xml",
31+
"tests-coverage-html": "vendor/bin/pest --colors=always --min=90 --coverage-html coverage",
32+
"post-install-cmd": [
33+
"Bugo\\Sass\\Installer::postInstall"
34+
],
35+
"post-update-cmd": [
36+
"Bugo\\Sass\\Installer::postInstall"
37+
],
38+
"post-autoload-dump": [
39+
"Bugo\\Sass\\Installer::postInstall"
40+
]
41+
},
42+
"bin": [
43+
"bin/bridge.js"
44+
],
45+
"config": {
46+
"optimize-autoloader": true,
47+
"preferred-install": "dist",
48+
"sort-packages": true,
49+
"allow-plugins": {
50+
"pestphp/pest-plugin": true
51+
}
52+
}
53+
}

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "sass-embedded-php",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"dependencies": {
7+
"sass-embedded": "^1.92.0"
8+
}
9+
}

phpunit.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
>
7+
<testsuites>
8+
<testsuite name="Test Suite">
9+
<directory>./tests</directory>
10+
</testsuite>
11+
</testsuites>
12+
<source>
13+
<include>
14+
<directory>src</directory>
15+
</include>
16+
<exclude>
17+
<file>src/Installer.php</file>
18+
</exclude>
19+
</source>
20+
</phpunit>

rector.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php declare(strict_types=1);
2+
3+
use Rector\Config\RectorConfig;
4+
5+
return RectorConfig::configure()
6+
->withPaths([
7+
__DIR__ . '/src',
8+
])
9+
->withPhpSets()
10+
->withTypeCoverageLevel(10)
11+
->withDeadCodeLevel(10)
12+
->withCodeQualityLevel(10)
13+
->withCodingStyleLevel(9);

src/Compiler.php

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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 basename;
9+
use function file_exists;
10+
use function file_get_contents;
11+
use function file_put_contents;
12+
use function getcwd;
13+
use function is_array;
14+
use function is_dir;
15+
use function json_decode;
16+
use function json_encode;
17+
use function realpath;
18+
use function strtoupper;
19+
use function substr;
20+
use function sys_get_temp_dir;
21+
use function tempnam;
22+
use function trim;
23+
24+
class Compiler implements CompilerInterface
25+
{
26+
protected array $options = [];
27+
28+
public function __construct(protected ?string $bridgePath = null, protected ?string $nodePath = null)
29+
{
30+
$this->bridgePath = $bridgePath ?? __DIR__ . '/../bin/bridge.js';
31+
$this->nodePath = $nodePath ?? $this->findNode();
32+
$this->checkEnvironment();
33+
}
34+
35+
public function setOptions(array $options): static
36+
{
37+
$this->options = $options;
38+
39+
return $this;
40+
}
41+
42+
public function getOptions(): array
43+
{
44+
return $this->options;
45+
}
46+
47+
public function compileString(string $source, array $options = []): string
48+
{
49+
if (trim($source) === '') {
50+
return '';
51+
}
52+
53+
$options = array_merge($this->options, $options);
54+
55+
if (! isset($merged['url'])) {
56+
$options['url'] = 'file://' . getcwd() . '/string.scss';
57+
}
58+
59+
return $this->compileSource($source, $options);
60+
}
61+
62+
public function compileFile(string $filePath, array $options = []): string
63+
{
64+
if (! file_exists($filePath)) {
65+
throw new Exception("File not found: $filePath");
66+
}
67+
68+
$content = $this->readFile($filePath);
69+
if ($content === false) {
70+
throw new Exception("Unable to read file: $filePath");
71+
}
72+
73+
if (trim($content) === '') {
74+
return '';
75+
}
76+
77+
$options = array_merge($this->options, $options);
78+
79+
if (! isset($options['url'])) {
80+
$options['url'] = 'file://' . realpath($filePath);
81+
}
82+
83+
return $this->compileSource($content, $options);
84+
}
85+
86+
protected function compileSource(string $source, array $options): string
87+
{
88+
$payload = [
89+
'source' => $source,
90+
'options' => $options,
91+
'url' => $options['url'],
92+
];
93+
94+
return $this->runCompile($payload);
95+
}
96+
97+
protected function runCompile(array $payload): string
98+
{
99+
$cmd = [$this->nodePath, $this->bridgePath, '--stdin'];
100+
101+
$process = $this->createProcess($cmd);
102+
$process->setInput(json_encode($payload));
103+
$process->run();
104+
105+
$out = trim($process->getOutput());
106+
if ($out === '') {
107+
$err = trim($process->getErrorOutput());
108+
throw new Exception('Sass process failed: ' . ($err ?: 'unknown error'));
109+
}
110+
111+
$data = json_decode($out, true);
112+
if (! is_array($data)) {
113+
throw new Exception('Invalid response from sass bridge');
114+
}
115+
116+
if (! empty($data['error'])) {
117+
throw new Exception('Sass parsing error: ' . $data['error']);
118+
}
119+
120+
$css = $data['css'] ?? '';
121+
122+
if (! empty($payload['options']['sourceMap']) && ! empty($data['sourceMap'])) {
123+
$mapFile = tempnam(sys_get_temp_dir(), 'sass_') . '.map';
124+
file_put_contents($mapFile, json_encode($data['sourceMap']));
125+
$css .= "\n/*# sourceMappingURL=" . basename($mapFile) . " */";
126+
}
127+
128+
return $css;
129+
}
130+
131+
protected function readFile(string $path): string|false
132+
{
133+
return file_get_contents($path);
134+
}
135+
136+
protected function createProcess(array $command): Process
137+
{
138+
return new Process($command);
139+
}
140+
141+
protected function findNode(): string
142+
{
143+
$candidates = ['node'];
144+
if ($this->isWindows()) {
145+
$candidates[] = 'C:\\Program Files\\nodejs\\node.exe';
146+
$candidates[] = 'C:\\Program Files (x86)\\nodejs\\node.exe';
147+
} else {
148+
$candidates[] = '/usr/local/bin/node';
149+
$candidates[] = '/usr/bin/node';
150+
$candidates[] = '/opt/homebrew/bin/node';
151+
}
152+
153+
foreach ($candidates as $node) {
154+
$process = $this->createProcess([$node, '--version']);
155+
$process->run();
156+
if ($process->isSuccessful()) {
157+
return $node;
158+
}
159+
}
160+
161+
throw new Exception(
162+
"Node.js not found. Please install Node.js >= 18 and make sure it's in PATH, " .
163+
"or pass its full path to your Compiler constructor."
164+
);
165+
}
166+
167+
protected function isWindows(): bool
168+
{
169+
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
170+
}
171+
172+
protected function checkEnvironment(): void
173+
{
174+
if (! file_exists($this->bridgePath)) {
175+
throw new Exception("bridge.js not found at $this->bridgePath");
176+
}
177+
178+
$nodeModules = $this->getPackageRoot() . '/node_modules/sass-embedded';
179+
if (! is_dir($nodeModules)) {
180+
throw new Exception("sass-embedded not found. Run `npm install` in {$this->getPackageRoot()}.");
181+
}
182+
}
183+
184+
protected function getPackageRoot(): string
185+
{
186+
return realpath(__DIR__ . '/../');
187+
}
188+
}

0 commit comments

Comments
 (0)