Skip to content

Commit 0fd324e

Browse files
committed
Refactor code
1 parent 3d40451 commit 0fd324e

File tree

3 files changed

+159
-28
lines changed

3 files changed

+159
-28
lines changed

bin/bridge.js

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,52 @@
11
#!/usr/bin/env node
22
import { compileString } from "sass-embedded";
33

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 = {};
4+
/**
5+
* Optimized bridge for sass compilation with support for:
6+
* - Large data streams
7+
* - Persistent mode for multiple requests
8+
* - Reduced JSON overhead
9+
* - Memory-efficient processing
10+
*/
11+
12+
// Check for persistent mode
13+
const isPersistent = process.argv.includes('--persistent');
14+
15+
if (isPersistent) {
16+
// Persistent mode: process multiple requests
17+
processPersistentMode();
18+
} else {
19+
// Single request mode (legacy)
20+
processSingleRequest();
21+
}
22+
23+
function processSingleRequest() {
24+
let buffer = [];
25+
let totalLength = 0;
26+
27+
process.stdin.setEncoding("utf8");
28+
29+
process.stdin.on("data", chunk => {
30+
buffer.push(chunk);
31+
totalLength += chunk.length;
32+
33+
// Prevent excessive memory usage for very large inputs
34+
if (totalLength > 50 * 1024 * 1024) { // 50MB limit
35+
process.stdout.write(JSON.stringify({
36+
error: "Input too large. Consider using streaming mode or splitting the input."
37+
}));
38+
39+
process.exit(1);
40+
}
41+
});
42+
43+
process.stdin.on("end", () => {
44+
try {
45+
const payload = JSON.parse(buffer.join('') || "{}");
46+
const source = String(payload.source || "");
47+
const options = payload.options || {};
48+
const url = payload.url ? new URL(String(payload.url)) : undefined;
49+
const compileOpts = {};
1450

1551
if (url) compileOpts.url = url;
1652

@@ -48,11 +84,39 @@ process.stdin.on("end", () => {
4884

4985
const result = compileString(source, compileOpts);
5086

51-
process.stdout.write(JSON.stringify({
87+
const response = {
5288
css: result.css,
53-
sourceMap: result.sourceMap || null,
54-
}));
89+
...(result.sourceMap && { sourceMap: result.sourceMap }),
90+
};
91+
92+
// Check if streaming mode is requested for large results
93+
if (options.streamResult && result.css.length > 1024 * 1024) {
94+
const cssChunks = Array.from(cssChunkGenerator(result.css));
95+
96+
response.chunks = cssChunks;
97+
response.isStreamed = true;
98+
99+
delete response.css;
100+
}
101+
102+
process.stdout.write(JSON.stringify(response));
55103
} catch (err) {
56104
process.stdout.write(JSON.stringify({ error: String(err?.message || err) }));
57105
}
58-
});
106+
});
107+
}
108+
109+
/**
110+
* Generator function for processing compilation results in chunks
111+
* Useful for memory-efficient handling of large CSS outputs
112+
*/
113+
function* cssChunkGenerator(css, chunkSize = 64 * 1024) {
114+
for (let i = 0; i < css.length; i += chunkSize) {
115+
yield css.slice(i, i + chunkSize);
116+
}
117+
}
118+
119+
function processPersistentMode() {
120+
// TODO: Implement persistent mode for multiple requests
121+
process.exit(1);
122+
}

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"composer/composer": "*",
2525
"mockery/mockery": "^2.0.x-dev",
2626
"pestphp/pest": "^3.8",
27-
"rector/rector": "^2.0"
27+
"rector/rector": "^2.0",
28+
"mikey179/vfsstream": "^1.6"
2829
},
2930
"autoload": {
3031
"psr-4": {

src/Compiler.php

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Bugo\Sass;
44

5+
use Generator;
56
use Symfony\Component\Process\Process;
67

78
use function array_merge;
@@ -17,6 +18,7 @@
1718
use function json_decode;
1819
use function json_encode;
1920
use function parse_url;
21+
use function preg_match;
2022
use function pathinfo;
2123
use function realpath;
2224
use function strtolower;
@@ -28,6 +30,10 @@ class Compiler implements CompilerInterface
2830
{
2931
protected array $options = [];
3032

33+
protected static ?Process $cachedProcess = null;
34+
35+
protected static ?array $cachedCommand = null;
36+
3137
public function __construct(protected ?string $bridgePath = null, protected ?string $nodePath = null)
3238
{
3339
$this->bridgePath = $bridgePath ?? __DIR__ . '/../bin/bridge.js';
@@ -88,8 +94,8 @@ public function compileFileAndSave(string $inputPath, string $outputPath, array
8894
throw new Exception("Source file not found: $inputPath");
8995
}
9096

91-
$inputMtime = filemtime($inputPath);
92-
$outputMtime = file_exists($outputPath) ? filemtime($outputPath) : 0;
97+
$inputMtime = $this->getFileMtime($inputPath);
98+
$outputMtime = file_exists($outputPath) ? $this->getFileMtime($outputPath) : 0;
9399

94100
if ($inputMtime > $outputMtime) {
95101
if (! empty($options['sourceMap']) && empty($options['sourceMapPath'])) {
@@ -105,6 +111,45 @@ public function compileFileAndSave(string $inputPath, string $outputPath, array
105111
return false;
106112
}
107113

114+
protected function getFileMtime(string $path): int
115+
{
116+
return filemtime($path);
117+
}
118+
119+
public function compileStringAsGenerator(string $source, array $options = []): Generator
120+
{
121+
if (trim($source) === '') {
122+
yield '';
123+
return;
124+
}
125+
126+
$options = array_merge($this->options, $options);
127+
128+
if (strlen($source) > 1024 * 1024) {
129+
$options['streamResult'] = true;
130+
}
131+
132+
$payload = [
133+
'source' => $source,
134+
'options' => $options,
135+
'url' => $options['url'] ?? null,
136+
];
137+
138+
$result = $this->runCompile($payload);
139+
140+
if (isset($result['isStreamed']) && $result['isStreamed']) {
141+
foreach ($result['chunks'] as $chunk) {
142+
yield $chunk;
143+
}
144+
} else {
145+
yield $result['css'] ?? '';
146+
}
147+
148+
if (! empty($result['sourceMap'])) {
149+
yield $this->processSourceMap($result['sourceMap'], $options);
150+
}
151+
}
152+
108153
protected function compileSource(string $source, array $options): string
109154
{
110155
$payload = [
@@ -113,14 +158,21 @@ protected function compileSource(string $source, array $options): string
113158
'url' => $options['url'] ?? null,
114159
];
115160

116-
return $this->runCompile($payload);
161+
$data = $this->runCompile($payload);
162+
$css = $data['css'] ?? '';
163+
164+
if (! empty($data['sourceMap'])) {
165+
$css .= $this->processSourceMap($data['sourceMap'], $options);
166+
}
167+
168+
return $css;
117169
}
118170

119-
protected function runCompile(array $payload): string
171+
protected function runCompile(array $payload): array
120172
{
121173
$cmd = [$this->nodePath, $this->bridgePath, '--stdin'];
122174

123-
$process = $this->createProcess($cmd);
175+
$process = $this->getOrCreateProcess($cmd);
124176
$process->setInput(json_encode($payload));
125177
$process->run();
126178

@@ -139,13 +191,7 @@ protected function runCompile(array $payload): string
139191
throw new Exception('Sass parsing error: ' . $data['error']);
140192
}
141193

142-
$css = $data['css'] ?? '';
143-
144-
if (! empty($data['sourceMap'])) {
145-
$css .= $this->processSourceMap($data['sourceMap'], $payload['options']);
146-
}
147-
148-
return $css;
194+
return $data;
149195
}
150196

151197
protected function processSourceMap(array $sourceMap, array $options): string
@@ -159,7 +205,7 @@ protected function processSourceMap(array $sourceMap, array $options): string
159205

160206
$mapFile = (string) $options['sourceMapPath'];
161207

162-
$isUrl = filter_var($mapFile, FILTER_VALIDATE_URL) !== false;
208+
$isUrl = filter_var($mapFile, FILTER_VALIDATE_URL) !== false && preg_match('/^https?:/', $mapFile);
163209
if ($isUrl) {
164210
$sourceMappingUrl = $mapFile;
165211
} else {
@@ -196,6 +242,25 @@ protected function readFile(string $path): string|false
196242
return file_get_contents($path);
197243
}
198244

245+
protected function getOrCreateProcess(array $command): Process
246+
{
247+
if (self::$cachedProcess !== null && self::$cachedCommand === $command) {
248+
if (self::$cachedProcess->isRunning()) {
249+
return self::$cachedProcess;
250+
}
251+
252+
self::$cachedProcess = null;
253+
self::$cachedCommand = null;
254+
}
255+
256+
$process = $this->createProcess($command);
257+
258+
self::$cachedProcess = $process;
259+
self::$cachedCommand = $command;
260+
261+
return $process;
262+
}
263+
199264
protected function createProcess(array $command): Process
200265
{
201266
return new Process($command);
@@ -216,6 +281,7 @@ protected function findNode(): string
216281
foreach ($candidates as $node) {
217282
$process = $this->createProcess([$node, '--version']);
218283
$process->run();
284+
219285
if ($process->isSuccessful()) {
220286
return $node;
221287
}

0 commit comments

Comments
 (0)