@jterrazz/typescript 9.0.0 → 9.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/bin/commands/check.sh +14 -4
- package/lib/check-gitignore.js +239 -46
- package/lib/merge-knip-config.js +94 -2
- package/package.json +5 -1
- package/src/oxfmt.d.ts +6 -0
- package/src/oxfmt.js +14 -0
- package/src/oxfmt.test.ts +17 -0
- package/src/oxlint.d.ts +1 -0
- package/src/oxlint.js +10 -2
- package/src/oxlint.test.ts +9 -1
package/bin/commands/check.sh
CHANGED
|
@@ -289,13 +289,23 @@ run_checks() {
|
|
|
289
289
|
|
|
290
290
|
# Gitignore (artefacts): the convention — every artefact under
|
|
291
291
|
# `.artifacts/<tool>/`, `dist` excepted — read off the project's own
|
|
292
|
-
# `.gitignore
|
|
293
|
-
#
|
|
294
|
-
#
|
|
295
|
-
#
|
|
292
|
+
# `.gitignore` AND, in check mode, the nearest ancestor `.gitignore` above it
|
|
293
|
+
# (the workspace root's — check-gitignore.js walks up to find it). Opt-in by
|
|
294
|
+
# existence: neither file names an artefact path, the gate has no question to
|
|
295
|
+
# ask. A workspace whose `lint` delegates to members needs the ancestor probed
|
|
296
|
+
# here, in bash, because it decides whether to print the pass at all — the
|
|
297
|
+
# node script decides everything past that. Fix mode stays own-file-only: it
|
|
298
|
+
# is the one gate whose remedy is a rewrite, and it never rewrites another
|
|
299
|
+
# project's `.gitignore`.
|
|
296
300
|
local gitignore_pid=""
|
|
297
301
|
local gitignore_status=0
|
|
302
|
+
local gitignore_applicable=false
|
|
298
303
|
if [ -f ".gitignore" ]; then
|
|
304
|
+
gitignore_applicable=true
|
|
305
|
+
elif [ "$FIX_MODE" = false ] && node "$PACKAGE_ROOT/lib/check-gitignore.js" --has-ancestor > /dev/null 2>&1; then
|
|
306
|
+
gitignore_applicable=true
|
|
307
|
+
fi
|
|
308
|
+
if [ "$gitignore_applicable" = true ]; then
|
|
299
309
|
if [ "$FIX_MODE" = true ]; then
|
|
300
310
|
node "$PACKAGE_ROOT/lib/check-gitignore.js" --fix > "$tmp_dir/gitignore.log" 2>&1 &
|
|
301
311
|
else
|
package/lib/check-gitignore.js
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The artefact convention, read off a project's `.gitignore
|
|
4
|
+
* The artefact convention, read off a project's `.gitignore` — and, in a
|
|
5
|
+
* workspace, off the ancestor `.gitignore` that covers it too.
|
|
5
6
|
*
|
|
6
7
|
* Every build, test and lint artefact lives under `.artifacts/<tool>/` at the
|
|
7
8
|
* project root — one folder per tool that writes. A `.gitignore` line naming an
|
|
8
9
|
* artefact ANYWHERE else is the old layout, and this gate says so; `.artifacts/`
|
|
9
10
|
* itself must be ignored, so the convention's own folder never reaches a commit.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* Two files are read, each judged by the same rules: the package's own
|
|
13
|
+
* `.gitignore`, and the nearest ANCESTOR `.gitignore` above it — the workspace
|
|
14
|
+
* root's, found by walking up to the nearest directory holding a lockfile or a
|
|
15
|
+
* `workspaces` manifest. A workspace whose `lint` delegates to members runs this
|
|
16
|
+
* gate once per member, cwd'd there; without the ancestor a root that ignores
|
|
17
|
+
* `.artifacts/` for everyone would look, from a member with no `.gitignore` of
|
|
18
|
+
* its own, exactly like a project declaring nothing. An ancestor pattern counts
|
|
19
|
+
* only when it is NOT anchored to the ancestor's own directory (no `/` besides a
|
|
20
|
+
* trailing one) — the same rule git applies when deciding whether a pattern
|
|
21
|
+
* reaches into a nested directory.
|
|
12
22
|
*
|
|
13
|
-
*
|
|
23
|
+
* Usage: node check-gitignore.js [--fix] [--has-ancestor] [root]
|
|
24
|
+
*
|
|
25
|
+
* `--has-ancestor` answers, silently, whether an ancestor `.gitignore` exists
|
|
26
|
+
* above `root` — the probe a caller uses to decide whether the gate has
|
|
27
|
+
* anything to read when `root` itself carries no `.gitignore`.
|
|
28
|
+
*
|
|
29
|
+
* `--fix` rewrites the package's OWN `.gitignore` (never the ancestor's, which
|
|
30
|
+
* is a different project's file): the artefact lines go, `.artifacts/` arrives,
|
|
14
31
|
* and everything else — comments, blank lines, order, the project's own paths —
|
|
15
32
|
* survives untouched. A `!` line is NEVER rewritten: it rescues a tracked file,
|
|
16
33
|
* and deleting the line it negates could hide a real path. Those are reported.
|
|
34
|
+
* Fix has nothing to do when the package owns no `.gitignore` of its own, even
|
|
35
|
+
* if an ancestor exists — it never creates a file, only repairs one.
|
|
17
36
|
*
|
|
18
37
|
* Exit code: 0 when the project holds the convention, 1 otherwise. In `--fix`
|
|
19
38
|
* mode only what the rewrite cannot repair — a committed artefact — still fails.
|
|
@@ -21,7 +40,7 @@
|
|
|
21
40
|
|
|
22
41
|
import { execFileSync } from 'node:child_process';
|
|
23
42
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
24
|
-
import { join } from 'node:path';
|
|
43
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
25
44
|
import { argv, exit, stdout } from 'node:process';
|
|
26
45
|
|
|
27
46
|
/** The convention's own directory: the one path that MUST be ignored. */
|
|
@@ -77,24 +96,104 @@ const EXCEPTIONS = new Set([
|
|
|
77
96
|
const EXCEPTION_PREFIXES = ['.metro-health-check'];
|
|
78
97
|
|
|
79
98
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* as OUTPUT, but a tracked file under them may be a project's own source —
|
|
83
|
-
* a `.gitignore` line declares intent, a tracked path declares nothing.
|
|
99
|
+
* `next.config.*` file names, in resolution order — mirrors what Next.js
|
|
100
|
+
* itself tries, closely enough for a textual `output` read.
|
|
84
101
|
*/
|
|
85
|
-
const
|
|
86
|
-
'.
|
|
87
|
-
'.
|
|
88
|
-
'.
|
|
89
|
-
'.
|
|
90
|
-
'.
|
|
91
|
-
'
|
|
92
|
-
|
|
93
|
-
'test-results',
|
|
94
|
-
]);
|
|
102
|
+
const NEXT_CONFIG_FILES = [
|
|
103
|
+
'next.config.js',
|
|
104
|
+
'next.config.mjs',
|
|
105
|
+
'next.config.ts',
|
|
106
|
+
'next.config.mts',
|
|
107
|
+
'next.config.cjs',
|
|
108
|
+
'next.config.cts',
|
|
109
|
+
];
|
|
95
110
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
111
|
+
/**
|
|
112
|
+
* With `output: 'export'`, Next reads `distDir` as the EXPORT destination and
|
|
113
|
+
* keeps its working directory at `.next` regardless — `next/dist/export/utils.js`
|
|
114
|
+
* (`hasCustomExportOutput`) refuses to move it. So `.next` cannot be told to
|
|
115
|
+
* live under `.artifacts/next/` in that one mode, proven by a real consumer
|
|
116
|
+
* (clawssify's site). Without `output: 'export'`, nothing pins it, and `.next`
|
|
117
|
+
* stays an ordinary artefact. Read textually — a project's own config may not
|
|
118
|
+
* even be valid JS in the tool's own runtime, and a regex answers the one
|
|
119
|
+
* question this gate has without loading it.
|
|
120
|
+
*/
|
|
121
|
+
function nextConfigDeclaresExport(dir) {
|
|
122
|
+
for (const name of NEXT_CONFIG_FILES) {
|
|
123
|
+
const path = join(dir, name);
|
|
124
|
+
if (!existsSync(path)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
if (/output\s*:\s*['"]export['"]/.test(readFileSync(path, 'utf8'))) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// An unreadable config answers no differently than a missing one.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Lockfiles whose presence marks a directory as a package manager's root. */
|
|
140
|
+
const WORKSPACE_LOCKFILES = [
|
|
141
|
+
'bun.lock',
|
|
142
|
+
'bun.lockb',
|
|
143
|
+
'npm-shrinkwrap.json',
|
|
144
|
+
'package-lock.json',
|
|
145
|
+
'pnpm-lock.yaml',
|
|
146
|
+
'yarn.lock',
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
/** A workspace root: a lockfile lives here, or its manifest declares `workspaces`. */
|
|
150
|
+
function isWorkspaceRoot(dir) {
|
|
151
|
+
if (WORKSPACE_LOCKFILES.some((name) => existsSync(join(dir, name)))) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const manifest = join(dir, 'package.json');
|
|
156
|
+
if (!existsSync(manifest)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).workspaces !== undefined;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The nearest ancestor ABOVE `root` that is a workspace root, or null. Never
|
|
169
|
+
* `root` itself — a project reads its OWN `.gitignore` regardless, so only
|
|
170
|
+
* what sits above it is worth a second file.
|
|
171
|
+
*/
|
|
172
|
+
function findWorkspaceRoot(root) {
|
|
173
|
+
let dir = dirname(resolve(root));
|
|
174
|
+
let parent = dirname(dir);
|
|
175
|
+
|
|
176
|
+
while (dir !== parent) {
|
|
177
|
+
if (isWorkspaceRoot(dir)) {
|
|
178
|
+
return dir;
|
|
179
|
+
}
|
|
180
|
+
dir = parent;
|
|
181
|
+
parent = dirname(dir);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return isWorkspaceRoot(dir) ? dir : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A pattern anchored to its OWN `.gitignore`'s directory — one that carries a
|
|
189
|
+
* `/` other than a trailing one, or a leading one — the way git itself reads
|
|
190
|
+
* it. An anchored pattern in an ANCESTOR's file never reaches a nested package;
|
|
191
|
+
* only an unanchored one (`.artifacts/`, not `/.artifacts/` or `out/.artifacts/`)
|
|
192
|
+
* matches at any depth below it.
|
|
193
|
+
*/
|
|
194
|
+
function isAnchored(pattern) {
|
|
195
|
+
return pattern.replace(/\/+$/, '').includes('/');
|
|
196
|
+
}
|
|
98
197
|
|
|
99
198
|
/**
|
|
100
199
|
* What a `.gitignore` pattern names, reduced to the one segment that carries
|
|
@@ -112,12 +211,15 @@ function subject(pattern) {
|
|
|
112
211
|
}
|
|
113
212
|
|
|
114
213
|
/** The `.artifacts/` home of the artefact a pattern names, or null. */
|
|
115
|
-
function artefactHome(pattern) {
|
|
214
|
+
function artefactHome(pattern, { nextIsExportDestination }) {
|
|
116
215
|
const name = subject(pattern);
|
|
117
216
|
|
|
118
217
|
if (name === '' || name === PRODUCT || EXCEPTIONS.has(name)) {
|
|
119
218
|
return null;
|
|
120
219
|
}
|
|
220
|
+
if (nextIsExportDestination && name === '.next') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
121
223
|
if (EXCEPTION_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
122
224
|
return null;
|
|
123
225
|
}
|
|
@@ -133,8 +235,9 @@ function artefactHome(pattern) {
|
|
|
133
235
|
return null;
|
|
134
236
|
}
|
|
135
237
|
|
|
136
|
-
/** Every line of
|
|
238
|
+
/** Every line of a `.gitignore`, classified once — the rewrite reads the same list. */
|
|
137
239
|
function readLines(gitignorePath) {
|
|
240
|
+
const nextIsExportDestination = nextConfigDeclaresExport(dirname(gitignorePath));
|
|
138
241
|
const text = readFileSync(gitignorePath, 'utf8').split('\n');
|
|
139
242
|
|
|
140
243
|
// The empty string a trailing newline leaves behind is not a line.
|
|
@@ -148,7 +251,10 @@ function readLines(gitignorePath) {
|
|
|
148
251
|
const isPattern = pattern !== '' && !pattern.startsWith('#');
|
|
149
252
|
|
|
150
253
|
return {
|
|
151
|
-
home:
|
|
254
|
+
home:
|
|
255
|
+
isPattern && !pattern.startsWith('!')
|
|
256
|
+
? artefactHome(pattern, { nextIsExportDestination })
|
|
257
|
+
: null,
|
|
152
258
|
isNegation: isPattern && pattern.startsWith('!'),
|
|
153
259
|
pattern,
|
|
154
260
|
text: line,
|
|
@@ -156,6 +262,52 @@ function readLines(gitignorePath) {
|
|
|
156
262
|
});
|
|
157
263
|
}
|
|
158
264
|
|
|
265
|
+
/**
|
|
266
|
+
* A `.gitignore` judged on its own: every line classified, plus what it
|
|
267
|
+
* contributes to the combined verdict. `reach` narrows an ancestor's file to
|
|
268
|
+
* the patterns that actually cross into a nested package — every line, for
|
|
269
|
+
* the package's own file, since that file's directory IS the project root.
|
|
270
|
+
*/
|
|
271
|
+
function readSource(path, { own, reach = () => true, root }) {
|
|
272
|
+
const lines = readLines(path);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
isIgnored: lines.some(
|
|
276
|
+
(line) =>
|
|
277
|
+
!line.isNegation &&
|
|
278
|
+
line.home === null &&
|
|
279
|
+
subject(line.pattern) === ARTIFACTS &&
|
|
280
|
+
reach(line.pattern),
|
|
281
|
+
),
|
|
282
|
+
label: relative(resolve(root), path).split('\\').join('/'),
|
|
283
|
+
lines,
|
|
284
|
+
misplaced: lines.filter((line) => line.home !== null && reach(line.pattern)),
|
|
285
|
+
negations: lines.filter((line) => line.isNegation),
|
|
286
|
+
own,
|
|
287
|
+
path,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Directories whose every tracked file is a committed artefact. Narrower than
|
|
293
|
+
* the list above on purpose: `bin`, `out`, `build` and `target` are ignorable
|
|
294
|
+
* as OUTPUT, but a tracked file under them may be a project's own source —
|
|
295
|
+
* a `.gitignore` line declares intent, a tracked path declares nothing.
|
|
296
|
+
*/
|
|
297
|
+
const TRACKED_ARTEFACT_DIRECTORIES = new Set([
|
|
298
|
+
'.artifacts',
|
|
299
|
+
'.cache',
|
|
300
|
+
'.next',
|
|
301
|
+
'.turbo',
|
|
302
|
+
'.vite',
|
|
303
|
+
'coverage',
|
|
304
|
+
'playwright-report',
|
|
305
|
+
'test-results',
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
/** The extension no tracked file may carry — a buildinfo is never source. */
|
|
309
|
+
const TRACKED_ARTEFACT_EXTENSION = '.tsbuildinfo';
|
|
310
|
+
|
|
159
311
|
/**
|
|
160
312
|
* The artefacts a project COMMITTED. Outside a git tree the question has no
|
|
161
313
|
* answer, and silence is the right one — a fixture directory is not a repo.
|
|
@@ -204,45 +356,86 @@ function rewrite(lines) {
|
|
|
204
356
|
}
|
|
205
357
|
|
|
206
358
|
const isFix = argv.includes('--fix');
|
|
359
|
+
const isProbe = argv.includes('--has-ancestor');
|
|
207
360
|
const root = argv.slice(2).find((argument) => !argument.startsWith('--')) ?? '.';
|
|
208
|
-
const gitignorePath = join(root, '.gitignore');
|
|
209
361
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
362
|
+
const workspaceRoot = findWorkspaceRoot(root);
|
|
363
|
+
const ancestorPath = workspaceRoot ? join(workspaceRoot, '.gitignore') : null;
|
|
364
|
+
|
|
365
|
+
// The probe answers one question — does an ancestor `.gitignore` exist above
|
|
366
|
+
// `root` — for a caller that already knows how to test `root`'s own.
|
|
367
|
+
if (isProbe) {
|
|
368
|
+
exit(ancestorPath && existsSync(ancestorPath) ? 0 : 1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const ownPath = join(root, '.gitignore');
|
|
372
|
+
const sources = [];
|
|
373
|
+
if (existsSync(ownPath)) {
|
|
374
|
+
sources.push(readSource(ownPath, { own: true, root }));
|
|
375
|
+
}
|
|
376
|
+
if (ancestorPath && existsSync(ancestorPath)) {
|
|
377
|
+
sources.push(
|
|
378
|
+
readSource(ancestorPath, { own: false, reach: (pattern) => !isAnchored(pattern), root }),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/*
|
|
383
|
+
* Neither the package nor any ancestor above it declares an artefact path —
|
|
384
|
+
* the gate has no question to ask, and stays silent.
|
|
385
|
+
*/
|
|
386
|
+
if (sources.length === 0) {
|
|
213
387
|
exit(0);
|
|
214
388
|
}
|
|
215
389
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
390
|
+
/*
|
|
391
|
+
* The combined verdict, own file and ancestor alike — an ancestor's line
|
|
392
|
+
* already narrowed to what actually reaches this package (`readSource`'s
|
|
393
|
+
* `reach`). Negations keep the original, file-agnostic wording: rescuing a
|
|
394
|
+
* tracked file is the same judgement call regardless of which file names it.
|
|
395
|
+
*/
|
|
396
|
+
const isIgnored = sources.some((source) => source.isIgnored);
|
|
397
|
+
const misplaced = sources.flatMap((source) =>
|
|
398
|
+
source.misplaced.map((line) => ({ ...line, source })),
|
|
221
399
|
);
|
|
400
|
+
const negations = sources.flatMap((source) => source.negations);
|
|
401
|
+
|
|
402
|
+
/*
|
|
403
|
+
* When nobody ignores it, the file to fix is the shared one when there is
|
|
404
|
+
* one — a workspace root covers every member, so that is where it belongs.
|
|
405
|
+
*/
|
|
406
|
+
const target = sources.find((source) => !source.own) ?? sources[0];
|
|
407
|
+
|
|
222
408
|
const tracked = trackedArtefacts(root);
|
|
223
409
|
|
|
224
410
|
if (isFix) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
411
|
+
/*
|
|
412
|
+
* Fix repairs the package's OWN file only — it never creates one, and it
|
|
413
|
+
* never rewrites an ancestor, which is a different project's file.
|
|
414
|
+
*/
|
|
415
|
+
const own = sources.find((source) => source.own);
|
|
416
|
+
if (own) {
|
|
417
|
+
const changed = own.misplaced.length > 0 || !own.isIgnored;
|
|
418
|
+
if (changed) {
|
|
419
|
+
writeFileSync(own.path, rewrite(own.lines));
|
|
420
|
+
stdout.write('.gitignore rewritten:\n');
|
|
421
|
+
for (const line of own.misplaced) {
|
|
422
|
+
stdout.write(` - ${line.pattern}\n`);
|
|
423
|
+
}
|
|
424
|
+
if (!own.isIgnored) {
|
|
425
|
+
stdout.write(` + ${ARTIFACTS}/\n`);
|
|
426
|
+
}
|
|
427
|
+
for (const line of own.negations) {
|
|
428
|
+
stdout.write(` · ${line.pattern} — a negation rescues a tracked file, kept\n`);
|
|
429
|
+
}
|
|
237
430
|
}
|
|
238
431
|
}
|
|
239
432
|
} else if (misplaced.length > 0 || !isIgnored) {
|
|
240
433
|
stdout.write('Artefacts belong under .artifacts/<tool>/ at the project root:\n');
|
|
241
434
|
for (const line of misplaced) {
|
|
242
|
-
stdout.write(` ✗ .
|
|
435
|
+
stdout.write(` ✗ ${line.source.label} names ${line.pattern} — its home is ${line.home}\n`);
|
|
243
436
|
}
|
|
244
437
|
if (!isIgnored) {
|
|
245
|
-
stdout.write(` ✗ .
|
|
438
|
+
stdout.write(` ✗ ${target.label} does not ignore ${ARTIFACTS}/ — add it\n`);
|
|
246
439
|
}
|
|
247
440
|
for (const line of negations) {
|
|
248
441
|
stdout.write(` · ${line.pattern} — a negation rescues a tracked file, left for you\n`);
|
package/lib/merge-knip-config.js
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Usage: node merge-knip-config.js <base.json> [project-knip.json]
|
|
8
8
|
*
|
|
9
|
+
* The project file is read as JSONC — comments and trailing commas — so an
|
|
10
|
+
* ignore can carry the reason it exists. Every entry knip is told to overlook
|
|
11
|
+
* is a judgement, and a judgement with no reason beside it is re-litigated by
|
|
12
|
+
* the next reader.
|
|
13
|
+
*
|
|
9
14
|
* Merge rules:
|
|
10
15
|
* - Arrays (ignoreDependencies, ignoreBinaries, ignore): concatenated and deduplicated
|
|
11
16
|
* - Objects (rules, vitest, etc.): project values override base
|
|
@@ -29,14 +34,101 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
29
34
|
|
|
30
35
|
import { workspaceMembers } from './workspace-members.js';
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Drops `//` and block comments, leaving everything inside a string literal
|
|
39
|
+
* alone — a comment marker in a glob (`ignore: ["**\/*.js"]`) is data.
|
|
40
|
+
*/
|
|
41
|
+
function withoutComments(text) {
|
|
42
|
+
let output = '',
|
|
43
|
+
index = 0,
|
|
44
|
+
inString = false;
|
|
45
|
+
|
|
46
|
+
while (index < text.length) {
|
|
47
|
+
const char = text[index];
|
|
48
|
+
|
|
49
|
+
if (inString) {
|
|
50
|
+
const escaped = char === '\\';
|
|
51
|
+
output += escaped ? char + (text[index + 1] ?? '') : char;
|
|
52
|
+
inString = escaped || char !== '"';
|
|
53
|
+
index += escaped ? 2 : 1;
|
|
54
|
+
} else if (char === '"') {
|
|
55
|
+
inString = true;
|
|
56
|
+
output += char;
|
|
57
|
+
index += 1;
|
|
58
|
+
} else if (char === '/' && text[index + 1] === '/') {
|
|
59
|
+
while (index < text.length && text[index] !== '\n') {
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
} else if (char === '/' && text[index + 1] === '*') {
|
|
63
|
+
const end = text.indexOf('*/', index + 2);
|
|
64
|
+
index = end === -1 ? text.length : end + 2;
|
|
65
|
+
} else {
|
|
66
|
+
output += char;
|
|
67
|
+
index += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether the comma at `index` is the last one of its collection — `["a",]`. */
|
|
75
|
+
function closesCollection(text, index) {
|
|
76
|
+
if (text[index] !== ',') {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let next = index + 1;
|
|
81
|
+
while (next < text.length && text[next].trim() === '') {
|
|
82
|
+
next += 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return text[next] === ']' || text[next] === '}';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Drops a comma that closes its collection, outside string literals. */
|
|
89
|
+
function withoutTrailingCommas(text) {
|
|
90
|
+
let output = '',
|
|
91
|
+
index = 0,
|
|
92
|
+
inString = false;
|
|
93
|
+
|
|
94
|
+
while (index < text.length) {
|
|
95
|
+
const char = text[index];
|
|
96
|
+
const closes = closesCollection(text, index);
|
|
97
|
+
|
|
98
|
+
if (inString) {
|
|
99
|
+
const escaped = char === '\\';
|
|
100
|
+
output += escaped ? char + (text[index + 1] ?? '') : char;
|
|
101
|
+
inString = escaped || char !== '"';
|
|
102
|
+
index += escaped ? 2 : 1;
|
|
103
|
+
} else {
|
|
104
|
+
inString = char === '"';
|
|
105
|
+
output += closes ? '' : char;
|
|
106
|
+
index += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reads a JSONC document. Hand-written on purpose: no JSONC parser is in this
|
|
115
|
+
* package's tree, and two token classes do not earn a dependency.
|
|
116
|
+
*/
|
|
117
|
+
function readJsonc(path) {
|
|
118
|
+
const document = readFileSync(path, 'utf8'),
|
|
119
|
+
json = withoutTrailingCommas(withoutComments(document));
|
|
120
|
+
|
|
121
|
+
return JSON.parse(json);
|
|
122
|
+
}
|
|
123
|
+
|
|
32
124
|
const basePath = process.argv[2];
|
|
33
125
|
const projectPath = process.argv[3];
|
|
34
126
|
|
|
35
|
-
const base =
|
|
127
|
+
const base = readJsonc(basePath);
|
|
36
128
|
|
|
37
129
|
let project = {};
|
|
38
130
|
if (projectPath) {
|
|
39
|
-
project =
|
|
131
|
+
project = readJsonc(projectPath);
|
|
40
132
|
}
|
|
41
133
|
|
|
42
134
|
const ARRAY_KEYS = new Set(['ignore', 'ignoreBinaries', 'ignoreDependencies', 'entry', 'project']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/typescript",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./src/index.d.ts",
|
|
24
24
|
"default": "./src/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./oxfmt": {
|
|
27
|
+
"types": "./src/oxfmt.d.ts",
|
|
28
|
+
"default": "./src/oxfmt.js"
|
|
29
|
+
},
|
|
26
30
|
"./oxlint": {
|
|
27
31
|
"types": "./src/oxlint.d.ts",
|
|
28
32
|
"default": "./src/oxlint.js"
|
package/src/oxfmt.d.ts
ADDED
package/src/oxfmt.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The tool-facing oxfmt entry (`@jterrazz/typescript/oxfmt`): the shared
|
|
3
|
+
* formatting preset plus oxfmt's own `defineConfig`, for the same reason
|
|
4
|
+
* `oxlint.js` re-exports its tool's — a consumer's config names this package
|
|
5
|
+
* and nothing else:
|
|
6
|
+
*
|
|
7
|
+
* import { base, defineConfig } from '@jterrazz/typescript/oxfmt';
|
|
8
|
+
*
|
|
9
|
+
* export default defineConfig(base);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { defineConfig } from 'oxfmt';
|
|
13
|
+
|
|
14
|
+
export { default as base } from '../presets/oxfmt/index.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { base, defineConfig } from './oxfmt.js';
|
|
4
|
+
|
|
5
|
+
test('exports the shared formatting preset', () => {
|
|
6
|
+
// Given - the tool-facing entry
|
|
7
|
+
// Then - the preset carries the ecosystem's format: four spaces, single quotes, 100 columns
|
|
8
|
+
expect(base).toMatchObject({ printWidth: 100, singleQuote: true, tabWidth: 4 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("re-exports oxfmt's own defineConfig", () => {
|
|
12
|
+
// Given - a config a consumer would write, with oxfmt declared nowhere in its project
|
|
13
|
+
const config = { printWidth: 80 };
|
|
14
|
+
|
|
15
|
+
// Then - the entry carries the tool's helper, which returns the config unchanged
|
|
16
|
+
expect(defineConfig(config)).toBe(config);
|
|
17
|
+
});
|
package/src/oxlint.d.ts
CHANGED
package/src/oxlint.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the named
|
|
3
|
-
* presets
|
|
4
|
-
* composes exactly the fragments it wants, nothing is
|
|
3
|
+
* presets, the `compose()` helper, and oxlint's own `defineConfig`. Wiring is
|
|
4
|
+
* EXPLICIT — a consumer composes exactly the fragments it wants, nothing is
|
|
5
|
+
* auto-detected:
|
|
5
6
|
*
|
|
6
7
|
* import { testing } from '@jterrazz/test/oxlint';
|
|
7
8
|
* import { compose, node } from '@jterrazz/typescript/oxlint';
|
|
@@ -50,6 +51,13 @@ export function compose(...fragments) {
|
|
|
50
51
|
return merged;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/* Oxlint's own `defineConfig`, re-exported from here: a bare
|
|
55
|
+
* `import { defineConfig } from 'oxlint'` in a consumer's config resolves only
|
|
56
|
+
* where the consumer declares oxlint itself, which pnpm's strict node_modules
|
|
57
|
+
* refuses to assume. Resolved from THIS package — where oxlint is a real
|
|
58
|
+
* dependency — the one-devDependency shape holds on every package manager. */
|
|
59
|
+
export { defineConfig } from 'oxlint';
|
|
60
|
+
|
|
53
61
|
export { default as hexagonal } from '../presets/oxlint/architectures/hexagonal.js';
|
|
54
62
|
export { default as expo } from '../presets/oxlint/expo.js';
|
|
55
63
|
export { default as next } from '../presets/oxlint/next.js';
|
package/src/oxlint.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { compose, expo, hexagonal, next, node } from './oxlint.js';
|
|
3
|
+
import { compose, defineConfig, expo, hexagonal, next, node } from './oxlint.js';
|
|
4
4
|
|
|
5
5
|
test('concatenates and dedupes plugin lists', () => {
|
|
6
6
|
// Given - two fragments sharing one jsPlugin
|
|
@@ -77,3 +77,11 @@ test('exports the named presets', () => {
|
|
|
77
77
|
expect(typeof preset).toBe('object');
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
test("re-exports oxlint's own defineConfig", () => {
|
|
82
|
+
// Given - a config a consumer would write, with oxlint declared nowhere in its project
|
|
83
|
+
const config = { rules: { curly: 'error' } };
|
|
84
|
+
|
|
85
|
+
// Then - the entry carries the tool's helper, which returns the config unchanged
|
|
86
|
+
expect(defineConfig(config)).toBe(config);
|
|
87
|
+
});
|