typescript-bedrock 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/files/base/justfile +2 -1
- package/files/bunfig.toml.template +1 -1
- package/files/tsconfig.json +1 -1
- package/oxlintrc.json +6 -0
- package/package.json +2 -2
- package/src/adopt/adopt.ts +19 -2
- package/src/adopt/blocks.ts +1 -0
- package/src/adopt/bunfig.ts +202 -0
- package/src/adopt/tests.ts +10 -12
- package/src/adopt/toml-scan.ts +117 -0
- package/src/adopt/tsconfig.ts +11 -2
- package/src/files.ts +23 -0
- package/src/guard.ts +1 -19
package/files/base/justfile
CHANGED
|
@@ -8,10 +8,11 @@ mod? typescript ".just/typescript.just"
|
|
|
8
8
|
mod? rust ".just/rust.just"
|
|
9
9
|
mod? swift ".just/swift.just"
|
|
10
10
|
mod? cpp ".just/cpp.just"
|
|
11
|
+
mod? jvm ".just/jvm.just"
|
|
11
12
|
|
|
12
13
|
import? "project.just"
|
|
13
14
|
|
|
14
|
-
languages := "typescript rust swift cpp"
|
|
15
|
+
languages := "typescript rust swift cpp jvm"
|
|
15
16
|
|
|
16
17
|
# List the recipes.
|
|
17
18
|
default:
|
|
@@ -24,7 +24,7 @@ coverage = true
|
|
|
24
24
|
coverageSkipTestFiles = true
|
|
25
25
|
# The threshold is enforced only while the `text` reporter is on.
|
|
26
26
|
coverageReporter = ["text", "lcov"]
|
|
27
|
-
coverageThreshold = { lines = 0
|
|
27
|
+
coverageThreshold = { lines = 0, functions = 0 }
|
|
28
28
|
|
|
29
29
|
[fmt]
|
|
30
30
|
useTabs = true
|
package/files/tsconfig.json
CHANGED
package/oxlintrc.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typescript-bedrock",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Strictest Bun + TypeScript 7 tooling: shared configs, git hooks, a guard, and an adopt command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "xsyetopz",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"eslint-plugin-jsdoc": "65.4.5",
|
|
29
29
|
"markdownlint-cli2": "0.24.0",
|
|
30
30
|
"markdownlint-rule-title-case-style": "0.4.3",
|
|
31
|
-
"oxlint": "1.
|
|
31
|
+
"oxlint": "1.93.0",
|
|
32
32
|
"oxlint-tsgolint": "7.0.2006",
|
|
33
33
|
"typescript": "7.0.2"
|
|
34
34
|
},
|
package/src/adopt/adopt.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { mergeBunfig } from "./bunfig.ts";
|
|
4
5
|
import { asRecord, exists, type Json, readJsonc, toJson } from "./json.ts";
|
|
5
6
|
import { migrateManifest } from "./manifest.ts";
|
|
6
7
|
import { sharedSteps } from "./shared.ts";
|
|
@@ -175,6 +176,21 @@ const planManifest = async (
|
|
|
175
176
|
);
|
|
176
177
|
};
|
|
177
178
|
|
|
179
|
+
const planBunfig = async (
|
|
180
|
+
filesRoot: string,
|
|
181
|
+
target: string,
|
|
182
|
+
): Promise<{ steps: Step[]; notes: string[] }> => {
|
|
183
|
+
const file = Bun.file(join(target, "bunfig.toml"));
|
|
184
|
+
const merged = mergeBunfig(
|
|
185
|
+
await Bun.file(join(filesRoot, storedPath("bunfig.toml"))).text(),
|
|
186
|
+
(await file.exists()) ? await file.text() : undefined,
|
|
187
|
+
);
|
|
188
|
+
return {
|
|
189
|
+
steps: [{ kind: "write", path: "bunfig.toml", content: merged.content }],
|
|
190
|
+
notes: merged.notes,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
178
194
|
const planTsconfig = async (
|
|
179
195
|
packageRoot: string,
|
|
180
196
|
target: string,
|
|
@@ -218,6 +234,7 @@ export const planAdoption = async (
|
|
|
218
234
|
const filesRoot = join(packageRoot, "files");
|
|
219
235
|
const migrated = await planManifest(packageRoot, target, options);
|
|
220
236
|
const ts = await planTsconfig(packageRoot, target);
|
|
237
|
+
const bunfig = await planBunfig(filesRoot, target);
|
|
221
238
|
const ci = Bun.file(join(target, OLD_CI));
|
|
222
239
|
const oldCi = (await ci.exists()) && OLD_CI_COMMAND.test(await ci.text());
|
|
223
240
|
const replaced = [
|
|
@@ -227,7 +244,7 @@ export const planAdoption = async (
|
|
|
227
244
|
const steps: Step[] = [
|
|
228
245
|
...(await jsonWrite(target, "package.json", migrated.manifest)),
|
|
229
246
|
...(await jsonWrite(target, "tsconfig.json", ts.tsconfig)),
|
|
230
|
-
...
|
|
247
|
+
...bunfig.steps,
|
|
231
248
|
...(await copies(join(filesRoot, "base"), target, BASE_FILES, true)),
|
|
232
249
|
...(await copies(filesRoot, target, TOOL_FILES, true)),
|
|
233
250
|
...(await copies(filesRoot, target, STARTER_FILES, false)),
|
|
@@ -238,6 +255,6 @@ export const planAdoption = async (
|
|
|
238
255
|
return {
|
|
239
256
|
steps: await pending(target, steps),
|
|
240
257
|
lockfiles: await existing(target, LOCKFILES),
|
|
241
|
-
notes: [...migrated.notes, ...ts.notes],
|
|
258
|
+
notes: [...migrated.notes, ...ts.notes, ...bunfig.notes],
|
|
242
259
|
};
|
|
243
260
|
};
|
package/src/adopt/blocks.ts
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { isArray, isRecord } from "./json.ts";
|
|
2
|
+
import {
|
|
3
|
+
COMMENT,
|
|
4
|
+
type Entry,
|
|
5
|
+
type Path,
|
|
6
|
+
scan,
|
|
7
|
+
type Section,
|
|
8
|
+
} from "./toml-scan.ts";
|
|
9
|
+
|
|
10
|
+
interface Merge {
|
|
11
|
+
readonly lines: string[];
|
|
12
|
+
readonly own: readonly string[];
|
|
13
|
+
readonly base: unknown;
|
|
14
|
+
readonly repo: unknown;
|
|
15
|
+
readonly after: Map<number, string[]>;
|
|
16
|
+
readonly appended: string[];
|
|
17
|
+
readonly notes: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const get = (value: unknown, path: Path): unknown => {
|
|
21
|
+
let node = value;
|
|
22
|
+
for (const part of path) {
|
|
23
|
+
if (typeof part === "number") {
|
|
24
|
+
node = isArray(node) ? node[part] : undefined;
|
|
25
|
+
} else {
|
|
26
|
+
node = isRecord(node) ? node[part] : undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return node;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const asList = (value: unknown): readonly unknown[] =>
|
|
33
|
+
isArray(value) ? value : [];
|
|
34
|
+
|
|
35
|
+
const has = (list: readonly unknown[], value: unknown): boolean =>
|
|
36
|
+
list.some((item) => Bun.deepEquals(item, value));
|
|
37
|
+
|
|
38
|
+
const toml = (value: unknown): string => JSON.stringify(value);
|
|
39
|
+
|
|
40
|
+
const valuesOn = (line: string): readonly unknown[] => {
|
|
41
|
+
try {
|
|
42
|
+
// The newline keeps a trailing comment from swallowing the `]`.
|
|
43
|
+
return asList(get(Bun.TOML.parse(`v = [${line}\n]`), ["v"]));
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const insert = (merge: Merge, line: number, text: readonly string[]): void => {
|
|
50
|
+
if (text.length > 0) {
|
|
51
|
+
merge.after.set(line, [...(merge.after.get(line) ?? []), ...text]);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const raw = (merge: Merge, from: number, end: number): string =>
|
|
56
|
+
merge.own.slice(from, end + 1).join("\n");
|
|
57
|
+
|
|
58
|
+
/** The repo's lines that hold an entry the template array lacks, with their comments. */
|
|
59
|
+
const carriedLines = (
|
|
60
|
+
merge: Merge,
|
|
61
|
+
entry: Entry,
|
|
62
|
+
mine: readonly unknown[],
|
|
63
|
+
): { lines: string[]; values: unknown[] } => {
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
const values: unknown[] = [];
|
|
66
|
+
let comments: string[] = [];
|
|
67
|
+
for (const line of merge.own.slice(entry.start + 1, entry.end)) {
|
|
68
|
+
if (COMMENT.test(line)) {
|
|
69
|
+
comments.push(line);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const fresh = valuesOn(line).filter((value) => !has(mine, value));
|
|
73
|
+
if (fresh.length > 0) {
|
|
74
|
+
lines.push(...comments, line);
|
|
75
|
+
values.push(...fresh);
|
|
76
|
+
}
|
|
77
|
+
comments = [];
|
|
78
|
+
}
|
|
79
|
+
return { lines, values };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const mergeArray = (
|
|
83
|
+
merge: Merge,
|
|
84
|
+
entry: Entry,
|
|
85
|
+
at: Entry,
|
|
86
|
+
mine: readonly unknown[],
|
|
87
|
+
): void => {
|
|
88
|
+
const theirs = asList(get(merge.repo, entry.path));
|
|
89
|
+
const extra = theirs.filter((value) => !has(mine, value));
|
|
90
|
+
if (extra.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (at.start === at.end) {
|
|
94
|
+
const values = [...mine, ...extra].map((value) => toml(value));
|
|
95
|
+
merge.lines[at.start] = `${at.key} = [${values.join(", ")}]`;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const indent = /^\s*/u.exec(merge.lines[at.end - 1] ?? "")?.[0] ?? "";
|
|
99
|
+
const carried = carriedLines(merge, entry, mine);
|
|
100
|
+
const rest = extra.filter((value) => !has(carried.values, value));
|
|
101
|
+
insert(merge, at.end - 1, [
|
|
102
|
+
...carried.lines,
|
|
103
|
+
...rest.map((value) => `${indent}${toml(value)},`),
|
|
104
|
+
]);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const mergeTable = (merge: Merge, section: Section, table: Section): void => {
|
|
108
|
+
const keys = new Map(
|
|
109
|
+
table.entries.map((entry) => [JSON.stringify(entry.path), entry]),
|
|
110
|
+
);
|
|
111
|
+
const added: string[] = [];
|
|
112
|
+
for (const entry of section.entries) {
|
|
113
|
+
const mine = get(merge.base, entry.path);
|
|
114
|
+
const theirs = get(merge.repo, entry.path);
|
|
115
|
+
const at = keys.get(JSON.stringify(entry.path));
|
|
116
|
+
if (mine === undefined) {
|
|
117
|
+
added.push(raw(merge, entry.from, entry.end));
|
|
118
|
+
} else if (at !== undefined && isArray(mine) && isArray(theirs)) {
|
|
119
|
+
mergeArray(merge, entry, at, mine);
|
|
120
|
+
} else if (!Bun.deepEquals(mine, theirs)) {
|
|
121
|
+
const name = entry.path.join(".");
|
|
122
|
+
merge.notes.push(
|
|
123
|
+
`bunfig.toml: kept the template's \`${name} = ${toml(mine)}\`; the repo had \`${name} = ${toml(theirs)}\`.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
insert(merge, table.end, added);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const mergeSection = (
|
|
131
|
+
merge: Merge,
|
|
132
|
+
section: Section,
|
|
133
|
+
tables: Map<string, Section>,
|
|
134
|
+
): void => {
|
|
135
|
+
const table = tables.get(JSON.stringify(section.path));
|
|
136
|
+
const isNew = section.array
|
|
137
|
+
? !has(
|
|
138
|
+
asList(get(merge.base, section.path.slice(0, -1))),
|
|
139
|
+
get(merge.repo, section.path),
|
|
140
|
+
)
|
|
141
|
+
: table === undefined;
|
|
142
|
+
if (isNew) {
|
|
143
|
+
merge.appended.push(raw(merge, section.from, section.end));
|
|
144
|
+
} else if (!section.array && table !== undefined) {
|
|
145
|
+
mergeTable(merge, section, table);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const render = (merge: Merge): string => {
|
|
150
|
+
const out = [...(merge.after.get(-1) ?? [])];
|
|
151
|
+
for (const [index, line] of merge.lines.entries()) {
|
|
152
|
+
out.push(line, ...(merge.after.get(index) ?? []));
|
|
153
|
+
}
|
|
154
|
+
let text = out.join("\n").replace(/\n*$/u, "\n");
|
|
155
|
+
for (const block of merge.appended) {
|
|
156
|
+
text += `\n${block}\n`;
|
|
157
|
+
}
|
|
158
|
+
return text;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Writes the template's bunfig.toml, then adds what the repo's copy adds to it:
|
|
163
|
+
* each key a table lacks, each entry a template array lacks, and each table or
|
|
164
|
+
* `[[…]]` element the template lacks, with the comments right above them.
|
|
165
|
+
* Where both set a value, the template's wins, since bunfig.toml cannot
|
|
166
|
+
* extend a shared file; each such value gets a note.
|
|
167
|
+
*/
|
|
168
|
+
export const mergeBunfig = (
|
|
169
|
+
template: string,
|
|
170
|
+
current?: string,
|
|
171
|
+
): { content: string; notes: string[] } => {
|
|
172
|
+
if (current === undefined) {
|
|
173
|
+
return { content: template, notes: [] };
|
|
174
|
+
}
|
|
175
|
+
let repo: unknown;
|
|
176
|
+
try {
|
|
177
|
+
repo = Bun.TOML.parse(current);
|
|
178
|
+
} catch {
|
|
179
|
+
return {
|
|
180
|
+
content: template,
|
|
181
|
+
notes: [
|
|
182
|
+
"bunfig.toml: the repo's copy is not valid TOML, so adopt wrote the template. Move its settings over from bunfig.toml.bak.",
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const merge: Merge = {
|
|
187
|
+
lines: template.split("\n"),
|
|
188
|
+
own: current.split("\n"),
|
|
189
|
+
base: Bun.TOML.parse(template),
|
|
190
|
+
repo,
|
|
191
|
+
after: new Map(),
|
|
192
|
+
appended: [],
|
|
193
|
+
notes: [],
|
|
194
|
+
};
|
|
195
|
+
const tables = new Map(
|
|
196
|
+
scan(merge.lines).map((section) => [JSON.stringify(section.path), section]),
|
|
197
|
+
);
|
|
198
|
+
for (const section of scan(merge.own)) {
|
|
199
|
+
mergeSection(merge, section, tables);
|
|
200
|
+
}
|
|
201
|
+
return { content: render(merge), notes: merge.notes };
|
|
202
|
+
};
|
package/src/adopt/tests.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
|
+
import { listFiles } from "../files.ts";
|
|
3
4
|
import type { Step } from "./steps.ts";
|
|
4
5
|
|
|
5
6
|
const TEST_FILES = new Bun.Glob(
|
|
@@ -39,18 +40,15 @@ export const migrateTestFile = (text: string): string => {
|
|
|
39
40
|
: text;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
/** Rewrites each test file outside node_modules that is not on bun:test yet. */
|
|
43
|
+
/** Rewrites each test file outside node_modules and git's ignored files that is not on bun:test yet. */
|
|
43
44
|
export const testSteps = async (target: string): Promise<Step[]> => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const text =
|
|
45
|
+
const paths = await listFiles(target, TEST_FILES);
|
|
46
|
+
const texts = await Promise.all(
|
|
47
|
+
paths.map((path) => Bun.file(join(target, path)).text()),
|
|
48
|
+
);
|
|
49
|
+
return paths.flatMap((path, index): Step[] => {
|
|
50
|
+
const text = texts[index] ?? "";
|
|
50
51
|
const content = migrateTestFile(text);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return steps;
|
|
52
|
+
return content === text ? [] : [{ kind: "write", path, content }];
|
|
53
|
+
});
|
|
56
54
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type Path = readonly (string | number)[];
|
|
2
|
+
|
|
3
|
+
export interface Entry {
|
|
4
|
+
readonly path: Path;
|
|
5
|
+
readonly key: string;
|
|
6
|
+
/** The first line of the comments right above the key. */
|
|
7
|
+
readonly from: number;
|
|
8
|
+
readonly start: number;
|
|
9
|
+
readonly end: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Section {
|
|
13
|
+
readonly path: Path;
|
|
14
|
+
readonly array: boolean;
|
|
15
|
+
readonly from: number;
|
|
16
|
+
readonly entries: Entry[];
|
|
17
|
+
end: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HEADER = /^\s*(\[\[?)\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$/u;
|
|
21
|
+
const KEY = /^\s*([^\s#=[][^=]*?)\s*=/u;
|
|
22
|
+
export const COMMENT = /^\s*#/u;
|
|
23
|
+
|
|
24
|
+
const keyPath = (text: string): string[] =>
|
|
25
|
+
text.split(".").map((part) => part.trim().replace(/^(["'])(.*)\1$/u, "$2"));
|
|
26
|
+
|
|
27
|
+
/** How many arrays the line opens minus how many it closes, outside strings and comments. */
|
|
28
|
+
const brackets = (line: string): number => {
|
|
29
|
+
let depth = 0;
|
|
30
|
+
let quote = "";
|
|
31
|
+
for (const char of line) {
|
|
32
|
+
if (quote !== "") {
|
|
33
|
+
quote = char === quote ? "" : quote;
|
|
34
|
+
} else if (char === '"' || char === "'") {
|
|
35
|
+
quote = char;
|
|
36
|
+
} else if (char === "#") {
|
|
37
|
+
break;
|
|
38
|
+
} else {
|
|
39
|
+
depth += Number(char === "[") - Number(char === "]");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return depth;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The last line of the value that starts on `start`. */
|
|
46
|
+
const valueEnd = (
|
|
47
|
+
lines: readonly string[],
|
|
48
|
+
start: number,
|
|
49
|
+
key: string,
|
|
50
|
+
): number => {
|
|
51
|
+
let end = start;
|
|
52
|
+
let depth = brackets((lines[start] ?? "").slice(key.length));
|
|
53
|
+
while (depth > 0 && end + 1 < lines.length) {
|
|
54
|
+
end += 1;
|
|
55
|
+
depth += brackets(lines[end] ?? "");
|
|
56
|
+
}
|
|
57
|
+
return end;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const startSection = (
|
|
61
|
+
header: RegExpExecArray,
|
|
62
|
+
from: number,
|
|
63
|
+
start: number,
|
|
64
|
+
counts: Map<string, number>,
|
|
65
|
+
): Section => {
|
|
66
|
+
const name = header[2] ?? "";
|
|
67
|
+
const array = header[1] === "[[";
|
|
68
|
+
const count = counts.get(name) ?? 0;
|
|
69
|
+
counts.set(name, count + 1);
|
|
70
|
+
return {
|
|
71
|
+
path: array ? [...keyPath(name), count] : keyPath(name),
|
|
72
|
+
array,
|
|
73
|
+
from,
|
|
74
|
+
entries: [],
|
|
75
|
+
end: start,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Splits a TOML file into its root and its tables, each with the line range of every key. */
|
|
80
|
+
export const scan = (lines: readonly string[]): Section[] => {
|
|
81
|
+
let section: Section = {
|
|
82
|
+
path: [],
|
|
83
|
+
array: false,
|
|
84
|
+
from: 0,
|
|
85
|
+
entries: [],
|
|
86
|
+
end: -1,
|
|
87
|
+
};
|
|
88
|
+
const sections = [section];
|
|
89
|
+
const counts = new Map<string, number>();
|
|
90
|
+
let from = -1;
|
|
91
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
92
|
+
const line = lines[index] ?? "";
|
|
93
|
+
if (COMMENT.test(line)) {
|
|
94
|
+
from = from < 0 ? index : from;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const header = HEADER.exec(line);
|
|
98
|
+
const key = KEY.exec(line);
|
|
99
|
+
if (header !== null) {
|
|
100
|
+
section = startSection(header, from < 0 ? index : from, index, counts);
|
|
101
|
+
sections.push(section);
|
|
102
|
+
} else if (key !== null) {
|
|
103
|
+
const end = valueEnd(lines, index, key[0]);
|
|
104
|
+
section.entries.push({
|
|
105
|
+
path: [...section.path, ...keyPath(key[1] ?? "")],
|
|
106
|
+
key: key[1] ?? "",
|
|
107
|
+
from: from < 0 ? index : from,
|
|
108
|
+
start: index,
|
|
109
|
+
end,
|
|
110
|
+
});
|
|
111
|
+
section.end = end;
|
|
112
|
+
index = end;
|
|
113
|
+
}
|
|
114
|
+
from = -1;
|
|
115
|
+
}
|
|
116
|
+
return sections;
|
|
117
|
+
};
|
package/src/adopt/tsconfig.ts
CHANGED
|
@@ -23,8 +23,17 @@ const REMOVED_OPTIONS = new Set([
|
|
|
23
23
|
/** Project facts the base config cannot know. */
|
|
24
24
|
const PROJECT_OPTIONS = new Set(["jsx", "lib", "types"]);
|
|
25
25
|
const BASE_FILE = "typescript-bedrock/tsconfig.base.json";
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Build output a repo can hold: Bun's, Rust's (`cargo doc` writes JavaScript), SwiftPM's, and
|
|
28
|
+
* Gradle's (its test reports are JavaScript).
|
|
29
|
+
*/
|
|
30
|
+
const GENERATED_DIRS = [
|
|
31
|
+
"**/.build",
|
|
32
|
+
"**/build",
|
|
33
|
+
"**/coverage",
|
|
34
|
+
"**/dist",
|
|
35
|
+
"**/target",
|
|
36
|
+
];
|
|
28
37
|
const EMIT_OPTIONS = [
|
|
29
38
|
"composite",
|
|
30
39
|
"declaration",
|
package/src/files.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { $, type Glob } from "bun";
|
|
4
|
+
|
|
5
|
+
/** The files under `cwd` that match `glob`, without the ones git ignores and without node_modules. */
|
|
6
|
+
export const listFiles = async (cwd: string, glob: Glob): Promise<string[]> => {
|
|
7
|
+
const git = await $`git ls-files --cached --others --exclude-standard`
|
|
8
|
+
.cwd(cwd)
|
|
9
|
+
.nothrow()
|
|
10
|
+
.quiet();
|
|
11
|
+
const listed = (
|
|
12
|
+
git.exitCode === 0
|
|
13
|
+
? git.stdout.toString().split("\n")
|
|
14
|
+
: await Array.fromAsync(glob.scan({ cwd }))
|
|
15
|
+
).filter(
|
|
16
|
+
(path) => glob.match(path) && !path.split("/").includes("node_modules"),
|
|
17
|
+
);
|
|
18
|
+
// The index still lists a file deleted since the last commit.
|
|
19
|
+
const present = await Promise.all(
|
|
20
|
+
listed.map((path) => Bun.file(join(cwd, path)).exists()),
|
|
21
|
+
);
|
|
22
|
+
return listed.filter((_, index) => present[index] === true);
|
|
23
|
+
};
|
package/src/guard.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { $, Glob } from "bun";
|
|
5
5
|
|
|
6
6
|
import { planAdoption, PROJECT_FILES } from "./adopt/adopt.ts";
|
|
7
|
+
import { listFiles } from "./files.ts";
|
|
7
8
|
|
|
8
9
|
// Written as alternations so this file does not match itself.
|
|
9
10
|
const SUPPRESSION =
|
|
@@ -40,25 +41,6 @@ const FOREIGN_FILES = [
|
|
|
40
41
|
"yarn.lock",
|
|
41
42
|
];
|
|
42
43
|
|
|
43
|
-
const listFiles = async (cwd: string, glob: Glob): Promise<string[]> => {
|
|
44
|
-
const git = await $`git ls-files --cached --others --exclude-standard`
|
|
45
|
-
.cwd(cwd)
|
|
46
|
-
.nothrow()
|
|
47
|
-
.quiet();
|
|
48
|
-
const listed = (
|
|
49
|
-
git.exitCode === 0
|
|
50
|
-
? git.stdout.toString().split("\n")
|
|
51
|
-
: await Array.fromAsync(glob.scan({ cwd }))
|
|
52
|
-
).filter(
|
|
53
|
-
(path) => glob.match(path) && !path.split("/").includes("node_modules"),
|
|
54
|
-
);
|
|
55
|
-
// The index still lists a file deleted since the last commit.
|
|
56
|
-
const present = await Promise.all(
|
|
57
|
-
listed.map((path) => Bun.file(join(cwd, path)).exists()),
|
|
58
|
-
);
|
|
59
|
-
return listed.filter((_, index) => present[index] === true);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
44
|
export const findSuppressions = async (
|
|
63
45
|
cwd: string,
|
|
64
46
|
paths: readonly string[],
|