Skip to content

Commit 78756f2

Browse files
committed
feat: Display selector error locations using sourcemaps.
The source location displayed in error messages by default now will be the source-mapped location for all selector errors. In the CLI the generated source location as well as the source mapped location are displayed. This is because, in my testing, the generated source displays the error better and more reliably, but you still need a pointer to the place in the authored source file to make the change, especially in the case of block files that are composed of content from multiple source files. Unfortunately, the sourcemap sometimes returns a location that is not what the author would expect and not helpful.
1 parent 2317880 commit 78756f2

19 files changed

Lines changed: 304 additions & 95 deletions

packages/@css-blocks/cli/src/extract-lines-from-source.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ export interface ExtractionResult {
88
};
99
}
1010
export function extractLinesFromSource(
11-
range: Required<SourceRange>,
11+
range: Required<SourceRange> & { source?: string },
1212
additionalLinesBefore = 1,
1313
additionalLinesAfter = 0,
14-
): ExtractionResult {
15-
let { filename, start, end } = range;
16-
let contents = fs.readFileSync(filename, "utf-8");
14+
): ExtractionResult | undefined {
15+
let contents: string | undefined;
16+
let { filename, start, end, source } = range;
17+
try {
18+
contents = source || fs.readFileSync(filename, "utf-8");
19+
} catch (e) {
20+
return;
21+
}
1722
let allLines = contents.split(/\r?\n/);
1823
if (start.line <= additionalLinesBefore) {
1924
additionalLinesBefore = start.line - 1;

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

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { BlockFactory, CssBlockError, ErrorWithPosition, Importer, NodeJsImporter, Preprocessors, hasErrorPosition } from "@css-blocks/core";
1+
import {
2+
BlockFactory,
3+
CssBlockError,
4+
ErrorWithPosition,
5+
Importer,
6+
NodeJsImporter,
7+
Preprocessors,
8+
hasErrorPosition,
9+
hasMappedPosition,
10+
} from "@css-blocks/core";
211
import chalk = require("chalk");
312
import fse = require("fs-extra");
413
import path = require("path");
@@ -162,32 +171,40 @@ export class CLI {
162171
if (!hasErrorPosition(loc)) {
163172
return;
164173
}
165-
loc.end.line = 4;
166174
let filename = path.relative(process.cwd(), path.resolve(loc && loc.filename || blockFileRelative));
167-
let context: ExtractionResult | undefined;
168-
let lineNumber: number | undefined;
169-
context = extractLinesFromSource(loc, 1, 1);
170-
lineNumber = loc.start.line - context.additionalLines.before;
171-
if (context) {
175+
this.println("\t" + this.chalk.bold.redBright(e.origMessage));
176+
if (hasMappedPosition(loc)) {
172177
this.println(
173-
this.chalk.bold.white("\tAt"),
174-
this.chalk.bold.whiteBright(`${filename}:${loc.start.line}:${loc.start.column}`),
175-
`${e.origMessage}`,
178+
this.chalk.bold.white("\tAt compiled output of"),
179+
this.chalk.bold.whiteBright(`${loc.generated.filename}:${loc.generated.start.line}:${loc.generated.start.column}`),
176180
);
177-
for (let i = 0; i < context.lines.length; i++) {
178-
let prefix;
179-
let line = context.lines[i];
180-
if (i < context.additionalLines.before ||
181-
i >= context.lines.length - context.additionalLines.after) {
182-
prefix = this.chalk.bold(`${lineNumber}: `);
183-
} else {
184-
prefix = this.chalk.bold.redBright(`${lineNumber}: `);
185-
let {before, during, after } = this.splitLineOnErrorRange(line, lineNumber, loc);
186-
line = `${before}${this.chalk.underline.redBright(during)}${after}`;
187-
}
188-
this.println("\t" + prefix + line);
189-
if (lineNumber) lineNumber++;
181+
this.displaySnippet(extractLinesFromSource(loc.generated, 1, 1), loc.generated);
182+
}
183+
this.println(
184+
this.chalk.bold.white(hasMappedPosition(loc) ? "\tSource Mapped to" : "\tAt"),
185+
this.chalk.bold.whiteBright(`${filename}:${loc.start.line}:${loc.start.column}`),
186+
);
187+
this.displaySnippet(extractLinesFromSource(loc, 1, 1), loc);
188+
}
189+
190+
displaySnippet(context: ExtractionResult | undefined, loc: ErrorWithPosition) {
191+
if (!context) return;
192+
let lineNumber: number | undefined;
193+
lineNumber = loc.start.line - context.additionalLines.before;
194+
195+
for (let i = 0; i < context.lines.length; i++) {
196+
let prefix;
197+
let line = context.lines[i];
198+
if (i < context.additionalLines.before ||
199+
i >= context.lines.length - context.additionalLines.after) {
200+
prefix = this.chalk.bold(`${lineNumber}:${line ? " " : ""}`);
201+
} else {
202+
prefix = this.chalk.bold.redBright(`${lineNumber}:${line ? " " : ""}`);
203+
let {before, during, after } = this.splitLineOnErrorRange(line, lineNumber, loc);
204+
line = `${before}${this.chalk.underline.redBright(during)}${after}`;
190205
}
206+
this.println("\t" + prefix + line);
207+
if (lineNumber) lineNumber++;
191208
}
192209
}
193210

packages/@css-blocks/cli/test/cli-test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ describe("validate", () => {
3131
await cli.run(["validate", fixture("basic/error.block.css")]);
3232
assert.equal(cli.output,
3333
`error\t${relFixture("basic/error.block.css")}
34-
\tAt ${relFixture("basic/error.block.css")}:1:5 Two distinct classes cannot be selected on the same element: .foo.bar
34+
\tTwo distinct classes cannot be selected on the same element: .foo.bar
35+
\tAt ${relFixture("basic/error.block.css")}:1:5
3536
\t1: .foo.bar {
3637
\t2: color: red;
37-
\t3: }
3838
Found 1 error in 1 file.
3939
`);
4040
assert.equal(cli.exitCode, 1);
@@ -44,10 +44,10 @@ Found 1 error in 1 file.
4444
await cli.run(["validate", fixture("basic/transitive-error.block.css")]);
4545
assert.equal(cli.output,
4646
`error\t${relFixture("basic/transitive-error.block.css")}
47-
\tAt ${relFixture("basic/error.block.css")}:1:5 Two distinct classes cannot be selected on the same element: .foo.bar
47+
\tTwo distinct classes cannot be selected on the same element: .foo.bar
48+
\tAt ${relFixture("basic/error.block.css")}:1:5
4849
\t1: .foo.bar {
4950
\t2: color: red;
50-
\t3: }
5151
Found 1 error in 1 file.
5252
`);
5353
assert.equal(cli.exitCode, 1);
@@ -78,7 +78,18 @@ describe("validate with preprocessors", () => {
7878
it("can check syntax for a bad block file", async () => {
7979
let cli = new CLI();
8080
await cli.run(["validate", "--preprocessors", distFile("test/preprocessors"), fixture("scss/error.block.scss")]);
81-
assert.equal(cli.output.split(/\n/)[1].trim(), `At ${relFixture("scss/error.block.scss")}:5:5 Two distinct classes cannot be selected on the same element: .foo.bar`);
81+
assert.equal(cli.output, `error test/fixtures/scss/error.block.scss
82+
\tTwo distinct classes cannot be selected on the same element: .foo.bar
83+
\tAt compiled output of test/fixtures/scss/error.block.scss:5:5
84+
\t4:
85+
\t5: .foo.bar {
86+
\t6: color: blue;
87+
\tSource Mapped to test/fixtures/scss/error.block.scss:3:5
88+
\t2:
89+
\t3: .foo {
90+
\t4: color: red;
91+
Found 1 error in 1 file.
92+
`);
8293
assert.equal(cli.exitCode, 1);
8394
});
8495
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.foo[disabled=disabled] {
2+
color: red;
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import "mixins";
2+
3+
.a {
4+
@include plus-b() {
5+
color: purple;
6+
}
7+
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
@import "mixins";
2+
13
.foo {
24
color: red;
3-
&.bar {
5+
@include add-bar() {
46
color: blue;
57
}
6-
}
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@mixin add-bar() {
2+
&.bar {
3+
@content;
4+
}
5+
}
6+
7+
@mixin plus-b() {
8+
& + .bbb {
9+
@content;
10+
}
11+
}
12+
13+
@mixin in-state($state-name, $state-value: null) {
14+
@if $state-value {
15+
&[state|#{$state-name}=#{$state-value}] {
16+
@content;
17+
}
18+
}
19+
@else {
20+
&[state|#{$state-name}] {
21+
@content;
22+
}
23+
}
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import "mixins";
2+
3+
div {
4+
@include in-state(selected) {
5+
outline: 1px dotted black;
6+
}
7+
}

packages/@css-blocks/cli/test/preprocessors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const scss: Preprocessors["scss"] = async (fullPath, content, _configuration, _s
99
outFile: fullPath.replace("scss", "css"),
1010
data: content,
1111
sourceMap: true,
12+
sourceMapContents: true,
1213
outputStyle: "expanded",
1314
indentedSyntax: false,
1415
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class BlockFactory {
175175
let sourceMap = sourceMapFromProcessedFile(preprocessResult);
176176
let content = preprocessResult.content;
177177
if (sourceMap) {
178-
content = annotateCssContentWithSourceMap(content, sourceMap);
178+
content = annotateCssContentWithSourceMap(this.configuration, filename, content, sourceMap);
179179
}
180180
let root = await this.postcssImpl.parse(content, { from: filename });
181181

0 commit comments

Comments
 (0)