@jterrazz/typescript 8.1.3 → 9.0.1

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 CHANGED
@@ -18,6 +18,7 @@ npx typescript dev # Build, run, and rebuild on changes
18
18
  npx typescript docs # Compile the committed docs/reference tree from source
19
19
  npx typescript check # Type-check, lint, format-check, and unused-code in parallel
20
20
  npx typescript fix # Auto-fix lint and formatting issues
21
+ npx typescript clean # Remove .artifacts/ (dist/ stays — it is the product)
21
22
  ```
22
23
 
23
24
  ## How it works
@@ -269,6 +269,11 @@ run_checks() {
269
269
  # Merge base config (from this package) with optional project-local knip.json.
270
270
  # Root-only on purpose: knip reads the workspace globs itself and reports per
271
271
  # member from one run — a second invocation per member would double-report.
272
+ #
273
+ # Deliberately UNCACHED. Knip's `--cache` validates a cached glob against the
274
+ # mtimes of the directories that held a match; a file added to a directory
275
+ # that held none is invisible to it, and the run exits 0 where the uncached
276
+ # run exits 1. A gate that can pass on stale knowledge is worse than a slow one.
272
277
  local knip_pid=""
273
278
  local knip_status=0
274
279
  if [ "$FIX_MODE" = false ]; then
@@ -282,6 +287,33 @@ run_checks() {
282
287
  knip_pid=$!
283
288
  fi
284
289
 
290
+ # Gitignore (artefacts): the convention — every artefact under
291
+ # `.artifacts/<tool>/`, `dist` excepted — read off the project's own
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`.
300
+ local gitignore_pid=""
301
+ local gitignore_status=0
302
+ local gitignore_applicable=false
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
309
+ if [ "$FIX_MODE" = true ]; then
310
+ node "$PACKAGE_ROOT/lib/check-gitignore.js" --fix > "$tmp_dir/gitignore.log" 2>&1 &
311
+ else
312
+ node "$PACKAGE_ROOT/lib/check-gitignore.js" > "$tmp_dir/gitignore.log" 2>&1 &
313
+ fi
314
+ gitignore_pid=$!
315
+ fi
316
+
285
317
  # Conventions checker: only in check mode, once per specs root the workspace
286
318
  # owns, gated by the package that OWNS that root — a member may depend on
287
319
  # @jterrazz/test while the root does not, and the reverse.
@@ -326,6 +358,7 @@ run_checks() {
326
358
  wait $lint_pid; local lint_status=$?
327
359
  wait $format_pid; local format_status=$?
328
360
  [ -n "$knip_pid" ] && { wait $knip_pid; knip_status=$?; }
361
+ [ -n "$gitignore_pid" ] && { wait $gitignore_pid; gitignore_status=$?; }
329
362
 
330
363
  # One pass, N runs: the pass fails if any run failed, and only the logs of
331
364
  # the runs that FAILED are printed — a green member stays silent.
@@ -380,6 +413,19 @@ run_checks() {
380
413
  printf "${GREEN}✓ Passed${NC}\n"
381
414
  fi
382
415
 
416
+ # The one pass that speaks on success: a rewrite changed a file the operator
417
+ # owns, and silence would hide it. In check mode a green gate writes nothing,
418
+ # so the green output stays byte-identical with the others.
419
+ if [ -n "$gitignore_pid" ]; then
420
+ printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Gitignore (artefacts)\n\n"
421
+ [ -s "$tmp_dir/gitignore.log" ] && cat "$tmp_dir/gitignore.log"
422
+ if [ $gitignore_status -ne 0 ]; then
423
+ printf "${RED}✗ Failed with exit code %d${NC}\n" $gitignore_status
424
+ else
425
+ printf "${GREEN}✓ Passed${NC}\n"
426
+ fi
427
+ fi
428
+
383
429
  if [ "$FIX_MODE" = false ]; then
384
430
  printf "\n${CYAN_BG}${BRIGHT_WHITE} RUN ${NC} Knip (unused code)\n\n"
385
431
  if [ $knip_status -ne 0 ]; then
@@ -421,7 +467,7 @@ run_checks() {
421
467
  printf "\n${CYAN_BG}${BRIGHT_WHITE} END ${NC} Finalizing quality checks\n\n"
422
468
  fi
423
469
 
424
- if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_status -eq 0 ]; then
470
+ if [ $type_status -eq 0 ] && [ $lint_status -eq 0 ] && [ $format_status -eq 0 ] && [ $knip_status -eq 0 ] && [ $gitignore_status -eq 0 ] && [ $checker_status -eq 0 ] && [ $docs_status -eq 0 ]; then
425
471
  printf "${GREEN}✓ All checks passed${NC}\n"
426
472
  exit 0
427
473
  else
package/bin/typescript.sh CHANGED
@@ -144,6 +144,22 @@ case "$COMMAND" in
144
144
  fi
145
145
  ;;
146
146
 
147
+ clean)
148
+ # Every tool writes under .artifacts/<tool>/, so removing that one
149
+ # directory is the whole cleanup. dist/ is NOT touched: it is the
150
+ # build's product, published from beside src/, not an artefact.
151
+ cd "$PROJECT_ROOT"
152
+
153
+ printf "${CYAN_BG}${BRIGHT_WHITE} TYPESCRIPT ${NC} Removing artefacts...\n\n"
154
+
155
+ if [ -d ".artifacts" ]; then
156
+ rm -rf .artifacts
157
+ printf "${GREEN}Removed .artifacts/${NC}\n"
158
+ else
159
+ printf "${GREEN}Nothing to remove — .artifacts/ is already absent${NC}\n"
160
+ fi
161
+ ;;
162
+
147
163
  check|fix)
148
164
  exec bash "$SCRIPT_DIR/commands/check.sh" "$COMMAND" "$@"
149
165
  ;;
@@ -158,7 +174,8 @@ case "$COMMAND" in
158
174
  printf " dev Build, run, and rebuild on changes\n"
159
175
  printf " docs Generate the committed docs/reference tree; --check verifies sync\n"
160
176
  printf " check Check types, lint, formatting, and unused code\n"
161
- printf " fix Auto-fix lint and formatting issues\n\n"
177
+ printf " fix Auto-fix lint and formatting issues\n"
178
+ printf " clean Remove .artifacts/ — dist/ stays, it is the build's product\n\n"
162
179
  printf "Examples:\n"
163
180
  printf " typescript build\n"
164
181
  printf " typescript bundle\n"
@@ -168,6 +185,7 @@ case "$COMMAND" in
168
185
  printf " typescript docs --check\n"
169
186
  printf " typescript check\n"
170
187
  printf " typescript fix\n"
188
+ printf " typescript clean\n"
171
189
  exit 1
172
190
  ;;
173
191
  esac
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * The artefact convention, read off a project's `.gitignore` — and, in a
5
+ * workspace, off the ancestor `.gitignore` that covers it too.
6
+ *
7
+ * Every build, test and lint artefact lives under `.artifacts/<tool>/` at the
8
+ * project root — one folder per tool that writes. A `.gitignore` line naming an
9
+ * artefact ANYWHERE else is the old layout, and this gate says so; `.artifacts/`
10
+ * itself must be ignored, so the convention's own folder never reaches a commit.
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.
22
+ *
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,
31
+ * and everything else — comments, blank lines, order, the project's own paths —
32
+ * survives untouched. A `!` line is NEVER rewritten: it rescues a tracked file,
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.
36
+ *
37
+ * Exit code: 0 when the project holds the convention, 1 otherwise. In `--fix`
38
+ * mode only what the rewrite cannot repair — a committed artefact — still fails.
39
+ */
40
+
41
+ import { execFileSync } from 'node:child_process';
42
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
43
+ import { dirname, join, relative, resolve } from 'node:path';
44
+ import { argv, exit, stdout } from 'node:process';
45
+
46
+ /** The convention's own directory: the one path that MUST be ignored. */
47
+ const ARTIFACTS = '.artifacts';
48
+
49
+ /**
50
+ * The one exception to the convention: a build's product stays beside `src/`
51
+ * and is published from there, so `dist` is a legitimate ignored path.
52
+ */
53
+ const PRODUCT = 'dist';
54
+
55
+ /** Directories a tool writes, and the `.artifacts/` home each one moves to. */
56
+ const ARTEFACT_DIRECTORIES = new Map([
57
+ ['.cache', '.artifacts/<tool>/'],
58
+ ['.next', '.artifacts/next/'],
59
+ ['.turbo', '.artifacts/turbo/'],
60
+ ['.vite', '.artifacts/vite/'],
61
+ ['bin', '.artifacts/go/'],
62
+ ['build', '.artifacts/<tool>/'],
63
+ ['coverage', '.artifacts/coverage/'],
64
+ ['out', '.artifacts/next/'],
65
+ ['playwright-report', '.artifacts/playwright/'],
66
+ ['target', '.artifacts/cargo/'],
67
+ ['test-results', '.artifacts/playwright/'],
68
+ ]);
69
+
70
+ /** Files a tool writes, matched on their extension, and where they belong. */
71
+ const ARTEFACT_EXTENSIONS = [
72
+ ['.tsbuildinfo', '.artifacts/tsc/'],
73
+ ['.log', '.artifacts/logs/'],
74
+ ];
75
+
76
+ /**
77
+ * The closed list of ignored paths that are NOT artefacts of the convention:
78
+ * platform working directories a toolchain owns and cannot be told to move,
79
+ * generated files a framework expects at a fixed path, and `node_modules`.
80
+ */
81
+ const EXCEPTIONS = new Set([
82
+ '.build',
83
+ '.expo',
84
+ '.gradle',
85
+ '.swiftpm',
86
+ '.vercel',
87
+ 'DerivedData',
88
+ 'Package.resolved',
89
+ 'android',
90
+ 'ios',
91
+ 'next-env.d.ts',
92
+ 'node_modules',
93
+ ]);
94
+
95
+ /** The one exception spelled as a prefix — Expo stamps a suffix onto it. */
96
+ const EXCEPTION_PREFIXES = ['.metro-health-check'];
97
+
98
+ /**
99
+ * `next.config.*` file names, in resolution order — mirrors what Next.js
100
+ * itself tries, closely enough for a textual `output` read.
101
+ */
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
+ ];
110
+
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
+ }
197
+
198
+ /**
199
+ * What a `.gitignore` pattern names, reduced to the one segment that carries
200
+ * the meaning: an anchored form, a nested form and a doubled-star form all name
201
+ * the same directory. A trailing `*` is dropped so `npm-debug.log*` reads as a
202
+ * log, and a `**` segment never wins over the name that follows it.
203
+ */
204
+ function subject(pattern) {
205
+ const segments = pattern
206
+ .replace(/\/+$/, '')
207
+ .split('/')
208
+ .filter((segment) => segment !== '' && segment !== '**');
209
+
210
+ return (segments.at(-1) ?? '').replace(/\*$/, '');
211
+ }
212
+
213
+ /** The `.artifacts/` home of the artefact a pattern names, or null. */
214
+ function artefactHome(pattern, { nextIsExportDestination }) {
215
+ const name = subject(pattern);
216
+
217
+ if (name === '' || name === PRODUCT || EXCEPTIONS.has(name)) {
218
+ return null;
219
+ }
220
+ if (nextIsExportDestination && name === '.next') {
221
+ return null;
222
+ }
223
+ if (EXCEPTION_PREFIXES.some((prefix) => name.startsWith(prefix))) {
224
+ return null;
225
+ }
226
+ if (ARTEFACT_DIRECTORIES.has(name)) {
227
+ return ARTEFACT_DIRECTORIES.get(name);
228
+ }
229
+ for (const [extension, home] of ARTEFACT_EXTENSIONS) {
230
+ if (name.endsWith(extension)) {
231
+ return home;
232
+ }
233
+ }
234
+
235
+ return null;
236
+ }
237
+
238
+ /** Every line of a `.gitignore`, classified once — the rewrite reads the same list. */
239
+ function readLines(gitignorePath) {
240
+ const nextIsExportDestination = nextConfigDeclaresExport(dirname(gitignorePath));
241
+ const text = readFileSync(gitignorePath, 'utf8').split('\n');
242
+
243
+ // The empty string a trailing newline leaves behind is not a line.
244
+ // Putting that newline back is the rewrite's job.
245
+ if (text.at(-1) === '') {
246
+ text.pop();
247
+ }
248
+
249
+ return text.map((line) => {
250
+ const pattern = line.trim();
251
+ const isPattern = pattern !== '' && !pattern.startsWith('#');
252
+
253
+ return {
254
+ home:
255
+ isPattern && !pattern.startsWith('!')
256
+ ? artefactHome(pattern, { nextIsExportDestination })
257
+ : null,
258
+ isNegation: isPattern && pattern.startsWith('!'),
259
+ pattern,
260
+ text: line,
261
+ };
262
+ });
263
+ }
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
+
311
+ /**
312
+ * The artefacts a project COMMITTED. Outside a git tree the question has no
313
+ * answer, and silence is the right one — a fixture directory is not a repo.
314
+ */
315
+ function trackedArtefacts(root) {
316
+ let listed;
317
+ try {
318
+ listed = execFileSync('git', ['ls-files', '-z'], {
319
+ cwd: root,
320
+ encoding: 'utf8',
321
+ stdio: ['ignore', 'pipe', 'ignore'],
322
+ });
323
+ } catch {
324
+ return [];
325
+ }
326
+
327
+ return listed
328
+ .split('\0')
329
+ .filter((path) => path !== '')
330
+ .filter((path) => {
331
+ const segments = path.split('/');
332
+
333
+ return (
334
+ segments
335
+ .slice(0, -1)
336
+ .some((segment) => TRACKED_ARTEFACT_DIRECTORIES.has(segment)) ||
337
+ path.endsWith(TRACKED_ARTEFACT_EXTENSION)
338
+ );
339
+ });
340
+ }
341
+
342
+ /** The rewritten file: artefact lines dropped, `.artifacts/` present, rest kept. */
343
+ function rewrite(lines) {
344
+ const kept = lines.filter((line) => line.home === null);
345
+ const isIgnored = kept.some((line) => !line.isNegation && subject(line.pattern) === ARTIFACTS);
346
+ const body = kept.map((line) => line.text);
347
+
348
+ if (!isIgnored) {
349
+ while (body.length > 0 && body.at(-1).trim() === '') {
350
+ body.pop();
351
+ }
352
+ body.push('', '# Build, test and lint artefacts (@jterrazz/typescript)', `${ARTIFACTS}/`);
353
+ }
354
+
355
+ return `${body.join('\n')}\n`;
356
+ }
357
+
358
+ const isFix = argv.includes('--fix');
359
+ const isProbe = argv.includes('--has-ancestor');
360
+ const root = argv.slice(2).find((argument) => !argument.startsWith('--')) ?? '.';
361
+
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) {
387
+ exit(0);
388
+ }
389
+
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 })),
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
+
408
+ const tracked = trackedArtefacts(root);
409
+
410
+ if (isFix) {
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
+ }
430
+ }
431
+ }
432
+ } else if (misplaced.length > 0 || !isIgnored) {
433
+ stdout.write('Artefacts belong under .artifacts/<tool>/ at the project root:\n');
434
+ for (const line of misplaced) {
435
+ stdout.write(` ✗ ${line.source.label} names ${line.pattern} — its home is ${line.home}\n`);
436
+ }
437
+ if (!isIgnored) {
438
+ stdout.write(` ✗ ${target.label} does not ignore ${ARTIFACTS}/ — add it\n`);
439
+ }
440
+ for (const line of negations) {
441
+ stdout.write(` · ${line.pattern} — a negation rescues a tracked file, left for you\n`);
442
+ }
443
+ stdout.write("Run 'typescript fix' to rewrite .gitignore.\n");
444
+ }
445
+
446
+ if (tracked.length > 0) {
447
+ stdout.write('Artefacts must never be committed:\n');
448
+ for (const path of tracked) {
449
+ stdout.write(` ✗ ${path} is tracked — git rm --cached ${path}\n`);
450
+ }
451
+ }
452
+
453
+ exit(tracked.length > 0 || (!isFix && (misplaced.length > 0 || !isIgnored)) ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "8.1.3",
3
+ "version": "9.0.1",
4
4
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,7 @@
7
7
  "allowJs": true,
8
8
  "baseUrl": ".",
9
9
  "esModuleInterop": true,
10
+ "incremental": true,
10
11
  "jsx": "react-native",
11
12
  "lib": ["DOM", "ESNext"],
12
13
  "moduleResolution": "node",
@@ -16,6 +17,7 @@
16
17
  },
17
18
  "resolveJsonModule": true,
18
19
  "skipLibCheck": true,
19
- "target": "ESNext"
20
+ "target": "ESNext",
21
+ "tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo"
20
22
  }
21
23
  }
@@ -7,6 +7,7 @@
7
7
  "strict": true,
8
8
  "noEmit": true,
9
9
  "incremental": true,
10
+ "tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
10
11
  "module": "esnext",
11
12
  "esModuleInterop": true,
12
13
  "moduleResolution": "bundler",
@@ -9,6 +9,8 @@
9
9
  "target": "ESNext",
10
10
  "types": ["node"],
11
11
  "strict": true,
12
+ "incremental": true,
13
+ "tsBuildInfoFile": "${configDir}/.artifacts/tsc/tsconfig.tsbuildinfo",
12
14
  "experimentalDecorators": true,
13
15
  "esModuleInterop": true,
14
16
  "resolveJsonModule": true,