@homeflare/config 0.1.1 → 0.2.0
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/README.md +45 -9
- package/bunfig.toml +22 -0
- package/dist/check.d.ts +27 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +90 -0
- package/dist/check.js.map +10 -0
- package/oxlintrc.json +62 -6
- package/package.json +9 -5
- package/src/check.ts +149 -0
package/README.md
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
# @homeflare/config
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
The HomeFlare toolchain, as configuration. One place to change a rule, rather than one
|
|
4
|
+
copy per repo that drifts.
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
7
|
bun add -D @homeflare/config
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
## What it gives you
|
|
11
|
+
|
|
12
|
+
| file | how to use it |
|
|
13
|
+
| -------------------- | --------------------------------------- |
|
|
14
|
+
| `tsconfig.base.json` | `extends` — apps and Workers |
|
|
15
|
+
| `tsconfig.lib.json` | `extends` — packages that publish types |
|
|
16
|
+
| `oxlintrc.json` | `extends` — 36 rules, 6 plugins |
|
|
17
|
+
| `oxfmtrc.json` | copy to `.oxfmtrc.json` |
|
|
18
|
+
| `bunfig.toml` | copy to `bunfig.toml` |
|
|
19
|
+
|
|
10
20
|
## tsconfig
|
|
11
21
|
|
|
12
22
|
```jsonc
|
|
@@ -17,11 +27,10 @@ bun add -D @homeflare/config
|
|
|
17
27
|
{ "extends": "@homeflare/config/tsconfig.lib.json" }
|
|
18
28
|
```
|
|
19
29
|
|
|
20
|
-
`tsconfig.lib.json` adds `isolatedDeclarations` and `declaration
|
|
21
|
-
|
|
22
|
-
`--noEmit`.
|
|
30
|
+
`tsconfig.lib.json` adds `isolatedDeclarations` and `declaration`. ⛔ Both are needed
|
|
31
|
+
together — `isolatedDeclarations` alone is TS5069, even under `--noEmit`.
|
|
23
32
|
|
|
24
|
-
The base turns on `strict
|
|
33
|
+
The base turns on `strict` plus the flags that catch the most runtime bugs:
|
|
25
34
|
`noUncheckedIndexedAccess` (`arr[0]` is `T | undefined`), `exactOptionalPropertyTypes`,
|
|
26
35
|
and `noFallthroughCasesInSwitch`.
|
|
27
36
|
|
|
@@ -34,14 +43,41 @@ and `noFallthroughCasesInSwitch`.
|
|
|
34
43
|
⚠️ oxlint's `extends` takes **file paths**, not package names — there is no
|
|
35
44
|
`eslint-config-*` style resolution, so the path into `node_modules` is written out.
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
36 rules across `typescript`, `unicorn`, `oxc`, `import`, `react` and `jsx-a11y`. The ones
|
|
47
|
+
worth knowing:
|
|
48
|
+
|
|
49
|
+
- `no-console: error` — ⚠️ except where `console` _is_ the transport, as in a Worker
|
|
50
|
+
writing JSON for Workers Logs. Override it per file, not globally.
|
|
51
|
+
- `typescript/no-non-null-assertion` — `!` hides exactly what `noUncheckedIndexedAccess`
|
|
52
|
+
is trying to surface.
|
|
53
|
+
- `react/exhaustive-deps`, `jsx-a11y/click-events-have-key-events` — only bite in React
|
|
54
|
+
code; a Worker never sees them.
|
|
55
|
+
|
|
56
|
+
## oxfmt and bunfig
|
|
38
57
|
|
|
39
58
|
```sh
|
|
40
59
|
cp node_modules/@homeflare/config/oxfmtrc.json .oxfmtrc.json
|
|
60
|
+
cp node_modules/@homeflare/config/bunfig.toml bunfig.toml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
⚠️ `.oxfmtrc.json` needs the **leading dot**. Without it oxfmt silently uses its defaults,
|
|
64
|
+
and the symptom is a formatter that rewrites your quotes.
|
|
65
|
+
|
|
66
|
+
⛔ Neither format supports `extends`, so these two are copies. That means they can drift —
|
|
67
|
+
which is what `@homeflare/config/check` exists to catch.
|
|
68
|
+
|
|
69
|
+
## Keeping a project honest
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { checkProject } from '@homeflare/config/check';
|
|
73
|
+
|
|
74
|
+
const problems = await checkProject(process.cwd());
|
|
75
|
+
if (problems.length > 0) throw new Error(problems.join('\n'));
|
|
41
76
|
```
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
★ Run it from a test. It asserts a project still extends the shared tsconfig and oxlint
|
|
79
|
+
presets, and that its copied `.oxfmtrc.json` and `bunfig.toml` still match — so a project
|
|
80
|
+
that quietly diverges fails its own suite rather than drifting for six months.
|
|
45
81
|
|
|
46
82
|
## License
|
|
47
83
|
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# The HomeFlare bun defaults.
|
|
2
|
+
#
|
|
3
|
+
# ★ ADOPTION IS A COPY, NOT AN EXTEND. bunfig.toml has no `extends`, so this is the one
|
|
4
|
+
# file in the preset that a consumer copies rather than references:
|
|
5
|
+
# cp node_modules/@homeflare/config/bunfig.toml bunfig.toml
|
|
6
|
+
# ⚠️ That means it CAN drift. `homeflareConfig()` in @homeflare/config/check asserts a
|
|
7
|
+
# project's copy still matches, so drift fails a test instead of going unnoticed.
|
|
8
|
+
|
|
9
|
+
[install]
|
|
10
|
+
# ⛔ EXACT VERSIONS, ALWAYS. A caret in a manifest means two machines resolve different
|
|
11
|
+
# trees on different days, which is the drift a catalog exists to prevent.
|
|
12
|
+
exact = true
|
|
13
|
+
|
|
14
|
+
[install.cache]
|
|
15
|
+
# ★ Bun's global cache, shared across every project on the machine.
|
|
16
|
+
disable = false
|
|
17
|
+
|
|
18
|
+
[test]
|
|
19
|
+
# ⚠️ Coverage is REPORTED by default and gated per project. A repo-wide threshold here
|
|
20
|
+
# would either be too low to matter or fail every new package on day one.
|
|
21
|
+
coverage = false
|
|
22
|
+
coverageReporter = ["text"]
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assert that a project still stands on the shared config.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY THIS EXISTS. Two of the five presets — `.oxfmtrc.json` and `bunfig.toml` — have no
|
|
5
|
+
* `extends` mechanism, so adopting them means COPYING them. A copy drifts silently: the
|
|
6
|
+
* project keeps building, the rules quietly diverge, and nobody finds out until two
|
|
7
|
+
* repos disagree about how the same code should look.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ IT REPORTS, IT DOES NOT REPAIR. A checker that rewrites a project's config would
|
|
10
|
+
* overwrite a deliberate local exception, and this cannot tell the difference between
|
|
11
|
+
* drift and a decision. It names what differs; a human decides which way to move.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ Every file it reads is parsed natively — `Bun.file().json()`, `Bun.TOML.parse`. No
|
|
14
|
+
* parsing dependency reaches a consumer.
|
|
15
|
+
* ⛔ NOT JSONC. Bun's `.json()` rejects comments (measured 2026-09-15), so a project
|
|
16
|
+
* whose tsconfig carries them is reported as unreadable rather than silently skipped.
|
|
17
|
+
*/
|
|
18
|
+
/** One thing a project should fix, in the imperative. */
|
|
19
|
+
export type Problem = string;
|
|
20
|
+
/**
|
|
21
|
+
* Check a project directory. Returns an empty array when everything is in step.
|
|
22
|
+
*
|
|
23
|
+
* const problems = await checkProject(process.cwd());
|
|
24
|
+
* expect(problems).toEqual([]);
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkProject(projectDir: string): Promise<readonly Problem[]>;
|
|
27
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,yDAAyD;AACzD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAkH7B;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CASlF"}
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/check.ts
|
|
3
|
+
function stripComments(text) {
|
|
4
|
+
let out = "";
|
|
5
|
+
let inString = false;
|
|
6
|
+
let escaped = false;
|
|
7
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
8
|
+
const ch = text[i] ?? "";
|
|
9
|
+
const next = text[i + 1] ?? "";
|
|
10
|
+
if (inString) {
|
|
11
|
+
out += ch;
|
|
12
|
+
if (escaped)
|
|
13
|
+
escaped = false;
|
|
14
|
+
else if (ch === "\\")
|
|
15
|
+
escaped = true;
|
|
16
|
+
else if (ch === '"')
|
|
17
|
+
inString = false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (ch === '"') {
|
|
21
|
+
inString = true;
|
|
22
|
+
out += ch;
|
|
23
|
+
} else if (ch === "/" && next === "/") {
|
|
24
|
+
while (i < text.length && text[i] !== `
|
|
25
|
+
`)
|
|
26
|
+
i += 1;
|
|
27
|
+
out += `
|
|
28
|
+
`;
|
|
29
|
+
} else if (ch === "/" && next === "*") {
|
|
30
|
+
i += 2;
|
|
31
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
32
|
+
i += 1;
|
|
33
|
+
i += 1;
|
|
34
|
+
} else {
|
|
35
|
+
out += ch;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
async function readJson(path) {
|
|
41
|
+
const file = Bun.file(path);
|
|
42
|
+
if (!await file.exists())
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(stripComments(await file.text()));
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function presetDir() {
|
|
51
|
+
return new URL("../", import.meta.url).pathname;
|
|
52
|
+
}
|
|
53
|
+
async function checkExtends(projectDir, file, needle, problems) {
|
|
54
|
+
const config = await readJson(`${projectDir}/${file}`);
|
|
55
|
+
if (config === undefined) {
|
|
56
|
+
problems.push(`${file}: missing or unparseable (JSONC comments are not supported)`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const extended = JSON.stringify(config["extends"] ?? "");
|
|
60
|
+
const workspacePath = needle.replace("@homeflare/config/", "packages/config/");
|
|
61
|
+
if (!extended.includes(needle) && !extended.includes(workspacePath)) {
|
|
62
|
+
problems.push(`${file}: does not extend ${needle} \u2014 it currently extends ${extended}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function checkCopy(projectDir, file, parse, problems) {
|
|
66
|
+
const mine = Bun.file(`${projectDir}/${file}`);
|
|
67
|
+
if (!await mine.exists()) {
|
|
68
|
+
problems.push(`${file}: missing \u2014 copy it from @homeflare/config`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const preset = parse(await Bun.file(`${presetDir()}${file.replace(/^\./, "")}`).text());
|
|
72
|
+
const theirs = parse(await mine.text());
|
|
73
|
+
if (JSON.stringify(theirs) !== JSON.stringify(preset)) {
|
|
74
|
+
problems.push(`${file}: differs from @homeflare/config \u2014 re-copy it, or say why it differs`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function checkProject(projectDir) {
|
|
78
|
+
const problems = [];
|
|
79
|
+
await checkExtends(projectDir, "tsconfig.json", "@homeflare/config/tsconfig", problems);
|
|
80
|
+
await checkExtends(projectDir, ".oxlintrc.json", "@homeflare/config/oxlintrc", problems);
|
|
81
|
+
await checkCopy(projectDir, ".oxfmtrc.json", (t) => JSON.parse(t), problems);
|
|
82
|
+
await checkCopy(projectDir, "bunfig.toml", (t) => Bun.TOML.parse(t), problems);
|
|
83
|
+
return problems;
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
checkProject
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
//# debugId=7F6869C495728F5464756E2164756E21
|
|
90
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/check.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Assert that a project still stands on the shared config.\n *\n * ★ WHY THIS EXISTS. Two of the five presets — `.oxfmtrc.json` and `bunfig.toml` — have no\n * `extends` mechanism, so adopting them means COPYING them. A copy drifts silently: the\n * project keeps building, the rules quietly diverge, and nobody finds out until two\n * repos disagree about how the same code should look.\n *\n * ⛔ IT REPORTS, IT DOES NOT REPAIR. A checker that rewrites a project's config would\n * overwrite a deliberate local exception, and this cannot tell the difference between\n * drift and a decision. It names what differs; a human decides which way to move.\n *\n * ⚠️ Every file it reads is parsed natively — `Bun.file().json()`, `Bun.TOML.parse`. No\n * parsing dependency reaches a consumer.\n * ⛔ NOT JSONC. Bun's `.json()` rejects comments (measured 2026-09-15), so a project\n * whose tsconfig carries them is reported as unreadable rather than silently skipped.\n */\n\n/** One thing a project should fix, in the imperative. */\nexport type Problem = string;\n\ntype Json = Record<string, unknown>;\n\n/**\n * ⚠️ A tsconfig LEGITIMATELY carries comments — that is where the reasoning for a strict\n * flag lives, and stripping them to satisfy a parser would be the wrong trade. Bun's\n * `.json()` rejects them (measured 2026-09-15), so comments are removed before parsing\n * rather than treated as a fault.\n * 🔴 A NAIVE REGEX CORRUPTS THE FILE IT IS READING. Measured while writing this: a\n * `(^|[^:\"])//.*$` strip mangled `\"./node_modules/oxlint/...\"` — the `//` inside a\n * STRING — turning valid JSON into a parse error reported as \"JSONC not supported\".\n * So this walks the text and tracks whether it is inside a string, which is the\n * smallest correct thing; a real JSONC parser would be a dependency every consumer\n * inherits for one check.\n */\nfunction stripComments(text: string): string {\n let out = '';\n let inString = false;\n let escaped = false;\n\n for (let i = 0; i < text.length; i += 1) {\n const ch = text[i] ?? '';\n const next = text[i + 1] ?? '';\n\n if (inString) {\n out += ch;\n if (escaped) escaped = false;\n else if (ch === '\\\\') escaped = true;\n else if (ch === '\"') inString = false;\n continue;\n }\n\n if (ch === '\"') {\n inString = true;\n out += ch;\n } else if (ch === '/' && next === '/') {\n while (i < text.length && text[i] !== '\\n') i += 1;\n out += '\\n';\n } else if (ch === '/' && next === '*') {\n i += 2;\n while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;\n i += 1;\n } else {\n out += ch;\n }\n }\n\n return out;\n}\n\nasync function readJson(path: string): Promise<Json | undefined> {\n const file = Bun.file(path);\n if (!(await file.exists())) return undefined;\n try {\n return JSON.parse(stripComments(await file.text())) as Json;\n } catch {\n return undefined;\n }\n}\n\n/** Where this package's own presets live, however it was installed. */\nfunction presetDir(): string {\n return new URL('../', import.meta.url).pathname;\n}\n\nasync function checkExtends(\n projectDir: string,\n file: string,\n needle: string,\n problems: Problem[],\n): Promise<void> {\n const config = await readJson(`${projectDir}/${file}`);\n if (config === undefined) {\n problems.push(`${file}: missing or unparseable (JSONC comments are not supported)`);\n return;\n }\n\n const extended = JSON.stringify(config['extends'] ?? '');\n\n // ⚠️ A WORKSPACE PATH COUNTS. Inside this repo the presets are referenced as\n // `./packages/config/...` rather than by package name, and reporting that as drift\n // would make the check fail in the one repo that defines the config.\n const workspacePath = needle.replace('@homeflare/config/', 'packages/config/');\n if (!extended.includes(needle) && !extended.includes(workspacePath)) {\n problems.push(`${file}: does not extend ${needle} — it currently extends ${extended}`);\n }\n}\n\n/**\n * Compare a copied file against the preset, ignoring formatting.\n * ⚠️ Structural comparison, not text: a project that reformats its copy has not drifted,\n * and reporting that would train people to ignore this check.\n */\nasync function checkCopy(\n projectDir: string,\n file: string,\n parse: (text: string) => unknown,\n problems: Problem[],\n): Promise<void> {\n const mine = Bun.file(`${projectDir}/${file}`);\n if (!(await mine.exists())) {\n problems.push(`${file}: missing — copy it from @homeflare/config`);\n return;\n }\n\n const preset = parse(await Bun.file(`${presetDir()}${file.replace(/^\\./, '')}`).text());\n const theirs = parse(await mine.text());\n\n if (JSON.stringify(theirs) !== JSON.stringify(preset)) {\n problems.push(`${file}: differs from @homeflare/config — re-copy it, or say why it differs`);\n }\n}\n\n/**\n * Check a project directory. Returns an empty array when everything is in step.\n *\n * const problems = await checkProject(process.cwd());\n * expect(problems).toEqual([]);\n */\nexport async function checkProject(projectDir: string): Promise<readonly Problem[]> {\n const problems: Problem[] = [];\n\n await checkExtends(projectDir, 'tsconfig.json', '@homeflare/config/tsconfig', problems);\n await checkExtends(projectDir, '.oxlintrc.json', '@homeflare/config/oxlintrc', problems);\n await checkCopy(projectDir, '.oxfmtrc.json', (t) => JSON.parse(t) as unknown, problems);\n await checkCopy(projectDir, 'bunfig.toml', (t) => Bun.TOML.parse(t), problems);\n\n return problems;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "7F6869C495728F5464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/oxlintrc.json
CHANGED
|
@@ -1,19 +1,75 @@
|
|
|
1
1
|
{
|
|
2
|
-
"plugins": ["typescript", "unicorn", "oxc", "import"],
|
|
2
|
+
"plugins": ["typescript", "unicorn", "oxc", "import", "react", "jsx-a11y"],
|
|
3
3
|
"categories": {
|
|
4
4
|
"correctness": "error",
|
|
5
|
-
"suspicious": "
|
|
6
|
-
"
|
|
5
|
+
"suspicious": "off",
|
|
6
|
+
"pedantic": "off",
|
|
7
|
+
"style": "off",
|
|
8
|
+
"restriction": "off",
|
|
9
|
+
"nursery": "off"
|
|
7
10
|
},
|
|
8
11
|
"env": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
12
|
+
"es2024": true,
|
|
13
|
+
"node": true,
|
|
14
|
+
"browser": true,
|
|
15
|
+
"serviceworker": true
|
|
11
16
|
},
|
|
12
17
|
"ignorePatterns": ["dist/**", "coverage/**"],
|
|
13
18
|
"rules": {
|
|
19
|
+
"no-unused-vars": [
|
|
20
|
+
"error",
|
|
21
|
+
{
|
|
22
|
+
"args": "none",
|
|
23
|
+
"caughtErrors": "none",
|
|
24
|
+
"varsIgnorePattern": "^_",
|
|
25
|
+
"ignoreRestSiblings": true
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"import/default": "off",
|
|
29
|
+
"import/no-unassigned-import": "off",
|
|
30
|
+
"no-underscore-dangle": "off",
|
|
31
|
+
"unicorn/no-empty-file": "off",
|
|
32
|
+
"eqeqeq": [
|
|
33
|
+
"error",
|
|
34
|
+
"always",
|
|
35
|
+
{
|
|
36
|
+
"null": "ignore"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
14
39
|
"no-console": "error",
|
|
15
|
-
"
|
|
40
|
+
"no-control-regex": "error",
|
|
41
|
+
"no-fallthrough": "error",
|
|
42
|
+
"no-shadow-restricted-names": "error",
|
|
43
|
+
"no-template-curly-in-string": "error",
|
|
16
44
|
"typescript/no-explicit-any": "error",
|
|
45
|
+
"default-param-last": "error",
|
|
46
|
+
"no-param-reassign": "error",
|
|
47
|
+
"operator-assignment": "error",
|
|
48
|
+
"prefer-const": "error",
|
|
49
|
+
"prefer-template": "error",
|
|
50
|
+
"typescript/no-non-null-assertion": "error",
|
|
51
|
+
"typescript/no-extraneous-class": "error",
|
|
52
|
+
"typescript/no-this-alias": "error",
|
|
53
|
+
"unicorn/no-array-for-each": "warn",
|
|
54
|
+
"unicorn/prefer-array-flat-map": "error",
|
|
55
|
+
"oxc/no-accumulating-spread": "error",
|
|
56
|
+
"react/no-danger": "error",
|
|
57
|
+
"react/button-has-type": "error",
|
|
58
|
+
"react/exhaustive-deps": "error",
|
|
59
|
+
"react/react-in-jsx-scope": "off",
|
|
60
|
+
"jsx-a11y/click-events-have-key-events": "error",
|
|
61
|
+
"jsx-a11y/prefer-tag-over-role": "off",
|
|
62
|
+
"react/set-state-in-effect": "off",
|
|
63
|
+
"typescript/dot-notation": "off",
|
|
64
|
+
"no-shadow": "off",
|
|
65
|
+
"unicorn/consistent-function-scoping": "off",
|
|
66
|
+
"typescript/consistent-type-imports": "error",
|
|
67
|
+
"sort-imports": [
|
|
68
|
+
"error",
|
|
69
|
+
{
|
|
70
|
+
"ignoreDeclarationSort": true
|
|
71
|
+
}
|
|
72
|
+
],
|
|
17
73
|
"import/no-default-export": "error"
|
|
18
74
|
},
|
|
19
75
|
"overrides": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared tsconfig, oxlint and oxfmt configuration for HomeFlare projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Timothy Schneider",
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
"directory": "packages/config"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src",
|
|
13
15
|
"tsconfig.base.json",
|
|
14
16
|
"tsconfig.lib.json",
|
|
15
17
|
"oxlintrc.json",
|
|
16
18
|
"oxfmtrc.json",
|
|
19
|
+
"bunfig.toml",
|
|
17
20
|
"README.md",
|
|
18
21
|
"LICENSE"
|
|
19
22
|
],
|
|
@@ -23,13 +26,14 @@
|
|
|
23
26
|
"./tsconfig.lib.json": "./tsconfig.lib.json",
|
|
24
27
|
"./oxlintrc.json": "./oxlintrc.json",
|
|
25
28
|
"./oxfmtrc.json": "./oxfmtrc.json",
|
|
29
|
+
"./bunfig.toml": "./bunfig.toml",
|
|
30
|
+
"./check": {
|
|
31
|
+
"types": "./dist/check.d.ts",
|
|
32
|
+
"default": "./dist/check.js"
|
|
33
|
+
},
|
|
26
34
|
"./package.json": "./package.json"
|
|
27
35
|
},
|
|
28
36
|
"publishConfig": {
|
|
29
37
|
"access": "public"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "echo 'config: nothing to build'",
|
|
33
|
-
"clean": "echo 'config: nothing to clean'"
|
|
34
38
|
}
|
|
35
39
|
}
|
package/src/check.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assert that a project still stands on the shared config.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY THIS EXISTS. Two of the five presets — `.oxfmtrc.json` and `bunfig.toml` — have no
|
|
5
|
+
* `extends` mechanism, so adopting them means COPYING them. A copy drifts silently: the
|
|
6
|
+
* project keeps building, the rules quietly diverge, and nobody finds out until two
|
|
7
|
+
* repos disagree about how the same code should look.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ IT REPORTS, IT DOES NOT REPAIR. A checker that rewrites a project's config would
|
|
10
|
+
* overwrite a deliberate local exception, and this cannot tell the difference between
|
|
11
|
+
* drift and a decision. It names what differs; a human decides which way to move.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ Every file it reads is parsed natively — `Bun.file().json()`, `Bun.TOML.parse`. No
|
|
14
|
+
* parsing dependency reaches a consumer.
|
|
15
|
+
* ⛔ NOT JSONC. Bun's `.json()` rejects comments (measured 2026-09-15), so a project
|
|
16
|
+
* whose tsconfig carries them is reported as unreadable rather than silently skipped.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** One thing a project should fix, in the imperative. */
|
|
20
|
+
export type Problem = string;
|
|
21
|
+
|
|
22
|
+
type Json = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ⚠️ A tsconfig LEGITIMATELY carries comments — that is where the reasoning for a strict
|
|
26
|
+
* flag lives, and stripping them to satisfy a parser would be the wrong trade. Bun's
|
|
27
|
+
* `.json()` rejects them (measured 2026-09-15), so comments are removed before parsing
|
|
28
|
+
* rather than treated as a fault.
|
|
29
|
+
* 🔴 A NAIVE REGEX CORRUPTS THE FILE IT IS READING. Measured while writing this: a
|
|
30
|
+
* `(^|[^:"])//.*$` strip mangled `"./node_modules/oxlint/..."` — the `//` inside a
|
|
31
|
+
* STRING — turning valid JSON into a parse error reported as "JSONC not supported".
|
|
32
|
+
* So this walks the text and tracks whether it is inside a string, which is the
|
|
33
|
+
* smallest correct thing; a real JSONC parser would be a dependency every consumer
|
|
34
|
+
* inherits for one check.
|
|
35
|
+
*/
|
|
36
|
+
function stripComments(text: string): string {
|
|
37
|
+
let out = '';
|
|
38
|
+
let inString = false;
|
|
39
|
+
let escaped = false;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
42
|
+
const ch = text[i] ?? '';
|
|
43
|
+
const next = text[i + 1] ?? '';
|
|
44
|
+
|
|
45
|
+
if (inString) {
|
|
46
|
+
out += ch;
|
|
47
|
+
if (escaped) escaped = false;
|
|
48
|
+
else if (ch === '\\') escaped = true;
|
|
49
|
+
else if (ch === '"') inString = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (ch === '"') {
|
|
54
|
+
inString = true;
|
|
55
|
+
out += ch;
|
|
56
|
+
} else if (ch === '/' && next === '/') {
|
|
57
|
+
while (i < text.length && text[i] !== '\n') i += 1;
|
|
58
|
+
out += '\n';
|
|
59
|
+
} else if (ch === '/' && next === '*') {
|
|
60
|
+
i += 2;
|
|
61
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
|
|
62
|
+
i += 1;
|
|
63
|
+
} else {
|
|
64
|
+
out += ch;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readJson(path: string): Promise<Json | undefined> {
|
|
72
|
+
const file = Bun.file(path);
|
|
73
|
+
if (!(await file.exists())) return undefined;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(stripComments(await file.text())) as Json;
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Where this package's own presets live, however it was installed. */
|
|
82
|
+
function presetDir(): string {
|
|
83
|
+
return new URL('../', import.meta.url).pathname;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function checkExtends(
|
|
87
|
+
projectDir: string,
|
|
88
|
+
file: string,
|
|
89
|
+
needle: string,
|
|
90
|
+
problems: Problem[],
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const config = await readJson(`${projectDir}/${file}`);
|
|
93
|
+
if (config === undefined) {
|
|
94
|
+
problems.push(`${file}: missing or unparseable (JSONC comments are not supported)`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const extended = JSON.stringify(config['extends'] ?? '');
|
|
99
|
+
|
|
100
|
+
// ⚠️ A WORKSPACE PATH COUNTS. Inside this repo the presets are referenced as
|
|
101
|
+
// `./packages/config/...` rather than by package name, and reporting that as drift
|
|
102
|
+
// would make the check fail in the one repo that defines the config.
|
|
103
|
+
const workspacePath = needle.replace('@homeflare/config/', 'packages/config/');
|
|
104
|
+
if (!extended.includes(needle) && !extended.includes(workspacePath)) {
|
|
105
|
+
problems.push(`${file}: does not extend ${needle} — it currently extends ${extended}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Compare a copied file against the preset, ignoring formatting.
|
|
111
|
+
* ⚠️ Structural comparison, not text: a project that reformats its copy has not drifted,
|
|
112
|
+
* and reporting that would train people to ignore this check.
|
|
113
|
+
*/
|
|
114
|
+
async function checkCopy(
|
|
115
|
+
projectDir: string,
|
|
116
|
+
file: string,
|
|
117
|
+
parse: (text: string) => unknown,
|
|
118
|
+
problems: Problem[],
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const mine = Bun.file(`${projectDir}/${file}`);
|
|
121
|
+
if (!(await mine.exists())) {
|
|
122
|
+
problems.push(`${file}: missing — copy it from @homeflare/config`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const preset = parse(await Bun.file(`${presetDir()}${file.replace(/^\./, '')}`).text());
|
|
127
|
+
const theirs = parse(await mine.text());
|
|
128
|
+
|
|
129
|
+
if (JSON.stringify(theirs) !== JSON.stringify(preset)) {
|
|
130
|
+
problems.push(`${file}: differs from @homeflare/config — re-copy it, or say why it differs`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check a project directory. Returns an empty array when everything is in step.
|
|
136
|
+
*
|
|
137
|
+
* const problems = await checkProject(process.cwd());
|
|
138
|
+
* expect(problems).toEqual([]);
|
|
139
|
+
*/
|
|
140
|
+
export async function checkProject(projectDir: string): Promise<readonly Problem[]> {
|
|
141
|
+
const problems: Problem[] = [];
|
|
142
|
+
|
|
143
|
+
await checkExtends(projectDir, 'tsconfig.json', '@homeflare/config/tsconfig', problems);
|
|
144
|
+
await checkExtends(projectDir, '.oxlintrc.json', '@homeflare/config/oxlintrc', problems);
|
|
145
|
+
await checkCopy(projectDir, '.oxfmtrc.json', (t) => JSON.parse(t) as unknown, problems);
|
|
146
|
+
await checkCopy(projectDir, 'bunfig.toml', (t) => Bun.TOML.parse(t), problems);
|
|
147
|
+
|
|
148
|
+
return problems;
|
|
149
|
+
}
|