@jterrazz/typescript 9.3.0 → 10.1.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 +20 -16
- package/bin/commands/check.sh +387 -146
- package/bin/find-tsc.sh +30 -0
- package/bin/typescript.sh +79 -1
- package/lib/check-architecture.js +89 -0
- package/lib/check-baseline.js +144 -0
- package/lib/check-docs.js +4 -3
- package/lib/check-drift.js +209 -0
- package/lib/check-gitignore.js +4 -4
- package/lib/check-markdown.js +279 -0
- package/lib/check-names.js +125 -0
- package/lib/check-publish.js +150 -0
- package/lib/check-secrets.js +115 -0
- package/lib/check-suppressions.js +355 -0
- package/lib/doctor.js +185 -0
- package/lib/entry-points.js +91 -0
- package/lib/merge-knip-config.js +57 -25
- package/lib/tracked-files.js +165 -0
- package/lib/unsafe-fixers.js +25 -0
- package/lib/workspace-members.js +5 -6
- package/package.json +36 -13
- package/presets/oxfmt/index.js +49 -5
- package/presets/oxlint/profiles/astro.js +10 -0
- package/presets/oxlint/profiles/bun.js +7 -0
- package/presets/oxlint/profiles/expo.js +7 -0
- package/presets/oxlint/profiles/library.js +16 -0
- package/presets/oxlint/profiles/next.js +7 -0
- package/presets/oxlint/profiles/node.js +7 -0
- package/presets/oxlint/profiles/react.js +7 -0
- package/presets/prettier/astro.json +6 -0
- package/presets/tsconfig/astro.json +25 -0
- package/presets/tsconfig/expo.json +16 -6
- package/presets/tsconfig/library.json +17 -0
- package/presets/tsconfig/next.json +12 -2
- package/presets/tsconfig/node.json +18 -4
- package/presets/tsconfig/react.json +33 -0
- package/presets/tsdown/build.d.ts +13 -0
- package/presets/tsdown/bundle.d.ts +13 -0
- package/presets/tsdown/bundle.js +10 -1
- package/rules/README.md +23 -0
- package/rules/_contract.js +207 -0
- package/rules/_contract.test.ts +81 -0
- package/rules/a11y.js +51 -0
- package/rules/architecture/hexagonal.js +56 -0
- package/rules/architecture/layers.js +75 -0
- package/rules/astro.js +56 -0
- package/rules/bundler.js +19 -0
- package/rules/catalog.js +166 -0
- package/rules/catalog.test.ts +98 -0
- package/rules/compile.js +125 -0
- package/rules/core/eslint.js +234 -0
- package/rules/core/import.js +117 -0
- package/rules/core/jsdoc.js +52 -0
- package/rules/core/node.js +36 -0
- package/rules/core/oxc.js +54 -0
- package/rules/core/promise.js +39 -0
- package/rules/core/typescript.js +223 -0
- package/rules/core/unicorn.js +210 -0
- package/rules/next.js +53 -0
- package/rules/profiles.js +95 -0
- package/rules/react-native.js +48 -0
- package/rules/react.js +155 -0
- package/rules/sorted.js +41 -0
- package/rules/vitest.js +178 -0
- package/src/docs.d.ts +4 -4
- package/src/docs.js +75 -57
- package/src/docs.test.ts +43 -31
- package/src/index.d.ts +14 -9
- package/src/index.js +17 -8
- package/src/oxfmt.d.ts +15 -2
- package/src/oxfmt.test.ts +10 -0
- package/src/oxlint.d.ts +59 -10
- package/src/oxlint.js +36 -50
- package/src/oxlint.test.ts +82 -28
- package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
- package/presets/oxlint/architectures/hexagonal.js +0 -13
- package/presets/oxlint/base.js +0 -145
- package/presets/oxlint/expo.js +0 -36
- package/presets/oxlint/next.js +0 -43
- package/presets/oxlint/node.js +0 -14
- package/presets/oxlint/plugins/codestyle.js +0 -231
package/lib/merge-knip-config.js
CHANGED
|
@@ -39,29 +39,26 @@ import { workspaceMembers } from './workspace-members.js';
|
|
|
39
39
|
* alone — a comment marker in a glob (`ignore: ["**\/*.js"]`) is data.
|
|
40
40
|
*/
|
|
41
41
|
function withoutComments(text) {
|
|
42
|
-
let output = ''
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
let output = '';
|
|
43
|
+
let index = 0;
|
|
44
|
+
let inString = false;
|
|
45
45
|
|
|
46
46
|
while (index < text.length) {
|
|
47
47
|
const char = text[index];
|
|
48
48
|
|
|
49
49
|
if (inString) {
|
|
50
|
-
const
|
|
51
|
-
output +=
|
|
52
|
-
inString =
|
|
53
|
-
index
|
|
50
|
+
const step = inStringStep(text, index);
|
|
51
|
+
output += step.kept;
|
|
52
|
+
inString = step.open;
|
|
53
|
+
index = step.next;
|
|
54
54
|
} else if (char === '"') {
|
|
55
55
|
inString = true;
|
|
56
56
|
output += char;
|
|
57
57
|
index += 1;
|
|
58
|
-
} else if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} else if (char === '/' && text[index + 1] === '*') {
|
|
63
|
-
const end = text.indexOf('*/', index + 2);
|
|
64
|
-
index = end === -1 ? text.length : end + 2;
|
|
58
|
+
} else if (opens(text, index, '//')) {
|
|
59
|
+
index = afterLine(text, index);
|
|
60
|
+
} else if (opens(text, index, '/*')) {
|
|
61
|
+
index = afterBlock(text, index);
|
|
65
62
|
} else {
|
|
66
63
|
output += char;
|
|
67
64
|
index += 1;
|
|
@@ -71,6 +68,41 @@ function withoutComments(text) {
|
|
|
71
68
|
return output;
|
|
72
69
|
}
|
|
73
70
|
|
|
71
|
+
/**
|
|
72
|
+
* One step of a scan already inside a string literal: what it keeps, whether
|
|
73
|
+
* the literal is still `open` after it, and where the scan resumes. A backslash
|
|
74
|
+
* carries the character behind it, so an escaped quote never closes anything.
|
|
75
|
+
*/
|
|
76
|
+
function inStringStep(text, index) {
|
|
77
|
+
const char = text[index];
|
|
78
|
+
const escaped = char === '\\';
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
kept: escaped ? char + (text[index + 1] ?? '') : char,
|
|
82
|
+
next: index + (escaped ? 2 : 1),
|
|
83
|
+
open: escaped || char !== '"',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether a two-character marker opens at `index`. */
|
|
88
|
+
function opens(text, index, marker) {
|
|
89
|
+
return text.startsWith(marker, index);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Where the scan resumes past a `//` comment: the newline that ends it. */
|
|
93
|
+
function afterLine(text, index) {
|
|
94
|
+
const end = text.indexOf('\n', index);
|
|
95
|
+
|
|
96
|
+
return end === -1 ? text.length : end;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Where the scan resumes past a block comment: after its closing marker. */
|
|
100
|
+
function afterBlock(text, index) {
|
|
101
|
+
const end = text.indexOf('*/', index + 2);
|
|
102
|
+
|
|
103
|
+
return end === -1 ? text.length : end + 2;
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
/** Whether the comma at `index` is the last one of its collection — `["a",]`. */
|
|
75
107
|
function closesCollection(text, index) {
|
|
76
108
|
if (text[index] !== ',') {
|
|
@@ -87,19 +119,19 @@ function closesCollection(text, index) {
|
|
|
87
119
|
|
|
88
120
|
/** Drops a comma that closes its collection, outside string literals. */
|
|
89
121
|
function withoutTrailingCommas(text) {
|
|
90
|
-
let output = ''
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
let output = '';
|
|
123
|
+
let index = 0;
|
|
124
|
+
let inString = false;
|
|
93
125
|
|
|
94
126
|
while (index < text.length) {
|
|
95
127
|
const char = text[index];
|
|
96
128
|
const closes = closesCollection(text, index);
|
|
97
129
|
|
|
98
130
|
if (inString) {
|
|
99
|
-
const
|
|
100
|
-
output +=
|
|
101
|
-
inString =
|
|
102
|
-
index
|
|
131
|
+
const step = inStringStep(text, index);
|
|
132
|
+
output += step.kept;
|
|
133
|
+
inString = step.open;
|
|
134
|
+
index = step.next;
|
|
103
135
|
} else {
|
|
104
136
|
inString = char === '"';
|
|
105
137
|
output += closes ? '' : char;
|
|
@@ -115,8 +147,8 @@ function withoutTrailingCommas(text) {
|
|
|
115
147
|
* package's tree, and two token classes do not earn a dependency.
|
|
116
148
|
*/
|
|
117
149
|
function readJsonc(path) {
|
|
118
|
-
const document = readFileSync(path, 'utf8')
|
|
119
|
-
|
|
150
|
+
const document = readFileSync(path, 'utf8');
|
|
151
|
+
const json = withoutTrailingCommas(withoutComments(document));
|
|
120
152
|
|
|
121
153
|
return JSON.parse(json);
|
|
122
154
|
}
|
|
@@ -302,7 +334,7 @@ for (const dep of devDeps) {
|
|
|
302
334
|
for (const dep of [...prodDeps, ...devDeps]) {
|
|
303
335
|
// @scope/sub-package when the unscoped root is a peer or dependency
|
|
304
336
|
// E.g. @hono/node-server → hono
|
|
305
|
-
const scopeMatch =
|
|
337
|
+
const scopeMatch = /^@(?<scope>[^/]+)\//u.exec(dep);
|
|
306
338
|
if (
|
|
307
339
|
scopeMatch &&
|
|
308
340
|
(peerDeps.includes(scopeMatch.groups.scope) ||
|
|
@@ -314,7 +346,7 @@ for (const dep of [...prodDeps, ...devDeps]) {
|
|
|
314
346
|
|
|
315
347
|
// <parent>-plugin-* or <parent>-preset-* when <parent> is installed
|
|
316
348
|
// E.g. vitepress-plugin-llms → vitepress
|
|
317
|
-
const pluginMatch =
|
|
349
|
+
const pluginMatch = /^(?<parent>.+?)-(?:plugin|preset|transformer|loader)-/u.exec(dep);
|
|
318
350
|
if (pluginMatch && allDepNames.includes(pluginMatch.groups.parent)) {
|
|
319
351
|
autoIgnoreDeps.push(dep);
|
|
320
352
|
continue;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a project would carry into a commit — the one sweep every
|
|
3
|
+
* tree-reading gate starts from.
|
|
4
|
+
*
|
|
5
|
+
* `git ls-files --cached --others --exclude-standard` is the answer wherever
|
|
6
|
+
* there is a git tree: it names what is tracked AND what the next `git add`
|
|
7
|
+
* would track, so a file fails a gate before it is committed, not after. A
|
|
8
|
+
* directory git knows nothing about — a fixture the spec runner copied, a tree
|
|
9
|
+
* nobody has `git init`ed — is walked instead, because a gate that goes silent
|
|
10
|
+
* outside git is a gate that passes on everything.
|
|
11
|
+
*
|
|
12
|
+
* The sweep REFUSES an answer that is empty while the tree is not. A read that
|
|
13
|
+
* silently sees nothing turns every rule above it vacuously green, which is the
|
|
14
|
+
* one failure mode a gate must never have — and a genuinely bare directory,
|
|
15
|
+
* which the sandboxes of `specs/cli/` build, is told apart from it by looking.
|
|
16
|
+
*
|
|
17
|
+
* It also refuses to answer for the GROUND a spec stands on. `_fixtures/`,
|
|
18
|
+
* `_expected/`, `_stubs/` and `_binary/` hold a tree deliberately broken, a
|
|
19
|
+
* golden held byte for byte, a fake system and a build — each belonging to the
|
|
20
|
+
* scenario that mounts it, not to the project carrying it. A gate judges that
|
|
21
|
+
* tree where the scenario runs it, on its own ground, and never twice.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import { lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { join, matchesGlob } from 'node:path';
|
|
27
|
+
|
|
28
|
+
/** Never source, never prose: what no gate of this toolchain has a question about. */
|
|
29
|
+
const UNSWEPT = new Set([
|
|
30
|
+
'.artifacts',
|
|
31
|
+
'.git',
|
|
32
|
+
'.next',
|
|
33
|
+
'.turbo',
|
|
34
|
+
'.vite',
|
|
35
|
+
'coverage',
|
|
36
|
+
'dist',
|
|
37
|
+
'node_modules',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/** Extensions whose bytes are not text — reading them for patterns is noise. */
|
|
41
|
+
const BINARY = /\.(?:avif|bin|gif|ico|jpeg|jpg|lock|mp4|pdf|png|svg|ttf|webp|woff2?|zip)$/iu;
|
|
42
|
+
|
|
43
|
+
/** Every path git carries or would carry, or null when there is no git tree here. */
|
|
44
|
+
function gitSweep(root) {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync(
|
|
47
|
+
'git',
|
|
48
|
+
['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
|
|
49
|
+
{ cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
50
|
+
)
|
|
51
|
+
.split('\0')
|
|
52
|
+
.filter((path) => path !== '');
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The same listing, derived from the filesystem, for a tree git does not know. */
|
|
59
|
+
function walkSweep(root) {
|
|
60
|
+
const found = [];
|
|
61
|
+
|
|
62
|
+
function walk(relativePath) {
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = readdirSync(join(root, relativePath), { withFileTypes: true });
|
|
66
|
+
} catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const entry of entries.toSorted((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
71
|
+
if (UNSWEPT.has(entry.name)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const child = relativePath === '' ? entry.name : `${relativePath}/${entry.name}`;
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
walk(child);
|
|
77
|
+
} else if (entry.isFile()) {
|
|
78
|
+
found.push(child);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
walk('');
|
|
84
|
+
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Whether anything the gates would ever read sits at the root at all. */
|
|
89
|
+
function holdsSomething(root) {
|
|
90
|
+
try {
|
|
91
|
+
return readdirSync(root).some((name) => !UNSWEPT.has(name));
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The ground a spec stands on, by the four names `@jterrazz/test` 14 gives it.
|
|
99
|
+
* `_common/` is deliberately absent: it is the project's own shared code, and
|
|
100
|
+
* the marker there says "of the row", not "not mine".
|
|
101
|
+
*/
|
|
102
|
+
const GROUND = new Set(['_binary', '_expected', '_fixtures', '_stubs']);
|
|
103
|
+
|
|
104
|
+
/** Whether a path lies under the ground a scenario mounts rather than in the project. */
|
|
105
|
+
const isGround = (path) => path.split('/').some((segment) => GROUND.has(segment));
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The sweep, filtered by what the run was told to overlook. `ignorePatterns`
|
|
109
|
+
* carries the `--ignore-pattern` globs `check` received, so one flag answers
|
|
110
|
+
* for the linter and for every gate that reads the same tree.
|
|
111
|
+
*/
|
|
112
|
+
export function trackedFiles(root = '.', { ignorePatterns = [] } = {}) {
|
|
113
|
+
const swept = gitSweep(root) ?? walkSweep(root);
|
|
114
|
+
|
|
115
|
+
if (swept.length === 0) {
|
|
116
|
+
if (holdsSomething(root)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`nothing to read under ${root} — a gate sweeping no file passes on every file`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return swept
|
|
126
|
+
.filter((path) => !path.split('/').some((segment) => UNSWEPT.has(segment)))
|
|
127
|
+
.filter((path) => !isGround(path))
|
|
128
|
+
.filter((path) => !ignorePatterns.some((pattern) => matchesGlob(path, pattern)))
|
|
129
|
+
.toSorted((left, right) => (left < right ? -1 : 1));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A tracked file's text, or null when there is no text to read: bytes, a file
|
|
134
|
+
* too large to be prose or source, one that is gone, or a SYMLINK — whose
|
|
135
|
+
* content belongs to its target, and is judged there once instead of twice.
|
|
136
|
+
*/
|
|
137
|
+
export function readText(root, path) {
|
|
138
|
+
if (BINARY.test(path)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const stats = lstatSync(join(root, path));
|
|
144
|
+
if (stats.isSymbolicLink() || stats.size > 1_000_000) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const text = readFileSync(join(root, path), 'utf8');
|
|
148
|
+
|
|
149
|
+
return text.includes('\0') ? null : text;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The `--ignore-pattern` globs of an argument list, in the order they arrived. */
|
|
156
|
+
export function ignorePatternsOf(argv) {
|
|
157
|
+
const patterns = [];
|
|
158
|
+
for (const [index, argument] of argv.entries()) {
|
|
159
|
+
if (argument === '--ignore-pattern' && argv[index + 1] !== undefined) {
|
|
160
|
+
patterns.push(argv[index + 1]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return patterns;
|
|
165
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The rules `fix` must not let oxlint rewrite, as the flags that switch them
|
|
5
|
+
* off for one run: `--allow <rule>` per rule, one per line.
|
|
6
|
+
*
|
|
7
|
+
* A fixer that changes MEANING cannot be applied unattended — it turns working
|
|
8
|
+
* code into code that does not compile, or into code that claims something
|
|
9
|
+
* else. The rules are marked in the manifest (`unsafeFix()`), this prints them
|
|
10
|
+
* for the shell, and the oxlint pass of `fix` passes them through. Check mode
|
|
11
|
+
* keeps every one of them armed: the diagnostic is still owed an answer, from
|
|
12
|
+
* a human ([Quality checks](../docs/06-quality-checks.md)).
|
|
13
|
+
*
|
|
14
|
+
* Usage: node unsafe-fixers.js
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { stdout } from 'node:process';
|
|
18
|
+
|
|
19
|
+
import { unsafeFixers } from '../rules/catalog.js';
|
|
20
|
+
|
|
21
|
+
if (import.meta.main) {
|
|
22
|
+
for (const { rule } of unsafeFixers()) {
|
|
23
|
+
stdout.write(`--allow\n${rule}\n`);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/lib/workspace-members.js
CHANGED
|
@@ -29,19 +29,18 @@
|
|
|
29
29
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
30
30
|
import { join, relative, resolve, sep } from 'node:path';
|
|
31
31
|
import { argv, stdout } from 'node:process';
|
|
32
|
-
import { fileURLToPath } from 'node:url';
|
|
33
32
|
|
|
34
33
|
const SKIPPED = new Set(['dist', 'node_modules']);
|
|
35
34
|
|
|
36
35
|
/** A glob over path segments — `*` stops at a separator, `**` does not. */
|
|
37
36
|
function toPattern(glob) {
|
|
38
37
|
const escaped = glob
|
|
39
|
-
.replaceAll(/[.+^${}()|[\]\\]/
|
|
38
|
+
.replaceAll(/[.+^${}()|[\]\\]/gu, String.raw`\$&`)
|
|
40
39
|
.replaceAll('**', ' ')
|
|
41
40
|
.replaceAll('*', '[^/]*')
|
|
42
41
|
.replaceAll(' ', '.*');
|
|
43
42
|
|
|
44
|
-
return new RegExp(`^${escaped}
|
|
43
|
+
return new RegExp(`^${escaped}$`, 'u');
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
/** The `workspaces` globs of a package.json, in either declared form. */
|
|
@@ -70,7 +69,7 @@ export function workspaceMembers(root = '.') {
|
|
|
70
69
|
const absolute = resolve(root);
|
|
71
70
|
const patterns = declaredGlobs(absolute)
|
|
72
71
|
.filter((glob) => typeof glob === 'string' && !glob.startsWith('!'))
|
|
73
|
-
.map((glob) => toPattern(glob.replace(
|
|
72
|
+
.map((glob) => toPattern(glob.replace(/\/+$/u, '')));
|
|
74
73
|
|
|
75
74
|
if (patterns.length === 0) {
|
|
76
75
|
return [];
|
|
@@ -113,11 +112,11 @@ export function workspaceMembers(root = '.') {
|
|
|
113
112
|
|
|
114
113
|
walk(absolute, 0);
|
|
115
114
|
|
|
116
|
-
return [...members].
|
|
115
|
+
return [...members].toSorted((left, right) => (left < right ? -1 : 1));
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
// CLI form — only when this file IS the process entry, never on import.
|
|
120
|
-
if (argv[1] && resolve(argv[1]) ===
|
|
119
|
+
if (argv[1] && resolve(argv[1]) === import.meta.filename) {
|
|
121
120
|
const members = workspaceMembers(argv[2] ?? '.');
|
|
122
121
|
if (members.length > 0) {
|
|
123
122
|
stdout.write(`${members.join('\n')}\n`);
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/typescript",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.1.0",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/jterrazz/package-typescript"
|
|
8
|
+
"url": "git+https://github.com/jterrazz/package-typescript.git"
|
|
8
9
|
},
|
|
9
10
|
"bin": {
|
|
10
11
|
"typescript": "bin/typescript.sh"
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
"bin",
|
|
14
15
|
"lib",
|
|
15
16
|
"presets",
|
|
17
|
+
"rules",
|
|
16
18
|
"src"
|
|
17
19
|
],
|
|
18
20
|
"type": "module",
|
|
@@ -37,34 +39,52 @@
|
|
|
37
39
|
},
|
|
38
40
|
"./tsconfig/*": "./presets/tsconfig/*.json",
|
|
39
41
|
"./tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
40
|
-
"./tsdown/*":
|
|
41
|
-
|
|
42
|
+
"./tsdown/*": {
|
|
43
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
44
|
+
"default": "./presets/tsdown/*.js"
|
|
45
|
+
},
|
|
46
|
+
"./tsdown/*.js": {
|
|
47
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
48
|
+
"default": "./presets/tsdown/*.js"
|
|
49
|
+
},
|
|
42
50
|
"./presets/tsconfig/*": "./presets/tsconfig/*.json",
|
|
43
51
|
"./presets/tsconfig/*.json": "./presets/tsconfig/*.json",
|
|
44
|
-
"./presets/tsdown/*":
|
|
45
|
-
|
|
52
|
+
"./presets/tsdown/*": {
|
|
53
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
54
|
+
"default": "./presets/tsdown/*.js"
|
|
55
|
+
},
|
|
56
|
+
"./presets/tsdown/*.js": {
|
|
57
|
+
"types": "./presets/tsdown/*.d.ts",
|
|
58
|
+
"default": "./presets/tsdown/*.js"
|
|
59
|
+
}
|
|
46
60
|
},
|
|
47
61
|
"publishConfig": {
|
|
48
62
|
"registry": "https://registry.npmjs.org/"
|
|
49
63
|
},
|
|
50
64
|
"scripts": {
|
|
51
65
|
"build": "# no build script",
|
|
52
|
-
"lint": "./bin/typescript.sh check
|
|
53
|
-
"lint:fix": "./bin/typescript.sh fix
|
|
66
|
+
"lint": "./bin/typescript.sh check",
|
|
67
|
+
"lint:fix": "./bin/typescript.sh fix",
|
|
54
68
|
"test": "vitest --run"
|
|
55
69
|
},
|
|
56
70
|
"dependencies": {
|
|
71
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
72
|
+
"dependency-cruiser": "^18.3.1",
|
|
57
73
|
"eslint-plugin-perfectionist": "^5.10.0",
|
|
58
|
-
"knip": "^6.
|
|
59
|
-
"oxfmt": "^0.
|
|
60
|
-
"oxlint": "^1.
|
|
61
|
-
"
|
|
74
|
+
"knip": "^6.35.1",
|
|
75
|
+
"oxfmt": "^0.68.0",
|
|
76
|
+
"oxlint": "^1.83.0",
|
|
77
|
+
"oxlint-tsgolint": "7.0.2001",
|
|
78
|
+
"prettier": "^3.9.6",
|
|
79
|
+
"prettier-plugin-astro": "^1.0.0",
|
|
80
|
+
"publint": "^0.3.24",
|
|
81
|
+
"tsdown": "^0.23.0",
|
|
62
82
|
"typedoc": "^0.28.20",
|
|
63
83
|
"typedoc-plugin-markdown": "^4.12.0",
|
|
64
84
|
"typescript": "^6.0.0"
|
|
65
85
|
},
|
|
66
86
|
"devDependencies": {
|
|
67
|
-
"@jterrazz/test": "^
|
|
87
|
+
"@jterrazz/test": "^15.0.0",
|
|
68
88
|
"@types/node": "^26.1.1",
|
|
69
89
|
"vitest": "^4.1.10"
|
|
70
90
|
},
|
|
@@ -76,5 +96,8 @@
|
|
|
76
96
|
"@typescript/typescript-linux-x64": "^7.0.2",
|
|
77
97
|
"@typescript/typescript-win32-arm64": "^7.0.2",
|
|
78
98
|
"@typescript/typescript-win32-x64": "^7.0.2"
|
|
99
|
+
},
|
|
100
|
+
"engines": {
|
|
101
|
+
"node": ">=24.0.0"
|
|
79
102
|
}
|
|
80
103
|
}
|
package/presets/oxfmt/index.js
CHANGED
|
@@ -1,18 +1,62 @@
|
|
|
1
1
|
import { defineConfig } from 'oxfmt';
|
|
2
2
|
|
|
3
|
+
/*
|
|
4
|
+
* The formatting decisions, in one place. Four of them are the estate's and do
|
|
5
|
+
* not move — 100 columns, 4 spaces, single quotes, trailing commas everywhere —
|
|
6
|
+
* because twenty-eight repositories are written that way and two of them hold
|
|
7
|
+
* signed attestation bytes a reflow would invalidate.
|
|
8
|
+
*
|
|
9
|
+
* The three sorters are the fourth law of the rulebook: sorting is formatting.
|
|
10
|
+
* oxfmt owns import order, package.json key order and Tailwind class order, so
|
|
11
|
+
* no lint rule reorders those bytes and no two tools fight over them
|
|
12
|
+
* ([Lint presets](../../docs/07-lint-presets.md)).
|
|
13
|
+
*/
|
|
3
14
|
export default defineConfig({
|
|
15
|
+
bracketSpacing: true,
|
|
16
|
+
endOfLine: 'lf',
|
|
4
17
|
printWidth: 100,
|
|
5
|
-
tabWidth: 4,
|
|
6
|
-
useTabs: false,
|
|
7
18
|
semi: true,
|
|
8
19
|
singleQuote: true,
|
|
20
|
+
/* The grouping the estate has read for three years: builtins and externals
|
|
21
|
+
* together at the top, then internals, then the relative block, then
|
|
22
|
+
* styles. A blank line between groups, case-insensitive inside one. */
|
|
23
|
+
sortImports: {
|
|
24
|
+
groups: [
|
|
25
|
+
['builtin', 'external'],
|
|
26
|
+
['internal', 'subpath'],
|
|
27
|
+
['parent', 'sibling', 'index'],
|
|
28
|
+
'style',
|
|
29
|
+
'unknown',
|
|
30
|
+
],
|
|
31
|
+
ignoreCase: true,
|
|
32
|
+
newlinesBetween: true,
|
|
33
|
+
order: 'asc',
|
|
34
|
+
},
|
|
35
|
+
sortPackageJson: true,
|
|
36
|
+
/* Tailwind's own order, on `class`/`className` and on the class-holding
|
|
37
|
+
* helpers every repo of the estate uses. Regex matchers are not supported
|
|
38
|
+
* yet — these are exact names. */
|
|
39
|
+
sortTailwindcss: {
|
|
40
|
+
functions: ['clsx', 'cn', 'cva', 'tv', 'twMerge', 'twJoin', 'tw'],
|
|
41
|
+
},
|
|
42
|
+
tabWidth: 4,
|
|
9
43
|
trailingComma: 'all',
|
|
10
|
-
|
|
11
|
-
endOfLine: 'lf',
|
|
44
|
+
useTabs: false,
|
|
12
45
|
/* `typescript docs` writes the byte-for-byte typedoc tree to docs/reference/ —
|
|
13
46
|
* formatting it would fight the Docs (sync) pass, so skip it by default.
|
|
14
47
|
* Asset trees and build caches are not source either — and a content tree
|
|
15
48
|
* can hold signed bytes (jterrazz-web's attestations), where one formatter
|
|
16
49
|
* pass invalidates every proof. */
|
|
17
|
-
ignorePatterns: [
|
|
50
|
+
ignorePatterns: [
|
|
51
|
+
'docs/reference',
|
|
52
|
+
'dist/**',
|
|
53
|
+
'assets/**',
|
|
54
|
+
'public/**',
|
|
55
|
+
'.next/**',
|
|
56
|
+
'.expo/**',
|
|
57
|
+
// A spec's input is deliberately mis-shaped and its golden is
|
|
58
|
+
// Byte-for-byte; formatting either would rewrite the claim.
|
|
59
|
+
'**/_fixtures/**',
|
|
60
|
+
'**/_expected/**',
|
|
61
|
+
],
|
|
18
62
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Astro: the rulebook, plus React (an island is a React component here),
|
|
8
|
+
* accessibility, and the decisions an `.astro` file's own shape forces.
|
|
9
|
+
*/
|
|
10
|
+
export default defineConfig(profile(PROFILES.astro));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/** Bun: the node rulebook, plus the globals Bun's runtime defines. */
|
|
7
|
+
export default defineConfig(profile(PROFILES.bun));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/** Expo and React Native: the rulebook, plus React, accessibility and the platform. */
|
|
7
|
+
export default defineConfig(profile(PROFILES.expo));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* A published package: the node rulebook, plus the one tree a library carries
|
|
8
|
+
* and never lints — the committed typedoc projection.
|
|
9
|
+
*
|
|
10
|
+
* The `.js` extension on every relative import is core's (`import/extensions`
|
|
11
|
+
* at `always`), because Node ESM resolves a specifier literally and a package
|
|
12
|
+
* published as ESM is read by Node before any bundler reads it. It pairs with
|
|
13
|
+
* `presets/tsdown/bundle.js`, whose `isolatedDeclarations` is what makes the
|
|
14
|
+
* published declarations emit without a type-checker.
|
|
15
|
+
*/
|
|
16
|
+
export default defineConfig(profile(PROFILES.library));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/** Next.js: the rulebook, plus React, accessibility and Next's own 21 rules. */
|
|
7
|
+
export default defineConfig(profile(PROFILES.next));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/** Node.js services and command-line tools: the rulebook, and nothing added. */
|
|
7
|
+
export default defineConfig(profile(PROFILES.node));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
|
|
3
|
+
import { profile } from '../../../rules/compile.js';
|
|
4
|
+
import { PROFILES } from '../../../rules/profiles.js';
|
|
5
|
+
|
|
6
|
+
/** React with no framework under it: the rulebook, plus React and accessibility. */
|
|
7
|
+
export default defineConfig(profile(PROFILES.react));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"display": "Astro",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"exactOptionalPropertyTypes": true,
|
|
7
|
+
"incremental": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"jsxImportSource": "react",
|
|
10
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
11
|
+
"module": "ESNext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noImplicitOverride": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noUncheckedSideEffectImports": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"strict": true,
|
|
21
|
+
"target": "ESNext",
|
|
22
|
+
"tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
|
|
23
|
+
"verbatimModuleSyntax": true
|
|
24
|
+
}
|
|
25
|
+
}
|