Skip to content

Commit fd7da0d

Browse files
committed
Implement persistent mode
1 parent 3a68da9 commit fd7da0d

File tree

4 files changed

+228
-64
lines changed

4 files changed

+228
-64
lines changed

bin/bridge.js

Lines changed: 117 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
2-
import { compileString } from "sass-embedded";
2+
import {compileString} from "sass-embedded";
3+
import readline from "readline";
34

45
/**
56
* Optimized bridge for sass compilation with support for:
@@ -9,14 +10,13 @@ import { compileString } from "sass-embedded";
910
* - Memory-efficient processing
1011
*/
1112

12-
// Check for persistent mode
13-
const isPersistent = process.argv.includes('--persistent');
13+
/** @type {string[]} */
14+
const argv = process.argv;
15+
const isPersistent = argv.includes('--persistent');
1416

1517
if (isPersistent) {
16-
// Persistent mode: process multiple requests
1718
processPersistentMode();
1819
} else {
19-
// Single request mode (legacy)
2020
processSingleRequest();
2121
}
2222

@@ -43,80 +43,139 @@ function processSingleRequest() {
4343
process.stdin.on("end", () => {
4444
try {
4545
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 = {};
46+
const response = compilePayload(payload);
5047

51-
if (url) compileOpts.url = url;
52-
53-
if (options.syntax === 'sass' || options.syntax === 'indented') {
54-
compileOpts.syntax = 'indented';
48+
process.stdout.write(JSON.stringify(response));
49+
} catch (err) {
50+
process.stdout.write(JSON.stringify({ error: String(err?.message || err) }));
5551
}
52+
});
53+
}
5654

57-
if (options.minimize || ('compressed' in options && options.compressed) || options.style === 'compressed') {
58-
compileOpts.style = 'compressed';
59-
}
55+
/**
56+
* Generator function for processing compilation results in chunks
57+
* Useful for memory-efficient handling of large CSS outputs
58+
*/
59+
function* cssChunkGenerator(css, chunkSize = 64 * 1024) {
60+
for (let i = 0; i < css.length; i += chunkSize) {
61+
yield css.slice(i, i + chunkSize);
62+
}
63+
}
6064

61-
if (options.sourceMap || options.sourceMapPath) {
62-
compileOpts.sourceMap = options.sourceMapPath || options.sourceMap;
65+
/**
66+
* Generator function for processing sourceMap in chunks
67+
* Useful for large sourceMap data
68+
*/
69+
function* sourceMapChunkGenerator(sourceMap, chunkSize = 64 * 1024) {
70+
const mapString = JSON.stringify(sourceMap);
6371

64-
if (options.includeSources) {
65-
compileOpts.sourceMapIncludeSources = options.includeSources;
72+
for (let i = 0; i < mapString.length; i += chunkSize) {
73+
yield mapString.slice(i, i + chunkSize);
74+
}
75+
}
76+
77+
function processPersistentMode() {
78+
const rl = readline.createInterface({
79+
input: process.stdin,
80+
output: process.stdout,
81+
terminal: false
82+
});
83+
84+
rl.on('line', (line) => {
85+
if (line.trim()) {
86+
try {
87+
const request = JSON.parse(line);
88+
89+
if (request.exit === true) {
90+
rl.close();
91+
process.exit(0);
92+
}
93+
94+
const response = compilePayload(request);
95+
96+
process.stdout.write(JSON.stringify(response) + '\n');
97+
} catch (err) {
98+
sendError(err.message);
6699
}
67100
}
101+
});
68102

69-
if (options.loadPaths) {
70-
compileOpts.loadPaths = options.loadPaths;
71-
}
103+
rl.on('close', () => {
104+
process.exit(0);
105+
});
106+
}
72107

73-
if (options.quietDeps) {
74-
compileOpts.quietDeps = options.quietDeps;
75-
}
108+
/**
109+
* Compiles a single payload and returns the response object
110+
*/
111+
function compilePayload(payload) {
112+
const source = String(payload.source || "");
113+
const options = payload.options || {};
114+
const url = payload.url ? new URL(String(payload.url)) : undefined;
115+
const compileOpts = {};
76116

77-
if (options.silenceDeprecations) {
78-
compileOpts.silenceDeprecations = options.silenceDeprecations;
79-
}
117+
if (url) compileOpts.url = url;
118+
119+
if (options.syntax === 'sass' || options.syntax === 'indented') {
120+
compileOpts.syntax = 'indented';
121+
}
122+
123+
if (options.minimize || ('compressed' in options && options.compressed) || options.style === 'compressed') {
124+
compileOpts.style = 'compressed';
125+
}
126+
127+
const sourceMapPath = 'sourceMapPath' in options && options.sourceMapPath ? options.sourceMapPath : false;
80128

81-
if (options.verbose) {
82-
compileOpts.verbose = options.verbose;
129+
if (options.sourceMap || sourceMapPath) {
130+
compileOpts.sourceMap = sourceMapPath || options.sourceMap;
131+
132+
if ('includeSources' in options && options.includeSources) {
133+
compileOpts.sourceMapIncludeSources = options.includeSources;
83134
}
135+
}
84136

85-
const result = compileString(source, compileOpts);
137+
if (options.loadPaths) {
138+
compileOpts.loadPaths = options.loadPaths;
139+
}
86140

87-
const response = {
88-
css: result.css,
89-
...(result.sourceMap && { sourceMap: result.sourceMap }),
90-
};
141+
if (options.quietDeps) {
142+
compileOpts.quietDeps = options.quietDeps;
143+
}
91144

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));
145+
if (options.silenceDeprecations) {
146+
compileOpts.silenceDeprecations = options.silenceDeprecations;
147+
}
95148

96-
response.chunks = cssChunks;
97-
response.isStreamed = true;
149+
if (options.verbose) {
150+
compileOpts.verbose = options.verbose;
151+
}
98152

99-
delete response.css;
100-
}
153+
const result = compileString(source, compileOpts);
101154

102-
process.stdout.write(JSON.stringify(response));
103-
} catch (err) {
104-
process.stdout.write(JSON.stringify({ error: String(err?.message || err) }));
155+
const response = {
156+
css: result.css,
157+
...(result.sourceMap && { sourceMap: result.sourceMap }),
158+
};
159+
160+
// Check if streaming mode is requested for large results
161+
if ('streamResult' in options && options.streamResult && result.css.length > 1024 * 1024) {
162+
response.chunks = Array.from(cssChunkGenerator(result.css));
163+
response.isStreamed = true;
164+
165+
delete response.css;
105166
}
106-
});
107-
}
108167

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);
168+
// Handle large sourceMap
169+
if (response.sourceMap && JSON.stringify(response.sourceMap).length > 1024 * 1024) {
170+
response.sourceMapChunks = Array.from(sourceMapChunkGenerator(response.sourceMap));
171+
response.sourceMapIsStreamed = true;
172+
173+
delete response.sourceMap;
116174
}
175+
176+
return response;
117177
}
118178

119-
function processPersistentMode() {
120-
// TODO: Implement persistent mode for multiple requests
121-
process.exit(1);
179+
function sendError(message) {
180+
process.stdout.write(JSON.stringify({ error: message }) + '\n');
122181
}

src/Compiler.php

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@
2626
use function substr;
2727
use function trim;
2828

29-
class Compiler implements CompilerInterface
29+
class Compiler implements CompilerInterface, PersistentCompilerInterface
3030
{
3131
protected array $options = [];
3232

3333
protected static ?Process $cachedProcess = null;
3434

3535
protected static ?array $cachedCommand = null;
3636

37+
protected bool $persistentMode = false;
38+
39+
protected ?Process $persistentProcess = null;
40+
3741
public function __construct(protected ?string $bridgePath = null, protected ?string $nodePath = null)
3842
{
3943
$this->bridgePath = $bridgePath ?? __DIR__ . '/../bin/bridge.js';
@@ -111,11 +115,6 @@ public function compileFileAndSave(string $inputPath, string $outputPath, array
111115
return false;
112116
}
113117

114-
protected function getFileMtime(string $path): int
115-
{
116-
return filemtime($path);
117-
}
118-
119118
public function compileStringAsGenerator(string $source, array $options = []): Generator
120119
{
121120
if (trim($source) === '') {
@@ -150,6 +149,41 @@ public function compileStringAsGenerator(string $source, array $options = []): G
150149
}
151150
}
152151

152+
public function compileInPersistentMode(string $source, array $options = []): string
153+
{
154+
if (trim($source) === '') {
155+
return '';
156+
}
157+
158+
$options = array_merge($this->options, $options);
159+
160+
return $this->compileSourceWithPersistent($source, $options);
161+
}
162+
163+
public function enablePersistentMode(): static
164+
{
165+
$this->persistentMode = true;
166+
167+
return $this;
168+
}
169+
170+
public function exitPersistentMode(): void
171+
{
172+
if ($this->persistentProcess !== null && $this->persistentProcess->isRunning()) {
173+
$this->persistentProcess->setInput(json_encode(['exit' => true]) . "\n");
174+
$this->persistentProcess->run();
175+
$this->persistentProcess->stop();
176+
}
177+
178+
$this->persistentProcess = null;
179+
$this->persistentMode = false;
180+
}
181+
182+
protected function getFileMtime(string $path): int
183+
{
184+
return filemtime($path);
185+
}
186+
153187
protected function compileSource(string $source, array $options): string
154188
{
155189
$payload = [
@@ -168,6 +202,48 @@ protected function compileSource(string $source, array $options): string
168202
return $css;
169203
}
170204

205+
protected function compileSourceWithPersistent(string $source, array $options): string
206+
{
207+
$payload = [
208+
'source' => $source,
209+
'options' => $options,
210+
'url' => $options['url'] ?? null,
211+
];
212+
213+
$data = $this->runCompilePersistent($payload);
214+
$css = $data['css'] ?? '';
215+
216+
if (! empty($data['sourceMap'])) {
217+
$css .= $this->processSourceMap($data['sourceMap'], $options);
218+
}
219+
220+
return $css;
221+
}
222+
223+
protected function runCompilePersistent(array $payload): array
224+
{
225+
$process = $this->getOrCreatePersistentProcess();
226+
$process->setInput(json_encode($payload) . "\n");
227+
$process->run();
228+
229+
$out = trim($process->getOutput());
230+
if ($out === '') {
231+
$err = trim($process->getErrorOutput());
232+
throw new Exception('Sass persistent process failed: ' . ($err ?: 'unknown error'));
233+
}
234+
235+
$data = json_decode($out, true);
236+
if (! is_array($data)) {
237+
throw new Exception('Invalid response from sass persistent bridge');
238+
}
239+
240+
if (! empty($data['error'])) {
241+
throw new Exception('Sass parsing error: ' . $data['error']);
242+
}
243+
244+
return $data;
245+
}
246+
171247
protected function runCompile(array $payload): array
172248
{
173249
$cmd = [$this->nodePath, $this->bridgePath, '--stdin'];
@@ -266,6 +342,19 @@ protected function createProcess(array $command): Process
266342
return new Process($command);
267343
}
268344

345+
protected function getOrCreatePersistentProcess(): Process
346+
{
347+
if ($this->persistentProcess !== null && $this->persistentProcess->isRunning()) {
348+
return $this->persistentProcess;
349+
}
350+
351+
$cmd = [$this->nodePath, $this->bridgePath, '--persistent'];
352+
$this->persistentProcess = $this->createProcess($cmd);
353+
$this->persistentProcess->start();
354+
355+
return $this->persistentProcess;
356+
}
357+
269358
protected function findNode(): string
270359
{
271360
$candidates = ['node'];

src/CompilerInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Bugo\Sass;
44

5+
use Generator;
6+
57
interface CompilerInterface
68
{
79
public function setOptions(array $options): static;
@@ -13,4 +15,6 @@ public function compileString(string $source, array $options = []): string;
1315
public function compileFile(string $filePath, array $options = []): string;
1416

1517
public function compileFileAndSave(string $inputPath, string $outputPath, array $options = []): bool;
18+
19+
public function compileStringAsGenerator(string $source, array $options = []): Generator;
1620
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bugo\Sass;
4+
5+
interface PersistentCompilerInterface
6+
{
7+
public function enablePersistentMode(): static;
8+
9+
public function compileInPersistentMode(string $source, array $options = []): string;
10+
11+
public function exitPersistentMode(): void;
12+
}

0 commit comments

Comments
 (0)