Skip to content

Commit 8adf7e6

Browse files
committed
Exclude patterns
1 parent e3d1117 commit 8adf7e6

5 files changed

Lines changed: 103 additions & 12 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ Folder containing `.cs` files. Subfolders are included.
5555
`-f`, `--file-filter` optional
5656
Glob-style filter for source files, such as `*.cs` or `*Dto.cs`. Default: `*.cs`.
5757

58+
`-x`, `--exclude-pattern` optional
59+
Glob-style source file exclude pattern. Can be repeated or passed as a comma-separated list, such as `*Attributes.cs,*Internal.cs`. Patterns are matched against file names and source-relative paths. Default: none.
60+
5861
`-t`, `--type-filter` optional
5962
Glob-style filter for parsed type names, such as `Person*`. Default: all parsed types.
6063

@@ -87,6 +90,7 @@ Create `typesharp.json`:
8790
{
8891
"source": "./samples/SampleModels",
8992
"fileFilter": "*.cs",
93+
"excludePatterns": ["*Attributes.cs", "*Internal.cs"],
9094
"typeFilter": "*",
9195
"destination": "./samples/SampleModels/Typescript",
9296
"watch": false,

src/typesharp-ts/dist/main.mjs

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.

src/typesharp-ts/spec/node_test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from "vitest";
22
import { strict as assert } from "node:assert";
3-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
44
import { tmpdir } from "node:os";
55
import { dirname, join, resolve } from "node:path";
66
import { fileURLToPath } from "node:url";
@@ -186,6 +186,7 @@ test("generates sample outputs for current golden DTOs", async () => {
186186
await convert({
187187
source: join(root, "samples/SampleModels"),
188188
fileFilter: "*.cs",
189+
excludePatterns: [],
189190
destination: temp,
190191
watch: false,
191192
exportModule: false,
@@ -212,6 +213,9 @@ test("supports current CLI arguments", () => {
212213
"-f",
213214
"*.cs",
214215
"--type-filter=Person*",
216+
"--exclude-pattern",
217+
"*Attributes.cs",
218+
"-x=*Internal.cs",
215219
"-d",
216220
"out",
217221
"-w",
@@ -226,6 +230,7 @@ test("supports current CLI arguments", () => {
226230
configPath: "./config/tssharp.json",
227231
source: "samples",
228232
fileFilter: "*.cs",
233+
excludePatterns: ["*Attributes.cs", "*Internal.cs"],
229234
typeFilter: "Person*",
230235
destination: "out",
231236
watch: true,
@@ -246,6 +251,7 @@ test("loads typesharp.json and lets CLI override file values", async () => {
246251
JSON.stringify({
247252
source: "models",
248253
fileFilter: "*.cs",
254+
excludePatterns: ["*Attributes.cs", "*Internal.cs"],
249255
destination: "generated",
250256
exportModule: true,
251257
dictionaryStyle: "index-signature",
@@ -257,6 +263,7 @@ test("loads typesharp.json and lets CLI override file values", async () => {
257263
assert.deepEqual(options, {
258264
source: join(temp, "models"),
259265
fileFilter: "*.cs",
266+
excludePatterns: ["*Attributes.cs", "*Internal.cs"],
260267
destination: join(temp, "generated"),
261268
typeFilter: undefined,
262269
watch: false,
@@ -286,6 +293,7 @@ test("defaults fileFilter to C# source files", async () => {
286293
assert.deepEqual(options, {
287294
source: join(temp, "models"),
288295
fileFilter: "*.cs",
296+
excludePatterns: [],
289297
destination: join(temp, "generated"),
290298
typeFilter: undefined,
291299
watch: false,
@@ -300,6 +308,38 @@ test("defaults fileFilter to C# source files", async () => {
300308
}
301309
});
302310

311+
test("excludes source files by configured patterns", async () => {
312+
const temp = await mkdtemp(join(tmpdir(), "typesharp-"));
313+
try {
314+
const source = join(temp, "models");
315+
const destination = join(temp, "generated");
316+
await writeFile(join(temp, "typesharp.json"), JSON.stringify({
317+
source: "models",
318+
destination: "generated",
319+
excludePatterns: ["*Attributes.cs", "Internal/*Internal.cs"],
320+
}));
321+
await mkdir(join(source, "Internal"), { recursive: true });
322+
await writeFile(join(source, "Person.cs"), "public class Person { public string Name { get; set; } }");
323+
await writeFile(
324+
join(source, "OptionalAttributes.cs"),
325+
"public class OptionalAttributes { public string Name { get; set; } }",
326+
);
327+
await writeFile(
328+
join(source, "Internal", "HiddenInternal.cs"),
329+
"public class HiddenInternal { public string Name { get; set; } }",
330+
);
331+
332+
const options = await resolveOptions([], temp);
333+
await convert(options);
334+
335+
assert.equal(await readFile(join(destination, "Person.ts"), "utf8").then(() => true), true);
336+
await assert.rejects(() => readFile(join(destination, "OptionalAttributes.ts"), "utf8"), /ENOENT/);
337+
await assert.rejects(() => readFile(join(destination, "HiddenInternal.ts"), "utf8"), /ENOENT/);
338+
} finally {
339+
await rm(temp, { recursive: true, force: true });
340+
}
341+
});
342+
303343
test("fails clearly on unsupported property bodies", () => {
304344
assert.throws(
305345
() => parseSourceFile("bad.cs", 'public class Bad { public string Name { get { return "x"; } set { } } }'),

src/typesharp-ts/src/main.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
99
export interface CliOptions extends GenerateOptions {
1010
source: string;
1111
fileFilter: string;
12+
excludePatterns: string[];
1213
destination: string;
1314
typeFilter?: string;
1415
watch: boolean;
@@ -30,7 +31,7 @@ export async function run(args: string[]): Promise<void> {
3031
}
3132

3233
export async function convert(options: CliOptions): Promise<void> {
33-
const sourceFiles = await findCSharpFiles(options.source, options.fileFilter);
34+
const sourceFiles = await findCSharpFiles(options.source, options.fileFilter, options.excludePatterns);
3435
const parsed = [];
3536
for (const path of sourceFiles) {
3637
parsed.push(parseSourceFile(path, await readFile(path, "utf8")));
@@ -75,12 +76,14 @@ export function parseArgs(args: string[]): ParsedCliOptions {
7576
c: "config",
7677
s: "source",
7778
f: "file-filter",
79+
x: "exclude-pattern",
7880
t: "type-filter",
7981
d: "destination",
8082
w: "watch",
8183
m: "export-module",
8284
};
8385
const booleanFlags = new Set(["watch", "export-module", "readonly-properties", "semicolons"]);
86+
const arrayFlags = new Set(["exclude-pattern", "exclude-patterns"]);
8487

8588
for (let i = 0; i < args.length; i++) {
8689
const raw = args[i];
@@ -94,6 +97,10 @@ export function parseArgs(args: string[]): ParsedCliOptions {
9497
const name = aliases[flagName] ?? flagName;
9598
if (booleanFlags.has(name)) {
9699
values.set(name, inlineValue === undefined ? true : parseBoolean(inlineValue, name));
100+
} else if (arrayFlags.has(name)) {
101+
const value = inlineValue ?? args[++i];
102+
if (!value || value.startsWith("-")) throw new Error(`Missing value for --${name}`);
103+
appendListValue(values, "exclude-patterns", value);
97104
} else {
98105
const value = inlineValue ?? args[++i];
99106
if (!value || value.startsWith("-")) throw new Error(`Missing value for --${name}`);
@@ -141,6 +148,7 @@ function finalizeOptions(options: ConfigFileOptions): CliOptions {
141148
return {
142149
source: options.source,
143150
fileFilter: options.fileFilter ?? "*.cs",
151+
excludePatterns: normalizeStringList(options.excludePatterns, "excludePatterns"),
144152
destination: options.destination,
145153
typeFilter: options.typeFilter,
146154
watch: options.watch ?? false,
@@ -157,6 +165,7 @@ function mapRawOptions(values: Map<string, string | boolean>): ParsedCliOptions
157165
setString(options, "configPath", values.get("config"));
158166
setString(options, "source", values.get("source"));
159167
setString(options, "fileFilter", values.get("file-filter"));
168+
setStringList(options, "excludePatterns", values.get("exclude-patterns"));
160169
setString(options, "typeFilter", values.get("type-filter"));
161170
setString(options, "destination", values.get("destination"));
162171
setBoolean(options, "watch", values.get("watch"));
@@ -168,6 +177,11 @@ function mapRawOptions(values: Map<string, string | boolean>): ParsedCliOptions
168177
return options;
169178
}
170179

180+
function appendListValue(values: Map<string, string | boolean>, key: string, value: string): void {
181+
const existing = values.get(key);
182+
values.set(key, [typeof existing === "string" ? existing : "", value].filter(Boolean).join(","));
183+
}
184+
171185
function setString<T extends Record<string, unknown>>(
172186
target: T,
173187
key: keyof T,
@@ -176,6 +190,14 @@ function setString<T extends Record<string, unknown>>(
176190
if (typeof value === "string") target[key] = value as T[keyof T];
177191
}
178192

193+
function setStringList<T extends Record<string, unknown>>(
194+
target: T,
195+
key: keyof T,
196+
value: string | boolean | undefined,
197+
): void {
198+
if (typeof value === "string") target[key] = splitList(value) as T[keyof T];
199+
}
200+
179201
function setBoolean<T extends Record<string, unknown>>(
180202
target: T,
181203
key: keyof T,
@@ -184,17 +206,35 @@ function setBoolean<T extends Record<string, unknown>>(
184206
if (typeof value === "boolean") target[key] = value as T[keyof T];
185207
}
186208

209+
function normalizeStringList(value: unknown, optionName: string): string[] {
210+
if (value === undefined) return [];
211+
if (typeof value === "string") return splitList(value);
212+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
213+
return value.map((item) => item.trim()).filter(Boolean);
214+
}
215+
throw new Error(`${optionName} must be an array of strings`);
216+
}
217+
218+
function splitList(value: string): string[] {
219+
return value.split(",").map((item) => item.trim()).filter(Boolean);
220+
}
221+
187222
function parseBoolean(value: string, flag: string): boolean {
188223
if (["true", "1", "yes"].includes(value.toLowerCase())) return true;
189224
if (["false", "0", "no"].includes(value.toLowerCase())) return false;
190225
throw new Error(`--${flag} must be true or false`);
191226
}
192227

193-
async function findCSharpFiles(source: string, filter: string): Promise<string[]> {
228+
async function findCSharpFiles(source: string, filter: string, excludePatterns: string[] = []): Promise<string[]> {
194229
const files: string[] = [];
195230
const matcher = globToRegExp(filter === "*" ? "*.cs" : filter);
231+
const excludeMatchers = excludePatterns.map(globToRegExp);
196232
for await (const entry of walk(source)) {
197-
if (entry.isFile && entry.path.endsWith(".cs") && matcher.test(basename(entry.path))) {
233+
const relativePath = relativePathFrom(source, entry.path);
234+
const excluded = excludeMatchers.some((excludeMatcher) =>
235+
excludeMatcher.test(basename(entry.path)) || excludeMatcher.test(relativePath)
236+
);
237+
if (entry.isFile && entry.path.endsWith(".cs") && matcher.test(basename(entry.path)) && !excluded) {
198238
files.push(entry.path);
199239
}
200240
}
@@ -267,6 +307,12 @@ function normalizePath(path: string): string {
267307
return normalized === "/" ? normalized : normalized.replace(/\/$/, "");
268308
}
269309

310+
function relativePathFrom(root: string, path: string): string {
311+
const normalizedRoot = `${normalizePath(root)}/`;
312+
const normalizedPath = normalizePath(path);
313+
return normalizedPath.startsWith(normalizedRoot) ? normalizedPath.slice(normalizedRoot.length) : basename(path);
314+
}
315+
270316
function resolvePath(base: string, path: string): string {
271317
return normalizePath(isAbsolute(path) ? path : resolve(base, path));
272318
}

src/typesharp-ts/typesharp.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"source": "./../../samples/SampleModels",
33
"fileFilter": "*.cs",
4+
"excludePatterns": [],
45
"typeFilter": "*",
56
"destination": "./../../samples/SampleModels/Typescript",
67
"watch": false,
@@ -9,4 +10,4 @@
910
"readonlyProperties": false,
1011
"quoteStyle": "double",
1112
"semicolons": true
12-
}
13+
}

0 commit comments

Comments
 (0)