Skip to content

Commit 2210c13

Browse files
devversionandrewseguin
authored andcommitted
build: configure custom sass compiler to allow for module imports
Workaround until https://docs.google.com/document/d/1yVRAJptClM1YNH2XqdpPOef9rmLMVeo_qTJLM5j1gDo/edit is addressed/resolved. We need to tell Sass how `@angular/<cdk,material,etc>` imports can be resolved so that we can start using them, similar to how NPM-consumers would. This is a key part of making our NPM packages work with Yarn berry, or more generally making our packages not rely on unspecified behavior (our package exports should not allow for deep imports, but currently we rely on those!)
1 parent 4eb6308 commit 2210c13

File tree

9 files changed

+163
-18
lines changed

9 files changed

+163
-18
lines changed

WORKSPACE

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,6 @@ load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories"
100100

101101
web_test_repositories()
102102

103-
# Fetch transitive dependencies which are needed to use the Sass rules.
104-
load("@io_bazel_rules_sass//:package.bzl", "rules_sass_dependencies")
105-
106-
rules_sass_dependencies()
107-
108103
# Setup the Sass rule repositories.
109104
load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories")
110105

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"@bazel/rollup": "5.3.0",
9393
"@bazel/runfiles": "5.3.0",
9494
"@bazel/terser": "5.3.0",
95+
"@bazel/worker": "5.3.0",
9596
"@firebase/app-types": "^0.7.0",
9697
"@material/animation": "14.0.0-canary.9736ddce9.0",
9798
"@material/auto-init": "14.0.0-canary.9736ddce9.0",

scripts/create-legacy-tests-bundle.mjs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,33 @@ import child_process from 'child_process';
55
import esbuild from 'esbuild';
66
import fs from 'fs';
77
import glob from 'glob';
8+
import module from 'module';
89
import {dirname, join, relative} from 'path';
910
import sass from 'sass';
1011
import url from 'url';
12+
import tsNode from 'ts-node';
1113

1214
const containingDir = dirname(url.fileURLToPath(import.meta.url));
1315
const projectDir = join(containingDir, '../');
16+
const packagesDir = join(projectDir, 'src/');
1417
const legacyTsconfigPath = join(projectDir, 'src/tsconfig-legacy.json');
1518

19+
// Some tooling utilities might be written in TS and we do not want to rewrite them
20+
// in JavaScript just for this legacy script. We can use ts-node for such scripts.
21+
tsNode.register({project: join(containingDir, 'tsconfig.json')});
22+
23+
const require = module.createRequire(import.meta.url);
24+
const sassImporterUtil = require('../tools/sass/local-sass-importer.ts');
25+
1626
const distDir = join(projectDir, 'dist/');
1727
const nodeModulesDir = join(projectDir, 'node_modules/');
1828
const outFile = join(distDir, 'legacy-test-bundle.spec.js');
1929
const ngcBinFile = join(nodeModulesDir, '@angular/compiler-cli/bundles/src/bin/ngc.js');
2030
const legacyOutputDir = join(distDir, 'legacy-test-out');
2131

32+
/** Sass importer used for resolving `@angular/<..>` imports. */
33+
const localPackageSassImporter = sassImporterUtil.createLocalAngularPackageImporter(packagesDir);
34+
2235
/**
2336
* This script builds the whole library in `angular/components` together with its
2437
* spec files into a single IIFE bundle.
@@ -136,11 +149,12 @@ async function createEntryPointSpecFile() {
136149

137150
/** Helper function to render a Sass file asynchronously using promises. */
138151
async function renderSassFileAsync(inputFile) {
139-
return new Promise((resolve, reject) => {
140-
sass.render({file: inputFile, includePaths: [nodeModulesDir]}, (err, result) =>
141-
err ? reject(err) : resolve(result.css),
142-
);
143-
});
152+
return sass
153+
.compileAsync(inputFile, {
154+
loadPaths: [nodeModulesDir, projectDir],
155+
importers: [localPackageSassImporter],
156+
})
157+
.then(result => result.css);
144158
}
145159

146160
/**

src/material/core/theming/tests/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ ts_library(
4848
devmode_module = "commonjs",
4949
deps = [
5050
"//tools/postcss",
51+
"//tools/sass:sass_lib",
52+
"@npm//@bazel/runfiles",
5153
"@npm//@types/jasmine",
5254
"@npm//@types/node",
5355
"@npm//@types/sass",

src/material/core/theming/tests/theming-api.spec.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import {parse, Root, Rule} from 'postcss';
2-
import {renderSync} from 'sass';
2+
import {compileString} from 'sass';
3+
import {runfiles} from '@bazel/runfiles';
4+
import * as path from 'path';
35

46
import {compareNodes} from '../../../../../tools/postcss/compare-nodes';
7+
import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer';
8+
9+
// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
10+
// which are guaranteed to reside in the source tree.
11+
const testDir = path.join(runfiles.resolvePackageRelative('../_all-theme.scss'), '../tests');
12+
const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..');
13+
14+
const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir);
515

616
describe('theming api', () => {
717
/** Map of known selectors for density styles and their corresponding AST rule. */
@@ -298,19 +308,20 @@ describe('theming api', () => {
298308

299309
/** Transpiles given Sass content into CSS. */
300310
function transpile(content: string) {
301-
return renderSync({
302-
data: `
311+
return compileString(
312+
`
303313
@import '../_all-theme.scss';
304314
@import '../../color/_all-color.scss';
305315
@import '../../density/private/_all-density.scss';
306316
@import '../../typography/_all-typography.scss';
317+
307318
${content}
308319
`,
309-
// Ensures that files can be resolved through Bazel runfile resolution. This
310-
// is necessary because the imported Sass files are not necessarily in the
311-
// bazel output directory. e.g. `_all-theme.scss` is a source file.
312-
importer: url => ({file: require.resolve(url)}),
313-
}).css.toString();
320+
{
321+
loadPaths: [testDir],
322+
importers: [localPackageSassImporter],
323+
},
324+
).css.toString();
314325
}
315326

316327
/**

tools/defaults.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def _getDefaultTsConfig(testonly):
7575
def sass_binary(sourcemap = False, **kwargs):
7676
_sass_binary(
7777
sourcemap = sourcemap,
78+
compiler = "//tools/sass:compiler",
7879
**kwargs
7980
)
8081

tools/sass/BUILD.bazel

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
2+
load("//tools:defaults.bzl", "ts_library")
3+
4+
package(default_visibility = ["//visibility:public"])
5+
6+
ts_library(
7+
name = "sass_lib",
8+
srcs = [
9+
"compiler-main.ts",
10+
"local-sass-importer.ts",
11+
],
12+
# TODO(ESM): remove this once the Bazel NodeJS rules can handle ESM with `nodejs_binary`.
13+
devmode_module = "commonjs",
14+
deps = [
15+
"@npm//@bazel/worker",
16+
"@npm//@types/node",
17+
"@npm//@types/yargs",
18+
"@npm//sass",
19+
"@npm//yargs",
20+
],
21+
)
22+
23+
nodejs_binary(
24+
name = "compiler",
25+
data = [":sass_lib"],
26+
entry_point = "compiler-main.ts",
27+
templated_args = ["--bazel_patch_module_resolver"],
28+
)

tools/sass/compiler-main.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as worker from '@bazel/worker';
2+
import * as fs from 'fs';
3+
import * as sass from 'sass';
4+
import * as path from 'path';
5+
import yargs from 'yargs';
6+
7+
import {createLocalAngularPackageImporter} from './local-sass-importer';
8+
9+
const workerArgs = process.argv.slice(2);
10+
11+
// Note: This path is relative to the current working directory as build actions
12+
// are always spawned in the execroot (which is exactly what we want).
13+
const execrootProjectDir = path.resolve('./src/');
14+
const localPackageSassImporter = createLocalAngularPackageImporter(execrootProjectDir);
15+
16+
if (require.main === module) {
17+
main().catch(e => {
18+
console.error(e);
19+
process.exitCode = 1;
20+
});
21+
}
22+
23+
async function main() {
24+
if (worker.runAsWorker(workerArgs)) {
25+
await worker.runWorkerLoop(args =>
26+
processBuildAction(args)
27+
.then(() => true)
28+
.catch(error => {
29+
worker.log(error);
30+
return false;
31+
}),
32+
);
33+
} else {
34+
// For non-worker mode, we parse the flag/params file ourselves. The Sass rule
35+
// uses a multi-line params file (with `\n` used as separator).
36+
const configFile = workerArgs[0].replace(/^@+/, '');
37+
const configContent = fs.readFileSync(configFile, 'utf8').trim();
38+
const args = configContent.split('\n');
39+
40+
await processBuildAction(args);
41+
}
42+
}
43+
44+
/**
45+
* Processes a build action expressed through command line arguments
46+
* as composed by the `sass_binary` rule.
47+
*/
48+
async function processBuildAction(args: string[]) {
49+
const {loadPath, style, sourceMap, embedSources, inputExecpath, outExecpath} = await yargs(args)
50+
.showHelpOnFail(false)
51+
.strict()
52+
.parserConfiguration({'greedy-arrays': false})
53+
.command('$0 <inputExecpath> <outExecpath>', 'Compiles a Sass file')
54+
.positional('inputExecpath', {type: 'string', demandOption: true})
55+
.positional('outExecpath', {type: 'string', demandOption: true})
56+
.option('embedSources', {type: 'boolean'})
57+
.option('errorCss', {type: 'boolean'})
58+
.option('sourceMap', {type: 'boolean'})
59+
.option('loadPath', {type: 'array', string: true})
60+
.option('style', {type: 'string'})
61+
.parseAsync();
62+
63+
const result = sass.compile(inputExecpath, {
64+
style: style as sass.OutputStyle,
65+
sourceMap,
66+
sourceMapIncludeSources: embedSources,
67+
loadPaths: loadPath,
68+
importers: [localPackageSassImporter],
69+
});
70+
71+
await fs.promises.writeFile(outExecpath, result.css);
72+
}

tools/sass/local-sass-importer.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {pathToFileURL} from 'url';
2+
import {join} from 'path';
3+
import * as sass from 'sass';
4+
5+
/** Prefix indicating Angular-owned Sass imports. */
6+
const angularPrefix = '@angular/';
7+
8+
/**
9+
* Creates a Sass `FileImporter` that resolves `@angular/<..>` packages to the
10+
* specified local packages directory.
11+
*/
12+
export function createLocalAngularPackageImporter(packageDirAbsPath: string): sass.FileImporter {
13+
return {
14+
findFileUrl: (url: string) => {
15+
if (url.startsWith(angularPrefix)) {
16+
return pathToFileURL(join(packageDirAbsPath, url.substring(angularPrefix.length)));
17+
}
18+
return null;
19+
},
20+
};
21+
}

0 commit comments

Comments
 (0)