Skip to content

Commit 80aba33

Browse files
committed
feat: Optional Preprocessors & library/application API contract.
There is now a core data type for preprocessors that conditionally process a stylesheet. The motivation for this is to allow libraries to expose a preprocessor integration for their specific Sass + Eyeglass library. This allows the library to maintain some semblance of control over how the compilation is performed, in case that is important to that module (E.g. the precision value for Sass or exposing assets via eyeglass). There's a base class for libraries to use (`DirectoryScopedPreprocessor`), and a helper function for applications that integrates all of the preprocessor adaptors into a single processor that supplies sass, eyeglass, and application default options. If the `DirectoryScopedPreprocessor` isn't sufficient for a library's needs, they can implement the `PreprocessorProvider` interface. Applications can use the simpler, `OptionalAdaptor` function for creating their own optional adaptors for preprocessing their own block files in different ways. The README for `@css-blocks/eyeglass` is updated to include boilerplate code that libraries and applications can use to get started.
1 parent 6023475 commit 80aba33

File tree

8 files changed

+297
-34
lines changed

8 files changed

+297
-34
lines changed

packages/@css-blocks/core/src/BlockParser/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export { BlockFactory } from "./BlockFactory";
1111

1212
export {
1313
Syntax,
14+
Preprocessor,
15+
OptionalPreprocessor,
1416
Preprocessors,
1517
ProcessedFile,
1618
} from "./preprocessing";

packages/@css-blocks/core/src/BlockParser/preprocessing.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export interface ProcessedFile {
4242
}
4343

4444
// export type ContentPreprocessor = (content: string) => Promise<ProcessedFile>;
45-
export type Preprocessor = (fullPath: string, content: string, configuration: ResolvedConfiguration, sourceMap?: RawSourceMap | string) => Promise<ProcessedFile>;
45+
export type Preprocessor<R extends ProcessedFile | null = ProcessedFile> = (fullPath: string, content: string, configuration: ResolvedConfiguration, sourceMap?: RawSourceMap | string) => Promise<R>;
46+
export type OptionalPreprocessor = Preprocessor<ProcessedFile | null>;
4647

4748
/**
4849
* A map of supported syntaxes to the preprocessor function for that syntax.

packages/@css-blocks/core/src/BlockTree/RulesetContainer.ts

+2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export class RulesetContainer<S extends Styles> {
100100
let style = this.parent;
101101
let selectors: ParsedSelector[] = style.getParsedSelectors(rule);
102102

103+
// XXX I think this is wrong. if the selectors target different styles it
104+
// will get confused. Need to add tests for that.
103105
selectors.forEach((selector) => {
104106
let ruleSet = new Ruleset(file, rule, style);
105107
let key = selector.key;

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

+67-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This package provides an easy way to integrate your CSS Blocks code with the Sas
77

88
## Usage
99

10-
Here's an example `css-blocks.config.js` file using this package.
10+
Here's an example `css-blocks.config.js` file using this package.
1111

1212
```js
1313
import sass from "node-sass";
@@ -25,4 +25,69 @@ module.exports = {
2525
};
2626
```
2727

28-
An important thing to notice here is that this adapter _does not provide Eyeglass for you_. Instead we use the module instance you pass into the adaptor. This means you're not tied whatever version of Eyeglass (or Sass) this package would include!
28+
## Building npm libraries that provide css-blocks written in Sass
29+
30+
If your addon provides CSS Block files that are written with Sass it will
31+
require the application that uses your addon to include Sass preprocessing in
32+
its configuration.
33+
34+
In turn, so the addon can maintain control over the preprocessing configuration
35+
that is used we recommend that your addon ship an "optional adaptor" that
36+
looks like this:
37+
38+
```ts
39+
import { DirectoryScopedPreprocessor } from "@css-blocks/eyeglass";
40+
41+
// a path to where your block files live
42+
const ADDON_DIR = path.resolve(__dirname, "..", "..") + "/";
43+
class MyModulesPreprocessor extends DirectoryScopedPreprocessor {
44+
setupOptions(options: EyeglassOptions): EyeglassOptions {
45+
// Don't manipulate the options passed in.
46+
return Object.assign({}, options, {precision: 20});
47+
}
48+
}
49+
50+
export const adaptor = new MyModulesPreprocessor(ADDON_DIR);
51+
```
52+
53+
## Building applications that consume Sass-preprocessed css-blocks
54+
55+
If your application consumes CSS Block files that are written with Sass
56+
you'll need to work with any adaptors provided by the extensions you're
57+
using. This css-blocks/eyeglass integration provides a helper function that
58+
will select the correct processor for the block file being processed or
59+
fall back to a default sass processor.
60+
61+
This css-blocks configuration file is an example of how to consume the
62+
eyeglass adaptors from other libraries that use this integration.
63+
64+
```ts
65+
// css-blocks.config.js
66+
67+
const sass = require("node-sass");
68+
const Eyeglass = require("eyeglass");
69+
import { adaptAll } from "@css-blocks/eyeglass";
70+
71+
// See the documentation for your module to know how to import
72+
// its adaptors.
73+
import { adaptor as fancyAdaptor } from "fancy-module";
74+
import { adaptor as anotherAdaptor } from "another-package";
75+
76+
const sassOptions = {
77+
// The default sass and eyeglass options for your application.
78+
// Where important, these options might be overridden by the module itself.
79+
};
80+
81+
// While it's probably irrelevant, the order of the adaptors here
82+
// does matter, the first one that wants to process a file will win.
83+
const eyeglassAdaptors = [
84+
fancyAdaptor,
85+
anotherAdaptor,
86+
];
87+
88+
export default {
89+
preprocesors: {
90+
scss: adaptAll(eyeglassAdaptors, sass, eyeglass, sassOptions),
91+
}
92+
};
93+
```
+141-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1-
import { ProcessedFile } from "@css-blocks/core";
2-
import Eyeglass from "eyeglass"; // works, even tho a cjs export. huh.
3-
import { Options, Result, SassError } from "node-sass";
4-
import SassImplementation from "node-sass";
1+
import type { OptionalPreprocessor, Preprocessor, ProcessedFile, ResolvedConfiguration } from "@css-blocks/core";
2+
import type { EyeglassOptions, default as Eyeglass } from "eyeglass"; // works, even tho a cjs export. huh.
3+
import type { Result, SassError } from "node-sass";
4+
import type SassImplementation from "node-sass";
5+
import { sep as PATH_SEPARATOR } from "path";
56

6-
export function adaptor(sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: Options = {}) {
7+
export type Adaptor = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions) => Preprocessor;
8+
export type OptionalAdaptor = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions) => OptionalPreprocessor;
9+
10+
/**
11+
* Given a Sass compiler (either dart-sass or node-sass), an Eyeglass
12+
* constructor, and common eyeglass/sass options. This function returns a
13+
* preprocessor, which is a function that can be used preprocess a single file.
14+
*
15+
* This function ensures that Sass is properly configured using the common
16+
* options for each file and that source map information is passed along to CSS
17+
* Blocks for correct error reporting.
18+
*/
19+
export const adaptor: Adaptor = (sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions = {}) => {
720
return (file: string, data: string) => {
821
return new Promise<ProcessedFile>((resolve, reject) => {
9-
const sassOptions = Object.assign(options, {
22+
const sassOptions = Object.assign({}, options, {
1023
file,
1124
data,
1225
sourceMap: true,
1326
outFile: file.replace(/scss$/, "css"),
1427
});
15-
1628
sass.render(eyeglass(sassOptions), (err: SassError, res: Result): void => {
1729
if (err) {
1830
reject(err);
@@ -26,4 +38,125 @@ export function adaptor(sass: typeof SassImplementation, eyeglass: typeof Eyegla
2638
});
2739
});
2840
};
29-
}
41+
};
42+
43+
/**
44+
* This is the core interface that adaptAll depends on to use an object (as
45+
* opposed to an OptionalAdaptor function) to create a preprocessor.
46+
*/
47+
export interface PreprocessorProvider {
48+
init(sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions): void;
49+
preprocessor(): Preprocessor | OptionalPreprocessor;
50+
}
51+
52+
/**
53+
* Type guard to check if an object fulfills the basic interface required by
54+
* PreprocessorProvider.
55+
*/
56+
function isPreprocessorProvider(obj: unknown): obj is PreprocessorProvider {
57+
if (typeof obj !== "object" || obj === null) return false;
58+
let provider = <PreprocessorProvider>obj;
59+
return typeof provider.init === "function" && typeof provider.preprocessor === "function";
60+
}
61+
62+
/**
63+
* Provides a preprocessor that only runs on files within a specific directory
64+
* (or subdirectories of that directory, recursively).
65+
*/
66+
export class DirectoryScopedPreprocessor implements PreprocessorProvider {
67+
protected filePrefix: string;
68+
protected scssProcessor: Preprocessor | undefined;
69+
70+
/**
71+
* Instantiates the preprocessor provider.
72+
*
73+
* In the case where a preprocessor provider is being provided by a an npm
74+
* package that is being consumed by an application, this instantiation
75+
* would be performed by the npm package.
76+
*
77+
* @param packageDirectory The absolute path to the directory that scopes
78+
* this preprocessor.
79+
*/
80+
constructor(packageDirectory: string) {
81+
this.filePrefix = packageDirectory.endsWith(PATH_SEPARATOR) ? packageDirectory : packageDirectory + PATH_SEPARATOR;
82+
}
83+
84+
/**
85+
* Initializes the sass preprocessor that is used to only compile the files
86+
* that are in scope. These parameters are provided by the application,
87+
* usually via adaptAll().
88+
*
89+
* If you need to enforce a version constraint on the Sass or Eyeglass
90+
* implementation being used, you can override this and check sass.info and
91+
* eyeglass.VERSION.
92+
*/
93+
init(sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions = {}) {
94+
this.scssProcessor = adaptor(sass, eyeglass, this.setupOptions(options));
95+
}
96+
97+
/**
98+
* Subclasses can override this to manipulate/override the eyeglass options
99+
* provided from the application that will be used for compiling this
100+
* package's block files.
101+
*
102+
* Note: If this is being used from a library that is an eyeglass module,
103+
* The module will be auto-discovered by eyeglass, you don't need to do
104+
* anything here.
105+
*/
106+
setupOptions(options: EyeglassOptions): EyeglassOptions {
107+
return options;
108+
}
109+
110+
/**
111+
* Subclasses can override this to decide whether a file should be processed.
112+
* By default it just checks that the file is within the directory for this
113+
* Preprocessor provider.
114+
*/
115+
shouldProcessFile(file: string) {
116+
return file.startsWith(this.filePrefix);
117+
}
118+
119+
/**
120+
* Subclasses shouldn't need to override this.
121+
* @returns the preprocessor expected by adaptAll.
122+
*/
123+
preprocessor(): OptionalPreprocessor {
124+
return (file: string, data: string, config: ResolvedConfiguration) => {
125+
if (!this.scssProcessor) return Promise.reject(new Error("Adaptor was not initialized!"));
126+
if (this.shouldProcessFile(file)) {
127+
return this.scssProcessor(file, data, config);
128+
} else {
129+
return Promise.resolve(null);
130+
}
131+
};
132+
}
133+
}
134+
135+
/**
136+
* Creates a unified preprocessor for an application to use when consuming
137+
* css blocks that have Sass preprocessed.
138+
*
139+
* The application provides a list of preprocessor adaptors, as well as the
140+
* desired versions of sass, eyeglass and common Sass/Eyeglass options for
141+
* compiling the sass files with eyeglass support.
142+
*/
143+
export function adaptAll(adaptors: Array<OptionalAdaptor | PreprocessorProvider>, sass: typeof SassImplementation, eyeglass: typeof Eyeglass, options: EyeglassOptions): Preprocessor {
144+
let processors = adaptors.map(adaptor => {
145+
if (isPreprocessorProvider(adaptor)) {
146+
adaptor.init(sass, eyeglass, options);
147+
return adaptor.preprocessor();
148+
} else {
149+
return adaptor(sass, eyeglass, options);
150+
}
151+
});
152+
let lastResortProcessor = adaptor(sass, eyeglass, options);
153+
return async (file: string, data: string, config: ResolvedConfiguration) => {
154+
for (let processor of processors) {
155+
let result = await processor(file, data, config);
156+
if (result) {
157+
return result;
158+
}
159+
}
160+
return lastResortProcessor(file, data, config);
161+
};
162+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@mixin block($name) {
2+
:root {
3+
name: $name;
4+
}
5+
}
6+
7+
@include block("uno");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@mixin block($name) {
2+
:root {
3+
name: $name;
4+
}
5+
}
6+
7+
@include block("dos");

0 commit comments

Comments
 (0)