@rtorcato/repo-tooling 3.3.0 → 3.5.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/dist/base/checks.js +23 -0
- package/dist/cli/commands/doctor.js +5 -1
- package/dist/cli/commands/fix-targets.js +5 -0
- package/dist/cli/utils/copy-preset.js +5 -0
- package/dist/cli/utils/detect-language.js +44 -2
- package/dist/languages/swift/checks.js +115 -2
- package/dist/languages/swift/fixers.js +64 -0
- package/package.json +2 -1
- package/tooling/swift/swift-format.json +10 -0
package/dist/base/checks.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { BADGE_START, hasPublicOnlyBadges } from '../cli/generators/badges.js';
|
|
4
|
+
import { detectNestedLanguages } from '../cli/utils/detect-language.js';
|
|
5
|
+
/**
|
|
6
|
+
* Root-only detection is the decision (#317), not an oversight — but it used to
|
|
7
|
+
* be a *silent* one: a Swift repo with a TypeScript docs app audited as Swift
|
|
8
|
+
* and never mentioned the half it skipped. This says so.
|
|
9
|
+
*
|
|
10
|
+
* Always `ok`. Nesting a second language is a legitimate repo shape, so failing
|
|
11
|
+
* on it would break every monorepo's CI to report a limitation of ours.
|
|
12
|
+
*/
|
|
13
|
+
export async function checkNestedLanguages(dir, rootLanguage) {
|
|
14
|
+
const check = 'Monorepo';
|
|
15
|
+
const nested = await detectNestedLanguages(dir, rootLanguage);
|
|
16
|
+
if (nested.length === 0) {
|
|
17
|
+
return { check, status: 'ok', detail: 'single-language repo' };
|
|
18
|
+
}
|
|
19
|
+
const summary = nested.map((n) => `${n.dir} (${n.language})`).join(', ');
|
|
20
|
+
return {
|
|
21
|
+
check,
|
|
22
|
+
status: 'ok',
|
|
23
|
+
detail: `auditing the root only — ${summary} ${nested.length === 1 ? 'is' : 'are'} not audited`,
|
|
24
|
+
hint: 'Run doctor with `--directory <path>` to audit a nested project on its own',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
4
27
|
export async function checkFile(dir, spec) {
|
|
5
28
|
for (const candidate of spec.candidates) {
|
|
6
29
|
const filepath = path.join(dir, candidate);
|
|
@@ -12,7 +12,7 @@ import { checkGitHubSettings } from '../../base/github-settings.js';
|
|
|
12
12
|
import { checkGitIdentity } from '../../base/git-identity.js';
|
|
13
13
|
import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
|
|
14
14
|
import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
|
|
15
|
-
import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
|
|
15
|
+
import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkNestedLanguages, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
|
|
16
16
|
import { allDeps, checkAreTheTypesWrong, checkConfigSchemaVersions, checkDocsSite, checkEnginesNode, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
|
|
17
17
|
export { evaluateNodeVersion };
|
|
18
18
|
const PACKAGE = '@rtorcato/repo-tooling';
|
|
@@ -145,6 +145,7 @@ function demoteDeclined(results, lock) {
|
|
|
145
145
|
async function runBaseChecks(dir, lock, opts) {
|
|
146
146
|
const results = [];
|
|
147
147
|
results.push(checkLockfile(lock));
|
|
148
|
+
results.push(await checkNestedLanguages(dir, opts.language));
|
|
148
149
|
results.push(await checkGitIdentity(dir));
|
|
149
150
|
results.push(await checkEditorConfig(dir));
|
|
150
151
|
results.push(await checkFile(dir, COMMITLINT_FILE_CHECK));
|
|
@@ -191,6 +192,7 @@ export async function runDoctor(dir) {
|
|
|
191
192
|
hooks: null,
|
|
192
193
|
badges: { audience: 'public', fixTarget: null },
|
|
193
194
|
presetWorkflow: null,
|
|
195
|
+
language,
|
|
194
196
|
})),
|
|
195
197
|
];
|
|
196
198
|
return demoteDeclined(results, lock);
|
|
@@ -211,6 +213,7 @@ export async function runDoctor(dir) {
|
|
|
211
213
|
// package.json name/repository, which a Swift repo hasn't got.
|
|
212
214
|
badges: { audience: 'public', fixTarget: null },
|
|
213
215
|
presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
|
|
216
|
+
language,
|
|
214
217
|
})),
|
|
215
218
|
...(await runSwiftChecks(targetDir)),
|
|
216
219
|
];
|
|
@@ -261,6 +264,7 @@ export async function runDoctor(dir) {
|
|
|
261
264
|
hooks: jsGitHooksProfile(pkg),
|
|
262
265
|
badges: { audience: jsBadgeAudience(pkg), fixTarget: 'badges' },
|
|
263
266
|
presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg))),
|
|
267
|
+
language,
|
|
264
268
|
})));
|
|
265
269
|
return demoteDeclined(results, lock);
|
|
266
270
|
}
|
|
@@ -58,6 +58,11 @@ const SWIFT_FIX_TARGETS = {
|
|
|
58
58
|
Periphery: 'periphery',
|
|
59
59
|
'Swift .gitignore': 'swift-gitignore',
|
|
60
60
|
'Release automation': 'swift-release',
|
|
61
|
+
'swift-format': 'swift-format',
|
|
62
|
+
DocC: 'docc',
|
|
63
|
+
// `Swift tests` is deliberately absent: when it fails for the manifest half
|
|
64
|
+
// (no `.testTarget(`) there's nothing to run, and rewriting Package.swift
|
|
65
|
+
// isn't safe. The check's own hint covers both halves.
|
|
61
66
|
};
|
|
62
67
|
export function getFixTargetForCheck(checkName, language) {
|
|
63
68
|
if (language === 'swift' && SWIFT_FIX_TARGETS[checkName]) {
|
|
@@ -41,6 +41,11 @@ export const PRESETS = {
|
|
|
41
41
|
target: '.swiftlint.yml',
|
|
42
42
|
desc: 'SwiftLint configuration (lint + --fix; the standard swift-common runs)',
|
|
43
43
|
},
|
|
44
|
+
'swift-format': {
|
|
45
|
+
source: 'tooling/swift/swift-format.json',
|
|
46
|
+
target: '.swift-format',
|
|
47
|
+
desc: "Apple swift-format configuration (4-space, 120 columns — matches the module's .editorconfig)",
|
|
48
|
+
},
|
|
44
49
|
periphery: {
|
|
45
50
|
source: 'tooling/swift/periphery.yml',
|
|
46
51
|
target: '.periphery.yml',
|
|
@@ -2,8 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
/**
|
|
4
4
|
* Marker files that identify a repo's language, checked in order — first match
|
|
5
|
-
* wins. One language per repo root; multi-language monorepos
|
|
6
|
-
*
|
|
5
|
+
* wins. One language per repo root; multi-language monorepos stay out of scope
|
|
6
|
+
* (#139, reaffirmed in #317 — the audit is root-only, but says so via
|
|
7
|
+
* `detectNestedLanguages`). A dir with no marker → 'unknown' (base checks only).
|
|
7
8
|
*/
|
|
8
9
|
const MARKERS = [
|
|
9
10
|
['js', ['package.json']],
|
|
@@ -20,3 +21,44 @@ export async function detectLanguage(dir) {
|
|
|
20
21
|
}
|
|
21
22
|
return 'unknown';
|
|
22
23
|
}
|
|
24
|
+
/** Never worth descending into, and expensive when we do. */
|
|
25
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'target', 'vendor', '.build']);
|
|
26
|
+
/**
|
|
27
|
+
* Sub-project dirs whose language marker disagrees with the root's, as
|
|
28
|
+
* `{ dir, language }` sorted by dir. Depth-2 scan — enough for the shapes that
|
|
29
|
+
* exist (`apps/docs`, `packages/*`, a top-level `docs/`) and cheap enough to run
|
|
30
|
+
* on every doctor.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately *not* driven by the workspace manifest: the case #317 is about is
|
|
33
|
+
* a Swift or Perl root with a JS docs app, and such a repo has no
|
|
34
|
+
* `pnpm-workspace.yaml` to read. Walking the tree finds it either way, and needs
|
|
35
|
+
* no glob dependency.
|
|
36
|
+
*/
|
|
37
|
+
export async function detectNestedLanguages(dir, rootLanguage) {
|
|
38
|
+
const found = [];
|
|
39
|
+
const scan = async (relative, depth) => {
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = await fs.readdir(path.join(dir, relative));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return; // unreadable dir is the caller's problem, not the audit's
|
|
46
|
+
}
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.startsWith('.') || SKIP_DIRS.has(entry))
|
|
49
|
+
continue;
|
|
50
|
+
const child = relative ? `${relative}/${entry}` : entry;
|
|
51
|
+
if (!(await fs.stat(path.join(dir, child)).catch(() => null))?.isDirectory())
|
|
52
|
+
continue;
|
|
53
|
+
const language = await detectLanguage(path.join(dir, child));
|
|
54
|
+
if (language !== 'unknown' && language !== rootLanguage) {
|
|
55
|
+
found.push({ dir: child, language });
|
|
56
|
+
continue; // a package's own subdirs belong to it, not to the root
|
|
57
|
+
}
|
|
58
|
+
if (depth > 1)
|
|
59
|
+
await scan(child, depth - 1);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
await scan('', 2);
|
|
63
|
+
return found.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
64
|
+
}
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* (lint + `--fix` in pre-commit, `--strict` in CI) and Periphery for dead code.
|
|
6
6
|
* Deliberately *not* checked:
|
|
7
7
|
*
|
|
8
|
-
* - SwiftFormat — SwiftLint's `--fix` does the
|
|
9
|
-
* would fight it.
|
|
8
|
+
* - SwiftFormat (the Nick Lockwood one) — SwiftLint's `--fix` does the
|
|
9
|
+
* formatting; a second *rewriting* formatter would fight it. Apple's
|
|
10
|
+
* `swift-format` is checked as of #311, but only as an optional slot: a repo
|
|
11
|
+
* that opts into it runs it *instead of* `swiftlint --fix`.
|
|
10
12
|
* - `.swift-version` — the toolchain is pinned in CI (`setup-xcode`) and the
|
|
11
13
|
* package declares `// swift-tools-version:`, so a root file would be a third
|
|
12
14
|
* place to keep in sync.
|
|
@@ -33,6 +35,17 @@ const SWIFT_FILE_CHECKS = [
|
|
|
33
35
|
optional: true,
|
|
34
36
|
hint: 'Run `npx @rtorcato/repo-tooling fix periphery` to scaffold a dead-code scan config',
|
|
35
37
|
},
|
|
38
|
+
{
|
|
39
|
+
// Apple's swift-format, the formatter slot (#311) — not a second linter.
|
|
40
|
+
// Optional because SwiftLint's `--fix` already formats: a repo runs one,
|
|
41
|
+
// the other, or both, so its absence is a choice rather than drift.
|
|
42
|
+
check: 'swift-format',
|
|
43
|
+
candidates: ['.swift-format', '.swift-format.json'],
|
|
44
|
+
expected: 'is a valid swift-format configuration',
|
|
45
|
+
matcher: /"(version|lineLength|indentation|rules)"\s*:/,
|
|
46
|
+
optional: true,
|
|
47
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix swift-format` to scaffold one (SwiftLint `--fix` formats without it)',
|
|
48
|
+
},
|
|
36
49
|
];
|
|
37
50
|
/**
|
|
38
51
|
* Build artefacts that must never be committed. `.build` and `DerivedData` are
|
|
@@ -105,6 +118,104 @@ export async function checkPackageSwift(dir) {
|
|
|
105
118
|
detail: `Package.swift declares tools ${toolsVersion} and explicit platforms`,
|
|
106
119
|
};
|
|
107
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Directory names under `Sources/`. For a SwiftPM package these are the target
|
|
123
|
+
* names — a target's sources and its DocC catalogue live in the same folder,
|
|
124
|
+
* so this is where both the check and the `docc` fixer have to look.
|
|
125
|
+
*/
|
|
126
|
+
export async function swiftSourceTargets(dir) {
|
|
127
|
+
try {
|
|
128
|
+
const entries = await fs.readdir(path.join(dir, 'Sources'), { withFileTypes: true });
|
|
129
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function findDoccCatalogue(dir) {
|
|
136
|
+
for (const target of await swiftSourceTargets(dir)) {
|
|
137
|
+
const entries = await fs.readdir(path.join(dir, 'Sources', target));
|
|
138
|
+
const catalogue = entries.find((e) => e.endsWith('.docc'));
|
|
139
|
+
if (catalogue)
|
|
140
|
+
return `Sources/${target}/${catalogue}`;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* DocC, the Swift shape of the TypeDoc check (#311). Two halves have to line
|
|
146
|
+
* up: a `.docc` catalogue holds the prose, and `swift-docc-plugin` is what
|
|
147
|
+
* makes `swift package generate-documentation` exist — either alone is docs
|
|
148
|
+
* that nobody can build, or a build command with nothing to say.
|
|
149
|
+
*/
|
|
150
|
+
export async function checkDocC(dir) {
|
|
151
|
+
const check = 'DocC';
|
|
152
|
+
const hint = 'Run `npx @rtorcato/repo-tooling fix docc` to scaffold a DocC catalogue';
|
|
153
|
+
const manifest = path.join(dir, 'Package.swift');
|
|
154
|
+
const contents = (await fs.pathExists(manifest)) ? await fs.readFile(manifest, 'utf-8') : '';
|
|
155
|
+
const hasPlugin = contents.includes('swift-docc-plugin');
|
|
156
|
+
const catalogue = await findDoccCatalogue(dir);
|
|
157
|
+
if (catalogue && hasPlugin) {
|
|
158
|
+
return { check, status: 'ok', detail: `${catalogue} with swift-docc-plugin declared` };
|
|
159
|
+
}
|
|
160
|
+
if (catalogue) {
|
|
161
|
+
return {
|
|
162
|
+
check,
|
|
163
|
+
status: 'drift',
|
|
164
|
+
detail: `${catalogue} found but Package.swift declares no swift-docc-plugin`,
|
|
165
|
+
hint: 'Add `.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0")` to Package.swift',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (hasPlugin) {
|
|
169
|
+
return {
|
|
170
|
+
check,
|
|
171
|
+
status: 'drift',
|
|
172
|
+
detail: 'swift-docc-plugin declared but no .docc catalogue under Sources/',
|
|
173
|
+
hint,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { check, status: 'optional-missing', detail: 'DocC not configured', hint };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The test setup (#311). SwiftPM has no test-runner config file to check — the
|
|
180
|
+
* suite is declared in the manifest and run by `swift test` — so the two facts
|
|
181
|
+
* worth asserting are that a test target exists at all and that CI runs it. A
|
|
182
|
+
* green pipeline over a package with no `.testTarget(` proves nothing.
|
|
183
|
+
*/
|
|
184
|
+
export async function checkSwiftTests(dir) {
|
|
185
|
+
const check = 'Swift tests';
|
|
186
|
+
const manifest = path.join(dir, 'Package.swift');
|
|
187
|
+
if (!(await fs.pathExists(manifest))) {
|
|
188
|
+
return { check, status: 'missing', detail: 'no Package.swift' };
|
|
189
|
+
}
|
|
190
|
+
if (!/\.testTarget\(/.test(await fs.readFile(manifest, 'utf-8'))) {
|
|
191
|
+
return {
|
|
192
|
+
check,
|
|
193
|
+
status: 'missing',
|
|
194
|
+
detail: 'Package.swift declares no `.testTarget(`',
|
|
195
|
+
hint: 'Add a `.testTarget(name: "<Target>Tests", dependencies: ["<Target>"])` to Package.swift',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const candidates = ['.gitlab-ci.yml', '.gitlab-ci.yaml'];
|
|
199
|
+
const workflowsDir = path.join(dir, '.github', 'workflows');
|
|
200
|
+
if (await fs.pathExists(workflowsDir)) {
|
|
201
|
+
const files = (await fs.readdir(workflowsDir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
202
|
+
candidates.unshift(...files.map((f) => path.join('.github', 'workflows', f)));
|
|
203
|
+
}
|
|
204
|
+
for (const candidate of candidates) {
|
|
205
|
+
const filepath = path.join(dir, candidate);
|
|
206
|
+
if (!(await fs.pathExists(filepath)))
|
|
207
|
+
continue;
|
|
208
|
+
if (/\bswift\s+test\b/.test(await fs.readFile(filepath, 'utf-8'))) {
|
|
209
|
+
return { check, status: 'ok', detail: `test target declared and run by ${candidate}` };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
check,
|
|
214
|
+
status: 'drift',
|
|
215
|
+
detail: 'test target declared but no CI job runs `swift test`',
|
|
216
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix swift-ci` to regenerate a workflow that runs `swift test`',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
108
219
|
/**
|
|
109
220
|
* Release automation for a SwiftPM package (#310). There is no publish step to
|
|
110
221
|
* look for — a release *is* a semver git tag consumers resolve with
|
|
@@ -159,6 +270,8 @@ export async function runSwiftChecks(dir) {
|
|
|
159
270
|
await checkPackageSwift(dir),
|
|
160
271
|
...(await Promise.all(SWIFT_FILE_CHECKS.map((spec) => checkFile(dir, spec)))),
|
|
161
272
|
await checkSwiftGitignore(dir),
|
|
273
|
+
await checkSwiftTests(dir),
|
|
274
|
+
await checkDocC(dir),
|
|
162
275
|
await checkSwiftRelease(dir),
|
|
163
276
|
];
|
|
164
277
|
}
|
|
@@ -7,9 +7,33 @@ import fs from 'fs-extra';
|
|
|
7
7
|
import { buildPresetConfig } from '../../cli/commands/setup-presets.js';
|
|
8
8
|
import { copyPreset } from '../../cli/utils/copy-preset.js';
|
|
9
9
|
import { LOCKFILE_NAME, writeLockfile } from '../../cli/utils/lockfile.js';
|
|
10
|
+
import { swiftSourceTargets } from './checks.js';
|
|
10
11
|
import { readSwiftPackage, renderSwiftGitLabCI, renderSwiftReleaseWorkflow, renderSwiftWorkflow, } from './ci.js';
|
|
11
12
|
import { SWIFT_HOOKS_DIR, installSwiftGitHooks } from './git-hooks.js';
|
|
12
13
|
import { ensureSwiftGitignore } from './gitignore.js';
|
|
14
|
+
/**
|
|
15
|
+
* A DocC module page. The heading is a symbol link (double backticks) because
|
|
16
|
+
* that's what binds the article to the module — a plain `# Name` heading makes
|
|
17
|
+
* DocC treat the file as a standalone article instead.
|
|
18
|
+
*/
|
|
19
|
+
function doccLandingPage(target) {
|
|
20
|
+
return `# \`\`${target}\`\`
|
|
21
|
+
|
|
22
|
+
Summary line for ${target} — replace this with what the module is for.
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
Write the prose documentation for ${target} here. Anything else in this
|
|
27
|
+
catalogue (articles, tutorials, resources) is picked up automatically.
|
|
28
|
+
|
|
29
|
+
Build it with \`swift package generate-documentation\`, or preview it with
|
|
30
|
+
\`swift package --disable-sandbox preview-documentation --target ${target}\`.
|
|
31
|
+
|
|
32
|
+
## Topics
|
|
33
|
+
|
|
34
|
+
### Essentials
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
13
37
|
export const SWIFT_FIXERS = [
|
|
14
38
|
{
|
|
15
39
|
target: 'swiftlint',
|
|
@@ -33,6 +57,46 @@ export const SWIFT_FIXERS = [
|
|
|
33
57
|
return { filesWritten: [result.target] };
|
|
34
58
|
},
|
|
35
59
|
},
|
|
60
|
+
{
|
|
61
|
+
target: 'swift-format',
|
|
62
|
+
description: 'Scaffold .swift-format (Apple swift-format, the optional formatter slot)',
|
|
63
|
+
appliesTo: ['swift-format'],
|
|
64
|
+
outputs: ['.swift-format'],
|
|
65
|
+
canFixDrift: true,
|
|
66
|
+
async run({ targetDir }) {
|
|
67
|
+
const result = await copyPreset('swift-format', targetDir);
|
|
68
|
+
return { filesWritten: [result.target] };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
target: 'docc',
|
|
73
|
+
description: 'Scaffold a DocC catalogue (Sources/<Target>/<Target>.docc) for the first target',
|
|
74
|
+
appliesTo: ['DocC'],
|
|
75
|
+
// Path depends on the target name, so `fix --dry-run` shows no diff for
|
|
76
|
+
// this one — the file it writes isn't knowable without reading Sources/.
|
|
77
|
+
outputs: ['Sources/<Target>/<Target>.docc/<Target>.md'],
|
|
78
|
+
riskLevel: 'safe-add',
|
|
79
|
+
canFixDrift: true,
|
|
80
|
+
async run({ targetDir }) {
|
|
81
|
+
// The library product's target first — that's the API consumers read
|
|
82
|
+
// docs for — falling back to whatever single target the package has.
|
|
83
|
+
const targets = await swiftSourceTargets(targetDir);
|
|
84
|
+
const { products } = await readSwiftPackage(targetDir);
|
|
85
|
+
const target = products.find((p) => targets.includes(p)) ?? targets[0];
|
|
86
|
+
if (!target) {
|
|
87
|
+
throw new Error('no Sources/<Target>/ directory to put a DocC catalogue in');
|
|
88
|
+
}
|
|
89
|
+
const file = `Sources/${target}/${target}.docc/${target}.md`;
|
|
90
|
+
// safe-add: the catalogue is prose someone wrote, so a re-run (DocC
|
|
91
|
+
// also drifts when the manifest lacks the plugin, which this fixer
|
|
92
|
+
// can't repair) must not overwrite it.
|
|
93
|
+
if (await fs.pathExists(path.join(targetDir, file)))
|
|
94
|
+
return { filesWritten: [] };
|
|
95
|
+
await fs.outputFile(path.join(targetDir, file), doccLandingPage(target));
|
|
96
|
+
console.log(chalk.yellow(' add swift-docc-plugin to Package.swift to build it: .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0")'));
|
|
97
|
+
return { filesWritten: [file] };
|
|
98
|
+
},
|
|
99
|
+
},
|
|
36
100
|
{
|
|
37
101
|
target: 'swift-gitignore',
|
|
38
102
|
description: 'Add the Swift/SwiftPM build artefacts (.build, DerivedData, xcuserdata) to .gitignore',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/repo-tooling",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"tooling/claude/*.md",
|
|
103
103
|
"tooling/mcp/mcp.json.example",
|
|
104
104
|
"tooling/swift/*.yml",
|
|
105
|
+
"tooling/swift/*.json",
|
|
105
106
|
"tooling/github-actions/workflows/*.yml",
|
|
106
107
|
"README.md",
|
|
107
108
|
"AGENTS.md"
|