Skip to content

Commit

Permalink
Add support for synchronous compilation functions
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Dec 15, 2021
1 parent 495b8ea commit 6738925
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 511 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 1.0.0-beta.6

* Expose (as yet incomplete) `compile()`, `compileString()`, `compileAsync()`,
and `compileStringAsync()` functions.

* Include the official TypeScript definition files.

## 1.0.0-beta.5

- Function and Values API
Expand Down
7 changes: 6 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export {sassNull} from './src/value/null';
export {SassNumber} from './src/value/number';
export {SassString} from './src/value/string';

export {compileAsync, compileStringAsync} from './src/compile';
export {
compile,
compileString,
compileAsync,
compileStringAsync,
} from './src/compile';
export {render} from './src/node-sass/render';

export const info = `sass-embedded\t${pkg.version}`;
175 changes: 142 additions & 33 deletions lib/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,53 @@
// https://opensource.org/licenses/MIT.

import * as p from 'path';
import {Observable} from 'rxjs';

import {EmbeddedCompiler} from './embedded-compiler/compiler';
import {AsyncEmbeddedCompiler} from './embedded-compiler/async-compiler';
import {SyncEmbeddedCompiler} from './embedded-compiler/sync-compiler';
import {PacketTransformer} from './embedded-compiler/packet-transformer';
import {MessageTransformer} from './embedded-protocol/message-transformer';
import {Dispatcher} from './embedded-protocol/dispatcher';
import {Dispatcher, DispatcherHandlers} from './embedded-protocol/dispatcher';
import {deprotifyException} from './embedded-protocol/utils';
import * as proto from './vendor/embedded-protocol/embedded_sass_pb';
import {CompileResult, Options, StringOptions} from './vendor/sass';

export function compile(
path: string,
options?: Options<'sync'>
): CompileResult {
// TODO(awjin): Create logger, importer, function registries.
return compileRequestSync(newCompilePathRequest(path, options));
}

export function compileString(
source: string,
options?: Options<'sync'>
): CompileResult {
// TODO(awjin): Create logger, importer, function registries.
return compileRequestSync(newCompileStringRequest(source, options));
}

export function compileAsync(
path: string,
options?: Options<'async'>
): Promise<CompileResult> {
// TODO(awjin): Create logger, importer, function registries.
return compileRequest(newCompilePathRequest(path, options));
return compileRequestAsync(newCompilePathRequest(path, options));
}

export function compileStringAsync(
source: string,
options?: StringOptions<'async'>
): Promise<CompileResult> {
// TODO(awjin): Create logger, importer, function registries.
return compileRequest(newCompileStringRequest(source, options));
return compileRequestAsync(newCompileStringRequest(source, options));
}

// Creates a request for compiling a file.
function newCompilePathRequest(
path: string,
options?: Options<'async'>
options?: Options<'sync' | 'async'>
): proto.InboundMessage.CompileRequest {
// TODO(awjin): Populate request with importer/function IDs.

Expand All @@ -43,7 +61,7 @@ function newCompilePathRequest(
// Creates a request for compiling a string.
function newCompileStringRequest(
source: string,
options?: StringOptions<'async'>
options?: StringOptions<'sync' | 'async'>
): proto.InboundMessage.CompileRequest {
// TODO(awjin): Populate request with importer/function IDs.

Expand Down Expand Up @@ -74,7 +92,7 @@ function newCompileStringRequest(
// Creates a compilation request for the given `options` without adding any
// input-specific options.
function newCompileRequest(
options?: Options<'async'>
options?: Options<'sync' | 'async'>
): proto.InboundMessage.CompileRequest {
const request = new proto.InboundMessage.CompileRequest();
request.setSourceMap(!!options?.sourceMap);
Expand All @@ -91,27 +109,71 @@ function newCompileRequest(
// Spins up a compiler, then sends it a compile request. Returns a promise that
// resolves with the CompileResult. Throws if there were any protocol or
// compilation errors. Shuts down the compiler after compilation.
async function compileRequest(
async function compileRequestAsync(
request: proto.InboundMessage.CompileRequest
): Promise<CompileResult> {
const embeddedCompiler = new EmbeddedCompiler();
const embeddedCompiler = new AsyncEmbeddedCompiler();

try {
const packetTransformer = new PacketTransformer(
// TODO(awjin): Pass import and function registries' handler functions to
// dispatcher.
const dispatcher = createDispatcher<'sync'>(
embeddedCompiler.stdout$,
buffer => embeddedCompiler.writeStdin(buffer)
buffer => {
embeddedCompiler.writeStdin(buffer);
},
{
handleImportRequest: () => {
throw Error('Custom importers not yet implemented.');
},
handleFileImportRequest: () => {
throw Error('Custom file importers not yet implemented.');
},
handleCanonicalizeRequest: () => {
throw Error('Canonicalize not yet implemented.');
},
handleFunctionCallRequest: () => {
throw Error('Custom functions not yet implemented.');
},
}
);

const messageTransformer = new MessageTransformer(
packetTransformer.outboundProtobufs$,
packet => packetTransformer.writeInboundProtobuf(packet)
// TODO(awjin): Subscribe logger to dispatcher's log events.

return handleCompileResponse(
await new Promise<proto.OutboundMessage.CompileResponse>(
(resolve, reject) =>
dispatcher.sendCompileRequest(request, (err, response) => {
if (err) {
reject(err);
} else {
resolve(response!);
}
})
)
);
} finally {
embeddedCompiler.close();
await embeddedCompiler.exit$;
}
}

// Spins up a compiler, then sends it a compile request. Returns a promise that
// resolves with the CompileResult. Throws if there were any protocol or
// compilation errors. Shuts down the compiler after compilation.
function compileRequestSync(
request: proto.InboundMessage.CompileRequest
): CompileResult {
const embeddedCompiler = new SyncEmbeddedCompiler();

try {
// TODO(awjin): Pass import and function registries' handler functions to
// dispatcher.
const dispatcher = new Dispatcher(
messageTransformer.outboundMessages$,
message => messageTransformer.writeInboundMessage(message),
const dispatcher = createDispatcher<'sync'>(
embeddedCompiler.stdout$,
buffer => {
embeddedCompiler.writeStdin(buffer);
},
{
handleImportRequest: () => {
throw Error('Custom importers not yet implemented.');
Expand All @@ -130,26 +192,73 @@ async function compileRequest(

// TODO(awjin): Subscribe logger to dispatcher's log events.

const response = await dispatcher.sendCompileRequest(request);
let error: unknown;
let response: proto.OutboundMessage.CompileResponse | undefined;
dispatcher.sendCompileRequest(request, (error_, response_) => {
if (error_) {
error = error_;
} else {
response = response_;
}
});

if (response.getSuccess()) {
const success = response.getSuccess()!;
const sourceMap = success.getSourceMap();
if (request.getSourceMap() && sourceMap === undefined) {
throw Error('Compiler did not provide sourceMap.');
for (;;) {
if (!embeddedCompiler.yield()) {
throw new Error('Embedded compiler exited unexpectedly.');
}
return {
css: success.getCss(),
loadedUrls: [], // TODO(nex3): Fill this out
sourceMap: sourceMap ? JSON.parse(sourceMap) : undefined,
};
} else if (response.getFailure()) {
throw deprotifyException(response.getFailure()!);
} else {
throw Error('Compiler sent empty CompileResponse.');

if (error) throw error;
if (response) return handleCompileResponse(response);
}
} finally {
embeddedCompiler.close();
await embeddedCompiler.exit$;
embeddedCompiler.yieldUntilExit();
}
}

/**
* Creates a dispatcher that dispatches messages from the given `stdout` stream.
*/
function createDispatcher<sync extends 'sync' | 'async'>(
stdout: Observable<Buffer>,
writeStdin: (buffer: Buffer) => void,
handlers: DispatcherHandlers<sync>
): Dispatcher<sync> {
const packetTransformer = new PacketTransformer(stdout, writeStdin);

const messageTransformer = new MessageTransformer(
packetTransformer.outboundProtobufs$,
packet => packetTransformer.writeInboundProtobuf(packet)
);

return new Dispatcher<sync>(
messageTransformer.outboundMessages$,
message => messageTransformer.writeInboundMessage(message),
handlers
);
}

/**
* Converts a `CompileResponse` into a `CompileResult`.
*
* Throws a `SassException` if the compilation failed.
*/
function handleCompileResponse(
response: proto.OutboundMessage.CompileResponse
): CompileResult {
if (response.getSuccess()) {
const success = response.getSuccess()!;
const result: CompileResult = {
css: success.getCss(),
loadedUrls: [], // TODO(nex3): Fill this out
};

const sourceMap = success.getSourceMap();
if (sourceMap) result.sourceMap = JSON.parse(sourceMap);
return result;
} else if (response.getFailure()) {
throw deprotifyException(response.getFailure()!);
} else {
throw Error('Compiler sent empty CompileResponse.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,19 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import * as fs from 'fs';
import {spawn} from 'child_process';
import {resolve} from 'path';
import {Observable} from 'rxjs';
import {takeUntil} from 'rxjs/operators';

import {compilerPath} from './compiler-path';

/**
* Invokes the Embedded Sass Compiler as a Node child process, exposing its
* stdio as Observables.
* An asynchronous wrapper for the embedded Sass compiler that exposes its stdio
* streams as Observables.
*/
export class EmbeddedCompiler {
private readonly process = (() => {
for (const path of ['../vendor', '../../../../lib/src/vendor']) {
const executable = resolve(
__dirname,
path,
`dart-sass-embedded/dart-sass-embedded${
process.platform === 'win32' ? '.bat' : ''
}`
);

if (fs.existsSync(executable)) {
return spawn(executable, {windowsHide: true});
}
}

throw new Error(
"Embedded Dart Sass couldn't find the embedded compiler executable."
);
})();
export class AsyncEmbeddedCompiler {
/** The underlying process that's being wrapped. */
private readonly process = spawn(compilerPath, {windowsHide: true});

/** The child process's exit event. */
readonly exit$ = new Promise<number | null>(resolve => {
Expand Down
25 changes: 25 additions & 0 deletions lib/src/embedded-compiler/compiler-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2021 Google LLC. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import * as fs from 'fs';
import * as p from 'path';

/** The path to the embedded compiler executable. */
export const compilerPath = (() => {
for (const path of ['../vendor', '../../../../lib/src/vendor']) {
const executable = p.resolve(
__dirname,
path,
`dart-sass-embedded/dart-sass-embedded${
process.platform === 'win32' ? '.bat' : ''
}`
);

if (fs.existsSync(executable)) return executable;
}

throw new Error(
"Embedded Dart Sass couldn't find the embedded compiler executable."
);
})();
Loading

0 comments on commit 6738925

Please sign in to comment.