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

+304
-95
lines changed

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

+9-4
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

+40-23
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

+16-5
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
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.foo[disabled=disabled] {
2+
color: red;
3+
}
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+
}
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+
}
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+
}
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

+1
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

+1-1
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

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ export class BlockParser {
8585
await globalAttributes(root, block, sourceFile);
8686
// Parse all block styles and build block tree.
8787
debug(` - Construct Block`);
88-
await constructBlock(root, block, debugIdent);
88+
await constructBlock(root, block, this.factory.configuration, debugIdent);
8989
// Verify that external blocks referenced have been imported, have defined the attribute being selected, and have marked it as a global state.
9090
debug(` - Assert Foreign Globals`);
91-
await assertForeignGlobalAttribute(root, block, debugIdent);
91+
await assertForeignGlobalAttribute(this.factory.configuration, root, block, debugIdent);
9292
// Construct block extensions and validate.
9393
debug(` - Extend Block`);
9494
await extendBlock(root, block, debugIdent);

packages/@css-blocks/core/src/BlockParser/features/assert-foreign-global-attribute.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { postcss, postcssSelectorParser as selectorParser } from "opticss";
22

33
import { Block } from "../../BlockTree";
4+
import { Configuration } from "../../configuration";
45
import * as errors from "../../errors";
56
import { selectorSourceRange as loc } from "../../SourceLocation";
67
import { isAttributeNode, toAttrToken } from "../block-intermediates";
@@ -12,7 +13,7 @@ import { isAttributeNode, toAttrToken } from "../block-intermediates";
1213
* @param rule The rule referencing the external block.
1314
* @param obj The parsed node making the external reference.
1415
*/
15-
export async function assertForeignGlobalAttribute(root: postcss.Root, block: Block, file: string) {
16+
export async function assertForeignGlobalAttribute(configuration: Configuration, root: postcss.Root, block: Block, file: string) {
1617

1718
root.walkRules((rule) => {
1819

@@ -35,30 +36,30 @@ export async function assertForeignGlobalAttribute(root: postcss.Root, block: Bl
3536
if (!isAttributeNode(node)) {
3637
throw new errors.InvalidBlockSyntax(
3738
`Only global states from other blocks can be used in selectors: ${rule.selector}`,
38-
loc(file, rule, node));
39+
loc(configuration, block.stylesheet, file, rule, node));
3940
}
4041

4142
// If referenced block does not exist, throw.
4243
let otherBlock = block.getReferencedBlock(blockName.value);
4344
if (!otherBlock) {
4445
throw new errors.InvalidBlockSyntax(
4546
`No Block named "${blockName.value}" found in scope: ${rule.selector}`,
46-
loc(file, rule, node));
47+
loc(configuration, block.stylesheet, file, rule, node));
4748
}
4849

4950
// If state referenced does not exist on external block, throw
5051
let otherAttr = otherBlock.rootClass.getAttributeValue(toAttrToken(node));
5152
if (!otherAttr) {
5253
throw new errors.InvalidBlockSyntax(
5354
`No state ${node.toString()} found in : ${rule.selector}`,
54-
loc(file, rule, node));
55+
loc(configuration, block.stylesheet, file, rule, node));
5556
}
5657

5758
// If external state is not set as global, throw.
5859
if (!otherAttr.isGlobal) {
5960
throw new errors.InvalidBlockSyntax(
6061
`${node.toString()} is not global: ${rule.selector}`,
61-
loc(file, rule, node));
62+
loc(configuration, block.stylesheet, file, rule, node));
6263
}
6364

6465
}

0 commit comments

Comments
 (0)