Skip to content

Commit

Permalink
Add support for Options.functions (#86)
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 a1ccd45
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 a1ccd45

Please sign in to comment.