Skip to content

Commit

Permalink
Extend sass-loader options to use custom Sass functions
Browse files Browse the repository at this point in the history
Building Blueprint 4 from the SCSS source requires custom Sass functions [1].

As we’re using react-scripts, we can’t directly modify Webpack loader options. Common workarounds include libraries like react-scripts-rewired, craco, and others. Most of them aren’t actively maintained, but craco’s latest alpha release has support for react-scripts v5, so I’m using this for now. In the mid/long term, I hope to get rid of react-scripts/Webpack and be able to use a simpler build setup.

[1]: palantir/blueprint#4032 (comment)
  • Loading branch information
tillprochaska committed Oct 17, 2022
1 parent 86e37a0 commit 4325933
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 28 deletions.
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ services:
volumes:
- "./ui/src:/alephui/src:cached"
- "./ui/package.json:/alephui/package.json:cached"
- "./ui/craco.config.js:/alephui/craco.config.js:cached"
- "./ui/node_modules:/alephui/node_modules:delegated"
environment:
PORT: "8080"
Expand Down
1 change: 1 addition & 0 deletions ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ COPY .npmrc /alephui/.npmrc
COPY .prettierrc /alephui/.prettierrc
COPY tsconfig.json /alephui/tsconfig.json
COPY package.json /alephui
COPY craco.config.js /alephui

RUN npm install
RUN cp -R /alephui/node_modules/ /node_modules
Expand Down
81 changes: 81 additions & 0 deletions ui/craco.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const {
IconSvgPaths16,
IconSvgPaths20,
iconNameToPathsRecordKey,
} = require('@blueprintjs/icons');
const sass = require('sass');

const findFill = (map) => {
for (let i = 0; i < map.getLength(); i++) {
const key = map.getKey(i).getValue();
const value = map.getValue(i);

if (key === 'fill' && value instanceof sass.types.Color) {
return `rgba(${value.getR()}, ${value.getG()}, ${value.getB()}, ${value.getA()})`;
}

if (value instanceof sass.types.Map) {
const recursiveFill = findFill(value);

if (recursiveFill !== null) {
return recursiveFill;
}
}
}

return null;
};

// Blueprint internally uses a custom Sass function `svg-icon` [1] to inline
// icon SVGs to data URIs. In order to be able to build Blueprint from the SCSS
// source, we need to provide a reimplementation of this function to the Sass
// compiler. The implementation used by Blueprint [2] won’t work out of the box,
// as it relies on the raw SVG files which are not exported by `@blueprint/icons`.
// For this reason, the `svgIcon` function is implemented differently, using the
// SVG paths exported by `@blueprint/icons`.
//
// [1]: https://sass-lang.com/documentation/js-api/interfaces/Options#functions
// [2]: https://github.com/palantir/blueprint/blob/develop/packages/core/scripts/sass-custom-functions.js

const svgIcon = (path, selectors) => {
// Parse a string "16px/chevron-right.svg" into size (16) and a name (chevron-right).
const { size, name } = path
.getValue()
.match(/^(?<size>16|20)px\/(?<name>[a-z\-]+)\.svg$/)?.groups;

// The `selectors` argument is a nested map that represents CSS rules. For example:
// (path: (fill: '#000')). We try to find a `fill` property in the list and use that
// in the SVG generated below.
const fill = findFill(selectors);

// Get the SVG path for requested icon size and name.
const paths = size === '16' ? IconSvgPaths16 : IconSvgPaths20;
const path = paths[iconNameToPathsRecordKey(name)];

// Assemble an icon SVG element and encode it as a data URI.
const svg =
`<svg viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">` +
`<path fill-rule="evenodd" clip-rule="evenodd" fill="${fill}" d="${path}" />` +
`</svg>`;

const value = `url('data:image/svg+xml,${encodeURIComponent(svg)}')`;

return new sass.types.String(value);
};

module.exports = {
style: {
sass: {
implementation: sass,
loaderOptions: (options) => ({
...options,
implementation: sass,
sassOptions: {
functions: {
'svg-icon($path, $selectors: null)': svgIcon,
},
},
}),
},
},
};
11 changes: 6 additions & 5 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@blueprintjs/popover2": "^1.5.1",
"@blueprintjs/select": "^4.5.3",
"@blueprintjs/table": "^4.6.0",
"@craco/craco": "^7.0.0-alpha.7",
"@formatjs/cli": "^5.0.2",
"@formatjs/intl-locale": "^3.0.3",
"@formatjs/intl-pluralrules": "^5.0.2",
Expand Down Expand Up @@ -59,19 +60,19 @@
"redux-act": "^1.7.4",
"redux-thunk": "^2.3.0",
"rehype-raw": "^6.0.0",
"sass": "^1.54.4",
"sass": "^1.54.5",
"stream": "npm:stream-browserify@^3.0.0",
"truncate": "^3.0.0",
"typescript": "^4.0.2",
"uuid": "^9.0.0",
"yaml": "^2.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"start": "craco start",
"build": "craco build",
"lint": "eslint --ext js,jsx,ts,tsx src",
"test": "react-scripts test",
"eject": "react-scripts eject",
"test": "craco test",
"eject": "craco eject",
"format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,scss}'",
"format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,css,scss}'",
"translate": "npm run messages && npm run compile-translations && npm run concat-translations",
Expand Down
23 changes: 0 additions & 23 deletions ui/src/app/App.scss
Original file line number Diff line number Diff line change
@@ -1,26 +1,3 @@
// hack hack hack
// https://github.com/palantir/blueprint/issues/2976#issuecomment-479231949
$svg-icon-map: (
'16px/small-minus.svg':
"path fill-rule='evenodd' clip-rule='evenodd' d='M11 7H5c-.55 0-1 .45-1 1s.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1z' fill='%23fff'/",
'16px/small-tick.svg':
"path fill-rule='evenodd' clip-rule='evenodd' d='M12 5c-.28 0-.53.11-.71.29L7 9.59l-2.29-2.3a1.003 1.003 0 0 0-1.42 1.42l3 3c.18.18.43.29.71.29s.53-.11.71-.29l5-5A1.003 1.003 0 0 0 12 5z' fill='%23fff'/",
// '16px/chevron-right.svg': "path fill-rule='evenodd' clip-rule='evenodd' d='M10.71 7.29l-4-4a1.003 1.003 0 0 0-1.42 1.42L8.59 8 5.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l4-4c.18-.18.29-.43.29-.71 0-.28-.11-.53-.29-.71z' fill='%235C7080'/",
'16px/chevron-right.svg':
"path fill-rule='evenodd' clip-rule='evenodd' d='M10.71 7.29l-4-4a1.003 1.003 0 0 0-1.42 1.42L8.59 8 5.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l4-4c.18-.18.29-.43.29-.71 0-.28-.11-.53-.29-.71z' fill='%23ccc'/",
'16px/more.svg':
"g fill='%235C7080'%3E%3Ccircle cx='2' cy='8.03' r='2'/%3E%3Ccircle cx='14' cy='8.03' r='2'/%3E%3Ccircle cx='8' cy='8.03' r='2'/%3E%3C/g",
);

@function svg-icon($inline-svg, $fill-color) {
@return url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3C" + map-get(
$svg-icon-map,
$inline-svg
) + '%3E%3C/svg%3E');
}

$icon-font-path: '~@blueprintjs/icons/resources/icons';

@import '~@blueprintjs/core/src/blueprint.scss';
@import '~@blueprintjs/table/lib/css/table.css';
@import '~@blueprintjs/icons/src/blueprint-icons.scss';
Expand Down

0 comments on commit 4325933

Please sign in to comment.