Skip to content

Commit a48431b

Browse files
authored
feat: extend exclude condition support to include strings and functions (#49)
1 parent b9a0ffc commit a48431b

File tree

7 files changed

+112
-26
lines changed

7 files changed

+112
-26
lines changed

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ At this time, the build artifacts can include all syntax supported by ES2020, su
166166
167167
### exclude
168168
169-
- **Type:** `RegExp | RegExp[]`
169+
- **Type:**
170+
171+
```ts
172+
type Condition = string | RegExp | ((filepath: string) => boolean);
173+
type Exclude = Condition | Condition[];
174+
```
175+
170176
- **Default:** `undefined`
171177
172178
Excludes a portion of source files during detection. You can pass in one or more regular expressions to match the paths of source files. Files that match the regular expression will be ignored and will not trigger syntax checking.
@@ -181,9 +187,33 @@ pluginCheckSyntax({
181187
});
182188
```
183189
190+
Or pass in a function to match the paths of source files. Files that return true when the function is called will be ignored:
191+
192+
```ts
193+
pluginCheckSyntax({
194+
exclude: (filepath) => filepath.includes("node_modules/foo"),
195+
});
196+
```
197+
198+
Or passing an absolute path to match the paths of source files. Files that match the absolute path will be ignored:
199+
200+
```ts
201+
import path from "node:path";
202+
203+
pluginCheckSyntax({
204+
exclude: path.posix.join(__dirname, "node_modules/foo"),
205+
});
206+
```
207+
184208
### excludeOutput
185209
186-
- **Type:** `RegExp | RegExp[]`
210+
- **Type:**
211+
212+
```ts
213+
type Condition = string | RegExp | ((filepath: string) => boolean);
214+
type Exclude = Condition | Condition[];
215+
```
216+
187217
- **Default:** `undefined`
188218
189219
Excludes a portion of output files before detection. You can pass in one or more regular expressions to match the paths of output files. Files that match the regular expression will be ignored and will not trigger syntax checking.

src/CheckSyntaxPlugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
22
import type { Rspack } from '@rsbuild/core';
33
import { CheckSyntax } from './checkSyntax.js';
44
import { printErrors } from './printErrors.js';
5-
import { isPathExcluded } from './utils.js';
5+
import { isExcluded } from './utils.js';
66

77
type Compiler = Rspack.Compiler;
88
type Compilation = Rspack.Compilation;
@@ -29,7 +29,7 @@ export class CheckSyntaxRspackPlugin extends CheckSyntax {
2929
// remove query from name
3030
const resourcePath = a.name.split('?')[0];
3131
const file = resolve(outputPath, resourcePath);
32-
if (isPathExcluded(file, this.excludeOutput)) {
32+
if (isExcluded(file, this.excludeOutput)) {
3333
return '';
3434
}
3535
return file;

src/checkSyntax.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
EcmaVersion,
1212
SyntaxErrorKey,
1313
} from './types.js';
14-
import { isPathExcluded } from './utils.js';
14+
import { isExcluded } from './utils.js';
1515

1616
const HTML_REGEX = /\.html$/;
1717
export const JS_REGEX: RegExp = /\.(?:js|mjs|cjs|jsx)$/;
@@ -72,7 +72,7 @@ export class CheckSyntax {
7272
const htmlScripts = await generateHtmlScripts(filepath);
7373
await Promise.all(
7474
htmlScripts.map(async (script) => {
75-
if (!isPathExcluded(filepath, this.exclude)) {
75+
if (!isExcluded(filepath, this.exclude)) {
7676
await this.tryParse(filepath, script);
7777
}
7878
}),

src/generateError.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import fs from 'node:fs';
2+
import path from 'node:path';
23
import color from 'picocolors';
34
import { SourceMapConsumer } from 'source-map';
45
import {
56
type AcornParseError,
67
type CheckSyntaxExclude,
78
ECMASyntaxError,
89
} from './types.js';
9-
import { isExcluded, isPathExcluded } from './utils.js';
10+
import { isExcluded } from './utils.js';
1011

1112
export function displayCodePointer(code: string, pos: number) {
1213
const SUB_LEN = 80;
@@ -36,6 +37,7 @@ export async function generateError({
3637
let error = await tryGenerateErrorFromSourceMap({
3738
err,
3839
code,
40+
rootPath,
3941
outputFilepath: filepath,
4042
relativeOutputPath,
4143
});
@@ -44,14 +46,15 @@ export async function generateError({
4446
error = new ECMASyntaxError(err.message, {
4547
source: {
4648
path: relativeOutputPath,
49+
absolutePath: filepath,
4750
line: err.loc.line,
4851
column: err.loc.column,
4952
code: displayCodePointer(code, err.pos),
5053
},
5154
});
5255
}
5356

54-
if (isPathExcluded(error.source.path, exclude)) {
57+
if (isExcluded(error.source.absolutePath, exclude)) {
5558
return null;
5659
}
5760

@@ -80,14 +83,25 @@ export function makeCodeFrame(lines: string[], highlightIndex: number) {
8083
return `\n${ret.join('\n')}`;
8184
}
8285

86+
/**
87+
* Extract resource path from Rspack's devtoolModuleFilenameTemplate
88+
* "resource-path" in "webpack://[namespace]/[resource-path]?[loaders]"
89+
*/
90+
function extractResourcePath(source: string) {
91+
const match = source.match(/^webpack:\/\/(?:(?:[^/]+)\/)?([^?]+)(?:\?.*)?$/);
92+
return match?.[1] || source;
93+
}
94+
8395
async function tryGenerateErrorFromSourceMap({
8496
err,
8597
code,
98+
rootPath,
8699
outputFilepath,
87100
relativeOutputPath,
88101
}: {
89102
err: AcornParseError;
90103
code: string;
104+
rootPath: string;
91105
outputFilepath: string;
92106
relativeOutputPath: string;
93107
}): Promise<ECMASyntaxError | null> {
@@ -113,12 +127,14 @@ async function tryGenerateErrorFromSourceMap({
113127
const sourceIndex = sources.indexOf(mappedPosition.source);
114128
const sourceContent: string | null =
115129
JSON.parse(sourcemap).sourcesContent?.[sourceIndex];
116-
const sourcePath = mappedPosition.source.replace(/webpack:\/\/(tmp)?/g, '');
130+
const sourcePath = extractResourcePath(mappedPosition.source);
131+
const absoluteSourcePath = path.join(rootPath, sourcePath);
117132

118133
if (!sourceContent) {
119134
return new ECMASyntaxError(err.message, {
120135
source: {
121136
path: sourcePath,
137+
absolutePath: absoluteSourcePath,
122138
line: mappedPosition.line ?? 0,
123139
column: mappedPosition.column ?? 0,
124140
code: displayCodePointer(code, err.pos),
@@ -137,6 +153,7 @@ async function tryGenerateErrorFromSourceMap({
137153
return new ECMASyntaxError(err.message, {
138154
source: {
139155
path: sourcePath,
156+
absolutePath: absoluteSourcePath,
140157
line: mappedPosition.line ?? 0,
141158
column: mappedPosition.column ?? 0,
142159
code: makeCodeFrame(rawLines, highlightLine),

src/types.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import type { ecmaVersion as EcmaVersion } from 'acorn';
22

33
export type { EcmaVersion };
44

5-
export type CheckSyntaxExclude = RegExp | RegExp[];
5+
export type Condition = string | RegExp | ((filepath: string) => boolean);
6+
7+
export type CheckSyntaxExclude = Condition | Condition[];
68

79
export type CheckSyntaxOptions = {
810
/**
@@ -24,7 +26,7 @@ export type CheckSyntaxOptions = {
2426
* Excludes specified error messages.
2527
* You can pass in one or more regular expressions to match the reasons.
2628
*/
27-
excludeErrorMessage?: CheckSyntaxExclude;
29+
excludeErrorMessage?: RegExp | RegExp[];
2830
/**
2931
* Ignores specified syntax error messages after detection.
3032
* You can pass in one or more error message types to ignore.
@@ -48,13 +50,15 @@ export interface File {
4850
column: number;
4951
}
5052

53+
type Source = File & { code: string } & { absolutePath: string };
54+
5155
interface SyntaxErrorOptions {
52-
source: File & { code: string };
56+
source: Source;
5357
output?: File;
5458
}
5559

5660
export class ECMASyntaxError extends Error {
57-
source: File & { code: string };
61+
source: Source;
5862

5963
output: File | undefined;
6064

src/utils.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ export const isExcluded = (input: string, exclude?: CheckSyntaxExclude) => {
44
if (!exclude) {
55
return false;
66
}
7+
78
const excludes = Array.isArray(exclude) ? exclude : [exclude];
8-
return excludes.some((reg) => reg.test(input));
9-
};
9+
// normalize to posix path for RegExp
10+
const normalizedPath = input.replace(/\\/g, '/');
1011

11-
export function isPathExcluded(
12-
path: string,
13-
exclude?: CheckSyntaxExclude,
14-
): boolean {
15-
// normalize to posix path
16-
const normalizedPath = path.replace(/\\/g, '/');
17-
return isExcluded(normalizedPath, exclude);
18-
}
12+
return excludes.some((condition) => {
13+
if (typeof condition === 'function') {
14+
return condition(input);
15+
}
16+
if (typeof condition === 'string') {
17+
return input.startsWith(condition);
18+
}
19+
return condition.test(normalizedPath);
20+
});
21+
};

test/rsbuild-basic/index.test.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { dirname } from 'node:path';
1+
import path, { dirname } from 'node:path';
22
import { fileURLToPath } from 'node:url';
33
import { createRsbuild, loadConfig, mergeRsbuildConfig } from '@rsbuild/core';
44
import { expect, test } from '@rstest/core';
55
import stripAnsi from 'strip-ansi';
6-
import { pluginCheckSyntax } from '../../dist';
6+
import { pluginCheckSyntax } from '../../src';
77
import { normalizeToPosixPath, proxyConsole } from '../helper';
88

99
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -92,7 +92,7 @@ test('should check assets with query correctly', async () => {
9292
).toBeTruthy();
9393
});
9494

95-
test('should not throw error when the source file is excluded', async () => {
95+
test('should not throw error when the source file is excluded via RegExp', async () => {
9696
const rsbuild = await createRsbuild({
9797
cwd: __dirname,
9898
rsbuildConfig: {
@@ -108,6 +108,38 @@ test('should not throw error when the source file is excluded', async () => {
108108
await expect(rsbuild.build()).resolves.toBeTruthy();
109109
});
110110

111+
test('should not throw error when the source file is excluded via function', async () => {
112+
const rsbuild = await createRsbuild({
113+
cwd: __dirname,
114+
rsbuildConfig: {
115+
...(await loadConfig({ cwd: __dirname })).content,
116+
plugins: [
117+
pluginCheckSyntax({
118+
exclude: (filepath) => /src[\\/]test\.js/.test(filepath),
119+
}),
120+
],
121+
},
122+
});
123+
124+
await expect(rsbuild.build()).resolves.toBeTruthy();
125+
});
126+
127+
test('should not throw error when the source file is excluded via string', async () => {
128+
const rsbuild = await createRsbuild({
129+
cwd: __dirname,
130+
rsbuildConfig: {
131+
...(await loadConfig({ cwd: __dirname })).content,
132+
plugins: [
133+
pluginCheckSyntax({
134+
exclude: path.join(__dirname, 'src/test.js'),
135+
}),
136+
],
137+
},
138+
});
139+
140+
await expect(rsbuild.build()).resolves.toBeTruthy();
141+
});
142+
111143
test('should not throw error when the output file is excluded', async () => {
112144
const rsbuild = await createRsbuild({
113145
cwd: __dirname,

0 commit comments

Comments
 (0)