Skip to content

Commit

Permalink
Add support for Options.functions
Browse files Browse the repository at this point in the history
This is still missing support for some value types, but the function
definition infrastructure is in place.

See sass/sass#2510
Closes #72
  • Loading branch information
nex3 committed Dec 21, 2021
1 parent d316ada commit 7d45259
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 22 deletions.
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
* Expose the `Exception` class and ensure that syntax errors match the official
JS API.

* Add support for the `style`, `alertColor`, `alertAscii`, `quietDeps`, and
`verbose` options in `compile()`, `compileString()`, `compileAsync()`, and
`compileStringAsync()`.
* Add support for the `style`, `alertColor`, `alertAscii`, `quietDeps`,
`verbose`, and `functions` options in `compile()`, `compileString()`,
`compileAsync()`, and `compileStringAsync()`.

* Add support for `CompileResult.loadedUrls`.

Expand Down
35 changes: 19 additions & 16 deletions lib/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,46 @@ import {Observable} from 'rxjs';
import * as supportsColor from 'supports-color';

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

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

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

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

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

// Creates a request for compiling a file.
Expand Down Expand Up @@ -99,6 +101,7 @@ function newCompileRequest(
options?: Options<'sync' | 'async'>
): proto.InboundMessage.CompileRequest {
const request = new proto.InboundMessage.CompileRequest();
request.setGlobalFunctionsList(Object.keys(options?.functions ?? {}));
request.setSourceMap(!!options?.sourceMap);
request.setAlertColor(options?.alertColor ?? !!supportsColor.stdout);
request.setAlertAscii(!!options?.alertAscii);
Expand Down Expand Up @@ -131,14 +134,16 @@ function newCompileRequest(
// resolves with the CompileResult. Throws if there were any protocol or
// compilation errors. Shuts down the compiler after compilation.
async function compileRequestAsync(
request: proto.InboundMessage.CompileRequest
request: proto.InboundMessage.CompileRequest,
options?: Options<'async'>
): Promise<CompileResult> {
const functionRegistry = new FunctionRegistry<'async'>(options?.functions);
const embeddedCompiler = new AsyncEmbeddedCompiler();

try {
// TODO(awjin): Pass import and function registries' handler functions to
// dispatcher.
const dispatcher = createDispatcher<'sync'>(
const dispatcher = createDispatcher<'async'>(
embeddedCompiler.stdout$,
buffer => {
embeddedCompiler.writeStdin(buffer);
Expand All @@ -153,9 +158,7 @@ async function compileRequestAsync(
handleCanonicalizeRequest: () => {
throw Error('Canonicalize not yet implemented.');
},
handleFunctionCallRequest: () => {
throw Error('Custom functions not yet implemented.');
},
handleFunctionCallRequest: request => functionRegistry.call(request),
}
);

Expand Down Expand Up @@ -183,8 +186,10 @@ async function compileRequestAsync(
// 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
request: proto.InboundMessage.CompileRequest,
options?: Options<'sync'>
): CompileResult {
const functionRegistry = new FunctionRegistry<'sync'>(options?.functions);
const embeddedCompiler = new SyncEmbeddedCompiler();

try {
Expand All @@ -205,9 +210,7 @@ function compileRequestSync(
handleCanonicalizeRequest: () => {
throw Error('Canonicalize not yet implemented.');
},
handleFunctionCallRequest: () => {
throw Error('Custom functions not yet implemented.');
},
handleFunctionCallRequest: request => functionRegistry.call(request),
}
);

Expand All @@ -225,7 +228,7 @@ function compileRequestSync(

for (;;) {
if (!embeddedCompiler.yield()) {
throw new Error('Embedded compiler exited unexpectedly.');
throw utils.compilerError('Embedded compiler exited unexpectedly.');
}

if (error) throw error;
Expand Down Expand Up @@ -280,6 +283,6 @@ function handleCompileResponse(
} else if (response.getFailure()) {
throw new Exception(response.getFailure()!);
} else {
throw Error('Compiler sent empty CompileResponse.');
throw utils.compilerError('Compiler sent empty CompileResponse.');
}
}
111 changes: 111 additions & 0 deletions lib/src/function-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2021 Google Inc. 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 {inspect} from 'util';

import * as types from './vendor/sass';
import * as utils from './utils';
import {CustomFunction} from './vendor/sass';
import {
InboundMessage,
OutboundMessage,
} from './vendor/embedded-protocol/embedded_sass_pb';
import {PromiseOr, catchOr, thenOr} from './utils';
import {Protofier} from './protofier';
import {Value} from './value';

export class FunctionRegistry<sync extends 'sync' | 'async'> {
private readonly functionsByName = new Map<string, CustomFunction<sync>>();
private readonly functionsById = new Array<CustomFunction<sync>>();
private readonly idsByFunction = new Map<CustomFunction<sync>, number>();

constructor(functionsBySignature?: Record<string, CustomFunction<sync>>) {
for (const [signature, fn] of Object.entries(functionsBySignature ?? {})) {
const openParen = signature.indexOf('(');
if (openParen === -1) {
throw new Error(`options.functions: "${signature}" is missing "("`);
}

this.functionsByName.set(signature.substring(0, openParen), fn);
}
}

/** Registers `fn` as a function that can be called using the returned ID. */
register(fn: CustomFunction<sync>): number {
return utils.putIfAbsent(this.idsByFunction, fn, () => {
const id = this.functionsById.length;
this.functionsById.push(fn);
return id;
});
}

/**
* Returns the function to which `request` refers and returns its response.
*/
call(
request: OutboundMessage.FunctionCallRequest
): PromiseOr<InboundMessage.FunctionCallResponse, sync> {
const protofier = new Protofier();
const fn = this.get(request);

return catchOr(
() => {
return thenOr(
fn(
request
.getArgumentsList()
.map(value => protofier.deprotofy(value) as types.Value)
),
result => {
if (!(result instanceof Value)) {
const name =
request.getName().length === 0
? 'anonymous function'
: `"${request.getName()}"`;
throw (
`options.functions: ${name} returned non-Value: ` +
inspect(result)
);
}

const response = new InboundMessage.FunctionCallResponse();
response.setSuccess(protofier.protofy(result));
return response;
}
);
},
error => {
const response = new InboundMessage.FunctionCallResponse();
response.setError(`${error}`);
return response;
}
);
}

/** Returns the function to which `request` refers. */
private get(
request: OutboundMessage.FunctionCallRequest
): CustomFunction<sync> {
if (
request.getIdentifierCase() ===
OutboundMessage.FunctionCallRequest.IdentifierCase.NAME
) {
const fn = this.functionsByName.get(request.getName());
if (fn) return fn;

throw new Error(
'Invalid OutboundMessage.FunctionCallRequest: there is no function ' +
`named "${request.getName()}"`
);
} else {
const fn = this.functionsById[request.getFunctionId()];
if (fn) return fn;

throw new Error(
'Invalid OutboundMessage.FunctionCallRequest: there is no function ' +
`with ID "${request.getFunctionId()}"`
);
}
}
}

0 comments on commit 7d45259

Please sign in to comment.