Skip to content

Commit f3b11af

Browse files
committed
feat: Update eyeglass integration to support synchronous preprocessing.
1 parent 0b31607 commit f3b11af

File tree

3 files changed

+141
-7
lines changed

3 files changed

+141
-7
lines changed

packages/@css-blocks/eyeglass/README.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ Here's an example `css-blocks.config.js` file using this package.
1212
```js
1313
import sass from "node-sass";
1414
import eyeglass from "eyeglass";
15-
import { adaptor } from "@css-blocks/eyeglass";
15+
import { adaptor, adaptorSync } from "@css-blocks/eyeglass";
1616

1717
const sassOptions = {
1818
outputStyle: "expanded"
1919
};
2020

2121
const scss = adaptor(sass, eyeglass, sassOptions);
22+
const scssSync = adaptorSync(sass, eyeglass, sassOptions);
2223

2324
module.exports = {
2425
preprocessors: { scss }
26+
preprocessorsSync: { scss: scssSync }
2527
};
2628
```
2729

@@ -67,7 +69,7 @@ eyeglass adaptors from other libraries that use this integration.
6769

6870
const sass = require("node-sass");
6971
const Eyeglass = require("eyeglass");
70-
import { adaptAll } from "@css-blocks/eyeglass";
72+
import { adaptAll, adaptAllSync } from "@css-blocks/eyeglass";
7173

7274
// See the documentation for your module to know how to import
7375
// its adaptors.
@@ -79,6 +81,10 @@ const sassOptions = {
7981
// Where important, these options might be overridden by the module itself.
8082
};
8183

84+
const sassOptionsSync = Object.assign({}, sassOptions, {
85+
// if necessary use different options for synchronous compilation (e.g. synchronous versions of JS functions)
86+
});
87+
8288
// While it's probably irrelevant, the order of the adaptors here
8389
// does matter, the first one that wants to process a file will win.
8490
const eyeglassAdaptors = [
@@ -90,5 +96,8 @@ export default {
9096
preprocesors: {
9197
scss: adaptAll(eyeglassAdaptors, sass, eyeglass, sassOptions),
9298
}
99+
preprocesorsSync: {
100+
scss: adaptAllSync(eyeglassAdaptors, sass, eyeglass, sassOptionsSync),
101+
}
93102
};
94103
```

packages/@css-blocks/eyeglass/src/index.ts

+97-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import type { OptionalPreprocessor, Preprocessor, ProcessedFile, ResolvedConfiguration } from "@css-blocks/core";
1+
import type { OptionalPreprocessor, OptionalPreprocessorSync, Preprocessor, PreprocessorSync, ProcessedFile, ResolvedConfiguration } from "@css-blocks/core";
22
import type { EyeglassOptions, default as Eyeglass } from "eyeglass"; // works, even tho a cjs export. huh.
33
import type { Result, SassError } from "node-sass";
44
import type SassImplementation from "node-sass";
55
import { sep as PATH_SEPARATOR } from "path";
66

77
export type Adaptor = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions) => Preprocessor;
8+
export type AdaptorSync = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions) => PreprocessorSync;
89
export type OptionalAdaptor = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions) => OptionalPreprocessor;
10+
export type OptionalAdaptorSync = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions) => OptionalPreprocessorSync;
911

1012
/**
1113
* Given a Sass compiler (either dart-sass or node-sass), an Eyeglass
@@ -40,13 +42,40 @@ export const adaptor: Adaptor = (sass: typeof SassImplementation, eyeglass: type
4042
};
4143
};
4244

45+
/**
46+
* Given a Sass compiler (either dart-sass or node-sass), an Eyeglass
47+
* constructor, and common eyeglass/sass options. This function returns a
48+
* sync preprocessor, which is a function that can be used preprocess a single file.
49+
*
50+
* This function ensures that Sass is properly configured using the common
51+
* options for each file and that source map information is passed along to CSS
52+
* Blocks for correct error reporting.
53+
*/
54+
export const adaptorSync: AdaptorSync = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions = {}) => {
55+
return (file: string, data: string) => {
56+
const sassOptions = Object.assign({}, options, {
57+
file,
58+
data,
59+
sourceMap: true,
60+
outFile: file.replace(/scss$/, "css"),
61+
});
62+
let res = sass.renderSync(eyeglass(sassOptions));
63+
return {
64+
content: res.css.toString(),
65+
sourceMap: res.map.toString(),
66+
dependencies: res.stats.includedFiles,
67+
};
68+
};
69+
};
70+
4371
/**
4472
* This is the core interface that adaptAll depends on to use an object (as
4573
* opposed to an OptionalAdaptor function) to create a preprocessor.
4674
*/
4775
export interface PreprocessorProvider {
4876
init(sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions): void;
4977
preprocessor(): Preprocessor | OptionalPreprocessor;
78+
preprocessorSync(): PreprocessorSync | OptionalPreprocessorSync;
5079
}
5180

5281
/**
@@ -56,7 +85,8 @@ export interface PreprocessorProvider {
5685
function isPreprocessorProvider(obj: unknown): obj is PreprocessorProvider {
5786
if (typeof obj !== "object" || obj === null) return false;
5887
let provider = <PreprocessorProvider>obj;
59-
return typeof provider.init === "function" && typeof provider.preprocessor === "function";
88+
return typeof provider.init === "function" && typeof provider.preprocessor === "function"
89+
&& typeof provider.preprocessorSync === "function";
6090
}
6191

6292
/**
@@ -66,6 +96,7 @@ function isPreprocessorProvider(obj: unknown): obj is PreprocessorProvider {
6696
export class DirectoryScopedPreprocessor implements PreprocessorProvider {
6797
protected filePrefix: string;
6898
protected scssProcessor: Preprocessor | undefined;
99+
protected scssProcessorSync: PreprocessorSync | undefined;
69100

70101
/**
71102
* Instantiates the preprocessor provider.
@@ -91,7 +122,10 @@ export class DirectoryScopedPreprocessor implements PreprocessorProvider {
91122
* eyeglass.VERSION.
92123
*/
93124
init(sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions = {}) {
94-
this.scssProcessor = adaptor(sass, eyeglass, this.setupOptions(options));
125+
let sassOptions = this.setupOptions(options);
126+
let sassOptionsSync = this.setupOptionsSync ? this.setupOptionsSync(sassOptions) : sassOptions;
127+
this.scssProcessor = adaptor(sass, eyeglass, sassOptions);
128+
this.scssProcessorSync = adaptorSync(sass, eyeglass, sassOptionsSync);
95129
}
96130

97131
/**
@@ -107,6 +141,20 @@ export class DirectoryScopedPreprocessor implements PreprocessorProvider {
107141
return options;
108142
}
109143

144+
/**
145+
* Subclasses can override this to manipulate/override the eyeglass options
146+
* provided from the application that will be used for compiling this
147+
* package's block files synchronously.
148+
*
149+
* The options passed into this function are those returned by
150+
* setupOptions(), so this method only needs to update those options as
151+
* appropriate to support synchronous compilation.
152+
*
153+
* If not provided, the options returned from setupOptions() are used for
154+
* synchronous compilation.
155+
*/
156+
setupOptionsSync?(options: EyeglassOptions): EyeglassOptions;
157+
110158
/**
111159
* Subclasses can override this to decide whether a file should be processed.
112160
* By default it just checks that the file is within the directory for this
@@ -130,6 +178,23 @@ export class DirectoryScopedPreprocessor implements PreprocessorProvider {
130178
}
131179
};
132180
}
181+
182+
/**
183+
* Subclasses shouldn't need to override this.
184+
* @returns the preprocessor expected by adaptAll.
185+
*/
186+
preprocessorSync(): OptionalPreprocessorSync {
187+
return (file: string, data: string, config: ResolvedConfiguration) => {
188+
if (!this.scssProcessorSync) {
189+
throw new Error("Adaptor was not initialized!");
190+
}
191+
if (this.shouldProcessFile(file)) {
192+
return this.scssProcessorSync(file, data, config);
193+
} else {
194+
return null;
195+
}
196+
};
197+
}
133198
}
134199

135200
/**
@@ -160,3 +225,32 @@ export function adaptAll(adaptors: Array<OptionalAdaptor | PreprocessorProvider>
160225
return lastResortProcessor(file, data, config);
161226
};
162227
}
228+
229+
/**
230+
* Creates a unified preprocessor for an application to use when consuming
231+
* css blocks that have Sass preprocessed.
232+
*
233+
* The application provides a list of preprocessor adaptors, as well as the
234+
* desired versions of sass, eyeglass and common Sass/Eyeglass options for
235+
* compiling the sass files with eyeglass support.
236+
*/
237+
export function adaptAllSync(adaptors: Array<OptionalAdaptorSync | PreprocessorProvider>, sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions): PreprocessorSync {
238+
let processors = adaptors.map(adaptor => {
239+
if (isPreprocessorProvider(adaptor)) {
240+
adaptor.init(sass, eyeglass, options);
241+
return adaptor.preprocessorSync();
242+
} else {
243+
return adaptorSync(sass, eyeglass, options);
244+
}
245+
});
246+
let lastResortProcessor = adaptorSync(sass, eyeglass, options);
247+
return (file: string, data: string, config: ResolvedConfiguration) => {
248+
for (let processor of processors) {
249+
let result = processor(file, data, config);
250+
if (result) {
251+
return result;
252+
}
253+
}
254+
return lastResortProcessor(file, data, config);
255+
};
256+
}

packages/@css-blocks/eyeglass/test/package-test.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SassImplementation = require("node-sass");
99
import * as path from "path";
1010
import * as sinon from "sinon";
1111

12-
import { DirectoryScopedPreprocessor, adaptAll, adaptor } from "../src/";
12+
import { DirectoryScopedPreprocessor, adaptAll, adaptAllSync, adaptor } from "../src/";
1313

1414
const fakeSass = {
1515
render: sinon.spy(),
@@ -32,7 +32,7 @@ describe("@css-blocks/eyeglass", async () => {
3232
expect(adaptor(fakeSass, fakeEyeglass, {})).to.be.a("function");
3333
});
3434

35-
it("returned function returns a Promise when called", async () => {
35+
it("returned function that returns a Promise when called", async () => {
3636
const injector = adaptor(fakeSass, fakeEyeglass, {});
3737
const result = injector("file", "data", resolveConfiguration({}));
3838

@@ -139,6 +139,37 @@ describe("@css-blocks/eyeglass", async () => {
139139
name: "dos";
140140
}
141141
142+
/*# sourceMappingURL=two.block.css.map */`));
143+
});
144+
it("can adapt from several optional adaptors - synchronous", () => {
145+
let package1Dir = fixture("package-1");
146+
let package1File = fixture("package-1/one.block.scss");
147+
let package2Dir = fixture("package-2");
148+
let package2File = fixture("package-2/two.block.scss");
149+
class Adaptor1 extends DirectoryScopedPreprocessor {
150+
setupOptions(options: EyeglassOptions): EyeglassOptions {
151+
return Object.assign({}, options, {outputStyle: "compact"});
152+
}
153+
}
154+
class Adaptor2 extends DirectoryScopedPreprocessor {
155+
setupOptions(options: EyeglassOptions): EyeglassOptions {
156+
return Object.assign({}, options, {outputStyle: "expanded"});
157+
}
158+
}
159+
let adaptor1 = new Adaptor1(package1Dir);
160+
let adaptor2 = new Adaptor2(package2Dir);
161+
let processor = adaptAllSync([adaptor1, adaptor2], SassImplementation, Eyeglass, {});
162+
let result1 = processor(package1File, fs.readFileSync(package1File, "utf-8"), resolveConfiguration({}));
163+
assert.equal(result1.content, dedent(`
164+
:root { name: "uno"; }
165+
166+
/*# sourceMappingURL=one.block.css.map */`));
167+
let result2 = processor(package2File, fs.readFileSync(package2File, "utf-8"), resolveConfiguration({}));
168+
assert.equal(result2.content, dedent(`
169+
:root {
170+
name: "dos";
171+
}
172+
142173
/*# sourceMappingURL=two.block.css.map */`));
143174
});
144175
});

0 commit comments

Comments
 (0)