@rtorcato/repo-tooling 2.59.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/AGENTS.md +113 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/cli/commands/doctor.js +336 -0
- package/dist/cli/commands/fix-targets.js +192 -0
- package/dist/cli/commands/fix.js +440 -0
- package/dist/cli/commands/setup-presets.js +281 -0
- package/dist/cli/commands/setup.js +501 -0
- package/dist/cli/generators/agent-rules.js +103 -0
- package/dist/cli/generators/badges.js +88 -0
- package/dist/cli/generators/build.js +216 -0
- package/dist/cli/generators/bun.js +25 -0
- package/dist/cli/generators/community-health.js +145 -0
- package/dist/cli/generators/docs-site.js +436 -0
- package/dist/cli/generators/git.js +164 -0
- package/dist/cli/generators/github-actions.js +35 -0
- package/dist/cli/generators/github-workflows.js +24 -0
- package/dist/cli/generators/gitlab-ci.js +10 -0
- package/dist/cli/generators/index.js +123 -0
- package/dist/cli/generators/linting.js +54 -0
- package/dist/cli/generators/misc.js +298 -0
- package/dist/cli/generators/nx.js +16 -0
- package/dist/cli/generators/package-json.js +317 -0
- package/dist/cli/generators/pnpm-workspace.js +126 -0
- package/dist/cli/generators/postcss.js +24 -0
- package/dist/cli/generators/readme.js +268 -0
- package/dist/cli/generators/security.js +156 -0
- package/dist/cli/generators/skills-install.js +70 -0
- package/dist/cli/generators/tailwind.js +34 -0
- package/dist/cli/generators/testing.js +88 -0
- package/dist/cli/generators/treeshake.js +148 -0
- package/dist/cli/generators/tsconfig.js +27 -0
- package/dist/cli/generators/turborepo.js +35 -0
- package/dist/cli/generators/typedoc.js +40 -0
- package/dist/cli/index.js +332 -0
- package/dist/cli/utils/copy-preset.js +98 -0
- package/dist/cli/utils/detect-language.js +22 -0
- package/dist/cli/utils/format.js +32 -0
- package/dist/cli/utils/install.js +28 -0
- package/dist/cli/utils/lockfile.js +84 -0
- package/dist/languages/js/checks.js +949 -0
- package/dist/languages/js/ci.js +251 -0
- package/dist/languages/js/fixers.js +735 -0
- package/dist/languages/registry.js +37 -0
- package/dist/languages/swift/checks.js +127 -0
- package/dist/languages/swift/ci.js +165 -0
- package/dist/languages/swift/fixers.js +102 -0
- package/dist/languages/swift/git-hooks.js +63 -0
- package/dist/languages/swift/gitignore.js +43 -0
- package/dist/languages/swift/scaffold.js +244 -0
- package/package.json +461 -0
- package/tooling/biome/README.md +90 -0
- package/tooling/biome/biome.json +63 -0
- package/tooling/bun/bunfig.toml +14 -0
- package/tooling/changesets/README.md +35 -0
- package/tooling/changesets/config.json +11 -0
- package/tooling/claude/repo-tooling.md +87 -0
- package/tooling/commitlint/commitlint.d.mts +4 -0
- package/tooling/commitlint/commitlint.mjs +40 -0
- package/tooling/cypress/cypress.config.d.mts +4 -0
- package/tooling/cypress/cypress.config.mjs +11 -0
- package/tooling/docusaurus/index.d.mts +17 -0
- package/tooling/docusaurus/index.mjs +38 -0
- package/tooling/docusaurus/sync-changelog.mjs +42 -0
- package/tooling/docusaurus/theme-tokens.css +79 -0
- package/tooling/docusaurus/theme.css +378 -0
- package/tooling/esbuild/index.d.mts +6 -0
- package/tooling/esbuild/index.mjs +102 -0
- package/tooling/eslint/base.d.mts +6 -0
- package/tooling/eslint/base.mjs +122 -0
- package/tooling/eslint/nextjs.d.mts +4 -0
- package/tooling/eslint/nextjs.mjs +22 -0
- package/tooling/eslint/types.d.ts +58 -0
- package/tooling/github-actions/workflows/cloudflare-pages.yml +42 -0
- package/tooling/github-actions/workflows/docker-publish.yml +44 -0
- package/tooling/github-actions/workflows/preview-deployments.yml +54 -0
- package/tooling/github-actions/workflows/vercel-deploy.yml +40 -0
- package/tooling/jest-presets/browser/jest-preset.d.mts +4 -0
- package/tooling/jest-presets/browser/jest-preset.mjs +14 -0
- package/tooling/jest-presets/node/jest-preset.d.mts +4 -0
- package/tooling/jest-presets/node/jest-preset.mjs +13 -0
- package/tooling/mcp/mcp.json.example +27 -0
- package/tooling/nx/nx.json +24 -0
- package/tooling/oxlint/README.md +25 -0
- package/tooling/oxlint/oxlintrc.json +28 -0
- package/tooling/playwright/playwright.config.d.mts +4 -0
- package/tooling/playwright/playwright.config.mjs +19 -0
- package/tooling/prettier/index.d.mts +4 -0
- package/tooling/prettier/index.mjs +36 -0
- package/tooling/release-please/.release-please-manifest.json +3 -0
- package/tooling/release-please/release-please-config.json +9 -0
- package/tooling/rolldown/rolldown.config.d.mts +18 -0
- package/tooling/rolldown/rolldown.config.mjs +59 -0
- package/tooling/rollup/rollup.config.d.mts +20 -0
- package/tooling/rollup/rollup.config.mjs +70 -0
- package/tooling/semantic-release/docker.d.mts +4 -0
- package/tooling/semantic-release/docker.mjs +59 -0
- package/tooling/semantic-release/github.d.mts +4 -0
- package/tooling/semantic-release/github.mjs +79 -0
- package/tooling/semantic-release/index.d.mts +4 -0
- package/tooling/semantic-release/index.mjs +80 -0
- package/tooling/swift/periphery.yml +4 -0
- package/tooling/swift/swiftlint.yml +23 -0
- package/tooling/tests/exports-resolution.d.mts +14 -0
- package/tooling/tests/exports-resolution.mjs +68 -0
- package/tooling/tests/ssr-safety.d.mts +14 -0
- package/tooling/tests/ssr-safety.mjs +53 -0
- package/tooling/tsup/index.d.mts +8 -0
- package/tooling/tsup/index.mjs +33 -0
- package/tooling/typedoc/typedoc.json +7 -0
- package/tooling/typescript/README.md +49 -0
- package/tooling/typescript/reset.d.ts +9 -0
- package/tooling/typescript/tsconfig.base.json +84 -0
- package/tooling/typescript/tsconfig.build.json +11 -0
- package/tooling/typescript/tsconfig.bun.json +9 -0
- package/tooling/typescript/tsconfig.express.json +9 -0
- package/tooling/typescript/tsconfig.next.json +20 -0
- package/tooling/typescript/tsconfig.node.json +9 -0
- package/tooling/typescript/tsconfig.react.json +15 -0
- package/tooling/typescript/tsconfig.test.json +8 -0
- package/tooling/typescript/v1/tsconfig.base.json +81 -0
- package/tooling/typescript/v1/tsconfig.express.json +9 -0
- package/tooling/typescript/v1/tsconfig.next.json +19 -0
- package/tooling/typescript/v1/tsconfig.node.json +9 -0
- package/tooling/typescript/v1/tsconfig.react.json +14 -0
- package/tooling/typescript/v1/tsconfig.test.json +8 -0
- package/tooling/vite/vite.config.d.mts +4 -0
- package/tooling/vite/vite.config.mjs +18 -0
- package/tooling/vitest/jsdom-shims.d.mts +1 -0
- package/tooling/vitest/jsdom-shims.mjs +58 -0
- package/tooling/vitest/vitest.config.d.mts +4 -0
- package/tooling/vitest/vitest.config.mjs +25 -0
- package/tooling/vitest/vitest.config.react.d.mts +4 -0
- package/tooling/vitest/vitest.config.react.mjs +27 -0
- package/tooling/vitest/vitest.setup.d.mts +1 -0
- package/tooling/vitest/vitest.setup.mjs +3 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const LANGUAGES = {
|
|
2
|
+
js: {
|
|
3
|
+
id: 'js',
|
|
4
|
+
label: 'JavaScript/TypeScript',
|
|
5
|
+
codeqlLanguages: ['javascript-typescript'],
|
|
6
|
+
dependabotEcosystem: 'npm',
|
|
7
|
+
supported: true,
|
|
8
|
+
},
|
|
9
|
+
swift: {
|
|
10
|
+
id: 'swift',
|
|
11
|
+
label: 'Swift',
|
|
12
|
+
codeqlLanguages: ['swift'],
|
|
13
|
+
dependabotEcosystem: 'swift',
|
|
14
|
+
supported: true,
|
|
15
|
+
},
|
|
16
|
+
python: {
|
|
17
|
+
id: 'python',
|
|
18
|
+
label: 'Python',
|
|
19
|
+
codeqlLanguages: ['python'],
|
|
20
|
+
dependabotEcosystem: 'pip',
|
|
21
|
+
supported: false,
|
|
22
|
+
},
|
|
23
|
+
perl: {
|
|
24
|
+
id: 'perl',
|
|
25
|
+
label: 'Perl',
|
|
26
|
+
codeqlLanguages: [],
|
|
27
|
+
dependabotEcosystem: null,
|
|
28
|
+
supported: false,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the module for a detected language. `unknown` (a bare dir mid-setup)
|
|
33
|
+
* maps to JS — the historical default so a fresh repo still gets the full suite.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveLanguageModule(language) {
|
|
36
|
+
return language === 'unknown' ? LANGUAGES.js : LANGUAGES[language];
|
|
37
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift language module — checks (#286).
|
|
3
|
+
*
|
|
4
|
+
* The standard encoded here is the one `swift-common` actually runs: SwiftLint
|
|
5
|
+
* (lint + `--fix` in pre-commit, `--strict` in CI) and Periphery for dead code.
|
|
6
|
+
* Deliberately *not* checked:
|
|
7
|
+
*
|
|
8
|
+
* - SwiftFormat — SwiftLint's `--fix` does the formatting; a second formatter
|
|
9
|
+
* would fight it.
|
|
10
|
+
* - `.swift-version` — the toolchain is pinned in CI (`setup-xcode`) and the
|
|
11
|
+
* package declares `// swift-tools-version:`, so a root file would be a third
|
|
12
|
+
* place to keep in sync.
|
|
13
|
+
*/
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import fs from 'fs-extra';
|
|
16
|
+
import { checkFile } from '../../base/checks.js';
|
|
17
|
+
import { SWIFT_HOOKS_DIR } from './git-hooks.js';
|
|
18
|
+
const SWIFT_FILE_CHECKS = [
|
|
19
|
+
{
|
|
20
|
+
check: 'SwiftLint',
|
|
21
|
+
candidates: ['.swiftlint.yml', '.swiftlint.yaml'],
|
|
22
|
+
// SwiftLint configs are project-owned — there's no `extends` mechanism to
|
|
23
|
+
// point at ours, so any file declaring real config counts as ok.
|
|
24
|
+
expected: 'is a valid SwiftLint configuration',
|
|
25
|
+
matcher: /^(disabled_rules|opt_in_rules|only_rules|included|excluded|analyzer_rules):/m,
|
|
26
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix swiftlint` to scaffold',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
check: 'Periphery',
|
|
30
|
+
candidates: ['.periphery.yml', '.periphery.yaml'],
|
|
31
|
+
expected: 'is a valid Periphery configuration',
|
|
32
|
+
matcher: /^(retain_public|project|schemes|targets|index_exclude):/m,
|
|
33
|
+
optional: true,
|
|
34
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix periphery` to scaffold a dead-code scan config',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Build artefacts that must never be committed. `.build` and `DerivedData` are
|
|
39
|
+
* the expensive ones — a single stray commit of either adds hundreds of MB.
|
|
40
|
+
*/
|
|
41
|
+
const REQUIRED_GITIGNORE_ENTRIES = ['.build', 'DerivedData', 'xcuserdata'];
|
|
42
|
+
export async function checkSwiftGitignore(dir) {
|
|
43
|
+
const filepath = path.join(dir, '.gitignore');
|
|
44
|
+
if (!(await fs.pathExists(filepath))) {
|
|
45
|
+
return {
|
|
46
|
+
check: 'Swift .gitignore',
|
|
47
|
+
status: 'missing',
|
|
48
|
+
detail: 'no .gitignore',
|
|
49
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix swift-gitignore` to scaffold the Swift template',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const contents = await fs.readFile(filepath, 'utf-8');
|
|
53
|
+
const missing = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !contents.includes(entry));
|
|
54
|
+
if (missing.length > 0) {
|
|
55
|
+
return {
|
|
56
|
+
check: 'Swift .gitignore',
|
|
57
|
+
status: 'drift',
|
|
58
|
+
detail: `.gitignore missing Swift build artefacts: ${missing.join(', ')}`,
|
|
59
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix swift-gitignore` to append the missing entries',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
check: 'Swift .gitignore',
|
|
64
|
+
status: 'ok',
|
|
65
|
+
detail: '.gitignore covers .build, DerivedData and xcuserdata',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Package.swift hygiene. Both signals are things SwiftPM will not infer for
|
|
70
|
+
* you: without a tools-version comment the manifest doesn't parse at all, and
|
|
71
|
+
* without `platforms:` SwiftPM assumes the oldest deployment target it supports,
|
|
72
|
+
* which silently rejects modern APIs at build time.
|
|
73
|
+
*/
|
|
74
|
+
export async function checkPackageSwift(dir) {
|
|
75
|
+
const filepath = path.join(dir, 'Package.swift');
|
|
76
|
+
if (!(await fs.pathExists(filepath))) {
|
|
77
|
+
return {
|
|
78
|
+
check: 'Package.swift',
|
|
79
|
+
status: 'missing',
|
|
80
|
+
detail: 'no Package.swift',
|
|
81
|
+
hint: 'Run `swift package init` to create a SwiftPM manifest',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const contents = await fs.readFile(filepath, 'utf-8');
|
|
85
|
+
const toolsVersion = contents.match(/^\/\/\s*swift-tools-version:\s*([\d.]+)/m)?.[1];
|
|
86
|
+
if (!toolsVersion) {
|
|
87
|
+
return {
|
|
88
|
+
check: 'Package.swift',
|
|
89
|
+
status: 'drift',
|
|
90
|
+
detail: 'Package.swift has no `// swift-tools-version:` comment',
|
|
91
|
+
hint: 'Add `// swift-tools-version: 5.9` as the first line — SwiftPM requires it',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (!/\bplatforms:\s*\[/.test(contents)) {
|
|
95
|
+
return {
|
|
96
|
+
check: 'Package.swift',
|
|
97
|
+
status: 'drift',
|
|
98
|
+
detail: `Package.swift (tools ${toolsVersion}) declares no \`platforms:\``,
|
|
99
|
+
hint: 'Add a `platforms:` clause — without it SwiftPM assumes the oldest supported deployment target',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
check: 'Package.swift',
|
|
104
|
+
status: 'ok',
|
|
105
|
+
detail: `Package.swift declares tools ${toolsVersion} and explicit platforms`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The Swift shape of the base `Git hooks` / `Pre-push hook` checks (#309).
|
|
110
|
+
* `install` is null because the wiring — `git config core.hooksPath` — is
|
|
111
|
+
* per-clone local state that nothing commits; flagging its absence would fail
|
|
112
|
+
* every fresh CI checkout for something that isn't repo drift.
|
|
113
|
+
*/
|
|
114
|
+
export const SWIFT_GIT_HOOKS = {
|
|
115
|
+
dir: SWIFT_HOOKS_DIR,
|
|
116
|
+
install: null,
|
|
117
|
+
verifyCommand: 'swift test',
|
|
118
|
+
fixTarget: 'swift-git-hooks',
|
|
119
|
+
};
|
|
120
|
+
/** The Swift module's suite, layered on top of the base checks by doctor. */
|
|
121
|
+
export async function runSwiftChecks(dir) {
|
|
122
|
+
return [
|
|
123
|
+
await checkPackageSwift(dir),
|
|
124
|
+
...(await Promise.all(SWIFT_FILE_CHECKS.map((spec) => checkFile(dir, spec)))),
|
|
125
|
+
await checkSwiftGitignore(dir),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift CI generation (#287), built on the language-agnostic skeleton in
|
|
3
|
+
* `src/base/ci.ts`. No `setup-node`, no `pnpm` — the Swift path runs on macOS
|
|
4
|
+
* runners with Xcode, SwiftPM and Homebrew-installed tools.
|
|
5
|
+
*
|
|
6
|
+
* Unlike the JS path there's no `ProjectConfig` to read: everything the Swift
|
|
7
|
+
* workflow needs (products, deployment platforms) is declared in Package.swift,
|
|
8
|
+
* so the jobs are derived from the repo itself.
|
|
9
|
+
*/
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import { renderGitHubWorkflow, renderGitLabCI } from '../../base/ci.js';
|
|
13
|
+
/** SwiftPM runs on macOS runners — Xcode isn't available anywhere else. */
|
|
14
|
+
const MACOS = 'macos-latest';
|
|
15
|
+
/**
|
|
16
|
+
* Pull the two facts CI needs out of a Package.swift. A regex rather than a
|
|
17
|
+
* SwiftPM invocation: `swift package dump-package` would need a Swift toolchain
|
|
18
|
+
* on the machine running `repo-tooling`, which is usually a Node environment.
|
|
19
|
+
*/
|
|
20
|
+
export function parsePackageSwift(contents) {
|
|
21
|
+
const products = [...contents.matchAll(/\.library\(\s*name:\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
22
|
+
// Only the `platforms:` array declares deployment targets — matching
|
|
23
|
+
// `.iOS(` across the whole manifest would also catch `.iOS` in target
|
|
24
|
+
// conditionals.
|
|
25
|
+
const platformsClause = contents.match(/platforms:\s*\[([^\]]*)\]/s)?.[1] ?? '';
|
|
26
|
+
const platforms = [...platformsClause.matchAll(/\.(iOS|macOS|tvOS|watchOS|visionOS)\s*\(/g)].map((m) => m[1]);
|
|
27
|
+
return { products, platforms: [...new Set(platforms)] };
|
|
28
|
+
}
|
|
29
|
+
export async function readSwiftPackage(dir) {
|
|
30
|
+
const filepath = path.join(dir, 'Package.swift');
|
|
31
|
+
if (!(await fs.pathExists(filepath)))
|
|
32
|
+
return { products: [], platforms: [] };
|
|
33
|
+
return parsePackageSwift(await fs.readFile(filepath, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
/** Checkout + pin Xcode. Every Swift job needs both before it can do anything. */
|
|
36
|
+
const XCODE_SETUP = ` - name: 📦 Checkout repository
|
|
37
|
+
uses: actions/checkout@v7
|
|
38
|
+
|
|
39
|
+
- name: 🛠️ Select latest stable Xcode
|
|
40
|
+
uses: maxim-lobanov/setup-xcode@v1
|
|
41
|
+
with:
|
|
42
|
+
xcode-version: latest-stable`;
|
|
43
|
+
/**
|
|
44
|
+
* SwiftPM's build products and the resolved-dependency clone are both expensive
|
|
45
|
+
* to rebuild; key on Package.resolved so the cache invalidates with the graph.
|
|
46
|
+
*/
|
|
47
|
+
const SPM_CACHE = ` - name: 📦 Cache SwiftPM
|
|
48
|
+
uses: actions/cache@v5
|
|
49
|
+
with:
|
|
50
|
+
path: |
|
|
51
|
+
.build
|
|
52
|
+
~/Library/Caches/org.swift.swiftpm
|
|
53
|
+
~/Library/Developer/Xcode/DerivedData
|
|
54
|
+
key: \${{ runner.os }}-spm-\${{ hashFiles('**/Package.resolved') }}
|
|
55
|
+
restore-keys: |
|
|
56
|
+
\${{ runner.os }}-spm-`;
|
|
57
|
+
export function swiftGithubJobs(pkg) {
|
|
58
|
+
const jobs = [
|
|
59
|
+
{
|
|
60
|
+
id: 'build-test',
|
|
61
|
+
runsOn: MACOS,
|
|
62
|
+
steps: `${XCODE_SETUP}
|
|
63
|
+
|
|
64
|
+
${SPM_CACHE}
|
|
65
|
+
|
|
66
|
+
- name: 🏗️ swift build
|
|
67
|
+
run: swift build
|
|
68
|
+
|
|
69
|
+
- name: 🧪 swift test
|
|
70
|
+
run: swift test`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'lint',
|
|
74
|
+
runsOn: MACOS,
|
|
75
|
+
steps: ` - name: 📦 Checkout repository
|
|
76
|
+
uses: actions/checkout@v7
|
|
77
|
+
|
|
78
|
+
- name: 📦 Install SwiftLint
|
|
79
|
+
run: brew install swiftlint
|
|
80
|
+
|
|
81
|
+
- name: 🔍 swiftlint --strict
|
|
82
|
+
run: swiftlint lint --strict --reporter github-actions-logging`,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
// Periphery is part of this module's standard (the `periphery` fixer ships
|
|
86
|
+
// its config alongside), so the job is unconditional. Gating it on
|
|
87
|
+
// .periphery.yml existing looked tidier but silently lost the job on a fresh
|
|
88
|
+
// repo: `fix` walks doctor's findings in order, and GitHub Actions comes
|
|
89
|
+
// before Periphery, so the CI file was written before the config existed.
|
|
90
|
+
jobs.push({
|
|
91
|
+
id: 'dead-code',
|
|
92
|
+
runsOn: MACOS,
|
|
93
|
+
// Informational: an established codebase almost always has unused
|
|
94
|
+
// declarations on day one, and a red X there trains people to ignore CI.
|
|
95
|
+
// Drop continue-on-error once the repo is clean.
|
|
96
|
+
extra: ' continue-on-error: true',
|
|
97
|
+
steps: `${XCODE_SETUP}
|
|
98
|
+
|
|
99
|
+
- name: 📦 Install Periphery
|
|
100
|
+
run: brew install periphery
|
|
101
|
+
|
|
102
|
+
- name: 🔍 periphery scan
|
|
103
|
+
run: periphery scan --strict --format github-actions`,
|
|
104
|
+
});
|
|
105
|
+
// xcodebuild per platform catches deployment-target breaks that `swift build`
|
|
106
|
+
// (host-platform only) never sees. Needs both a scheme and declared platforms.
|
|
107
|
+
const scheme = pkg.products[0];
|
|
108
|
+
if (scheme && pkg.platforms.length > 0) {
|
|
109
|
+
jobs.push({
|
|
110
|
+
id: 'platforms',
|
|
111
|
+
runsOn: MACOS,
|
|
112
|
+
extra: ` name: xcodebuild \${{ matrix.platform }}
|
|
113
|
+
strategy:
|
|
114
|
+
fail-fast: false
|
|
115
|
+
matrix:
|
|
116
|
+
platform:
|
|
117
|
+
${pkg.platforms.map((p) => ` - ${p}`).join('\n')}`,
|
|
118
|
+
steps: `${XCODE_SETUP}
|
|
119
|
+
|
|
120
|
+
- name: 📦 Cache DerivedData
|
|
121
|
+
uses: actions/cache@v5
|
|
122
|
+
with:
|
|
123
|
+
path: ~/Library/Developer/Xcode/DerivedData
|
|
124
|
+
key: \${{ runner.os }}-derived-\${{ matrix.platform }}-\${{ hashFiles('**/Package.resolved') }}
|
|
125
|
+
restore-keys: |
|
|
126
|
+
\${{ runner.os }}-derived-\${{ matrix.platform }}-
|
|
127
|
+
|
|
128
|
+
- name: 🏗️ Build for \${{ matrix.platform }}
|
|
129
|
+
run: |
|
|
130
|
+
set -o pipefail
|
|
131
|
+
xcodebuild \\
|
|
132
|
+
-scheme ${scheme} \\
|
|
133
|
+
-destination "generic/platform=\${{ matrix.platform }}" \\
|
|
134
|
+
-skipPackagePluginValidation \\
|
|
135
|
+
build | xcbeautify --renderer github-actions`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return jobs;
|
|
139
|
+
}
|
|
140
|
+
export function renderSwiftWorkflow(pkg) {
|
|
141
|
+
return renderGitHubWorkflow(swiftGithubJobs(pkg));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* GitLab runs Swift in the official Linux image. That rules out Xcode — so no
|
|
145
|
+
* platform matrix and no SwiftLint (a Homebrew/macOS tool in practice); the
|
|
146
|
+
* Linux-portable half of the pipeline is `swift build` + `swift test`.
|
|
147
|
+
*/
|
|
148
|
+
function swiftGitlabSpec() {
|
|
149
|
+
return {
|
|
150
|
+
image: 'swift:6.0',
|
|
151
|
+
preamble: `cache:
|
|
152
|
+
key:
|
|
153
|
+
files:
|
|
154
|
+
- Package.resolved
|
|
155
|
+
paths:
|
|
156
|
+
- .build`,
|
|
157
|
+
jobs: [
|
|
158
|
+
{ id: 'build', stage: 'build', script: ['swift build'] },
|
|
159
|
+
{ id: 'test', stage: 'test', script: ['swift test'] },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function renderSwiftGitLabCI() {
|
|
164
|
+
return renderGitLabCI(swiftGitlabSpec());
|
|
165
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift language module — fixers (#286). One per check in ./checks.ts.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import { buildPresetConfig } from '../../cli/commands/setup-presets.js';
|
|
8
|
+
import { copyPreset } from '../../cli/utils/copy-preset.js';
|
|
9
|
+
import { LOCKFILE_NAME, writeLockfile } from '../../cli/utils/lockfile.js';
|
|
10
|
+
import { readSwiftPackage, renderSwiftGitLabCI, renderSwiftWorkflow } from './ci.js';
|
|
11
|
+
import { SWIFT_HOOKS_DIR, installSwiftGitHooks } from './git-hooks.js';
|
|
12
|
+
import { ensureSwiftGitignore } from './gitignore.js';
|
|
13
|
+
export const SWIFT_FIXERS = [
|
|
14
|
+
{
|
|
15
|
+
target: 'swiftlint',
|
|
16
|
+
description: 'Scaffold .swiftlint.yml (the SwiftLint config swift-common runs)',
|
|
17
|
+
appliesTo: ['SwiftLint'],
|
|
18
|
+
outputs: ['.swiftlint.yml'],
|
|
19
|
+
canFixDrift: true,
|
|
20
|
+
async run({ targetDir }) {
|
|
21
|
+
const result = await copyPreset('swiftlint', targetDir);
|
|
22
|
+
return { filesWritten: [result.target] };
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
target: 'periphery',
|
|
27
|
+
description: 'Scaffold .periphery.yml (dead-code scan config, retains public API)',
|
|
28
|
+
appliesTo: ['Periphery'],
|
|
29
|
+
outputs: ['.periphery.yml'],
|
|
30
|
+
canFixDrift: true,
|
|
31
|
+
async run({ targetDir }) {
|
|
32
|
+
const result = await copyPreset('periphery', targetDir);
|
|
33
|
+
return { filesWritten: [result.target] };
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
target: 'swift-gitignore',
|
|
38
|
+
description: 'Add the Swift/SwiftPM build artefacts (.build, DerivedData, xcuserdata) to .gitignore',
|
|
39
|
+
appliesTo: ['Swift .gitignore'],
|
|
40
|
+
// Appends what's missing; never clobbers a project's own entries.
|
|
41
|
+
riskLevel: 'safe-merge',
|
|
42
|
+
outputs: ['.gitignore'],
|
|
43
|
+
canFixDrift: true,
|
|
44
|
+
async run({ targetDir }) {
|
|
45
|
+
return { filesWritten: await ensureSwiftGitignore(targetDir) };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
target: 'swift-git-hooks',
|
|
50
|
+
description: `Scaffold ${SWIFT_HOOKS_DIR}/pre-commit + pre-push (SwiftLint, swift build/test) and point git at them via core.hooksPath`,
|
|
51
|
+
appliesTo: ['Git hooks', 'Pre-push hook'],
|
|
52
|
+
outputs: [`${SWIFT_HOOKS_DIR}/pre-commit`, `${SWIFT_HOOKS_DIR}/pre-push`],
|
|
53
|
+
canFixDrift: true,
|
|
54
|
+
async run({ targetDir }) {
|
|
55
|
+
const { filesWritten, hooksPathSet } = await installSwiftGitHooks(targetDir);
|
|
56
|
+
console.log(hooksPathSet
|
|
57
|
+
? chalk.dim(` git config core.hooksPath ${SWIFT_HOOKS_DIR}`)
|
|
58
|
+
: chalk.yellow(` run \`git config core.hooksPath ${SWIFT_HOOKS_DIR}\` once per clone — it's local git config, not a committed file`));
|
|
59
|
+
return { filesWritten };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
target: 'swift-ci',
|
|
64
|
+
description: 'Scaffold .github/workflows/ci.yml for Swift (macOS runners: swift build/test, SwiftLint, xcodebuild per platform)',
|
|
65
|
+
appliesTo: ['GitHub Actions'],
|
|
66
|
+
outputs: ['.github/workflows/ci.yml'],
|
|
67
|
+
canFixDrift: true,
|
|
68
|
+
async run({ targetDir }) {
|
|
69
|
+
const workflowsDir = path.join(targetDir, '.github', 'workflows');
|
|
70
|
+
await fs.ensureDir(workflowsDir);
|
|
71
|
+
const workflow = renderSwiftWorkflow(await readSwiftPackage(targetDir));
|
|
72
|
+
await fs.writeFile(path.join(workflowsDir, 'ci.yml'), workflow);
|
|
73
|
+
return { filesWritten: ['.github/workflows/ci.yml'] };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
target: 'swift-gitlab-ci',
|
|
78
|
+
description: 'Scaffold .gitlab-ci.yml for Swift (swift build + test on the Linux Swift image)',
|
|
79
|
+
appliesTo: ['GitLab CI'],
|
|
80
|
+
outputs: ['.gitlab-ci.yml'],
|
|
81
|
+
canFixDrift: true,
|
|
82
|
+
async run({ targetDir }) {
|
|
83
|
+
await fs.writeFile(path.join(targetDir, '.gitlab-ci.yml'), renderSwiftGitLabCI());
|
|
84
|
+
return { filesWritten: ['.gitlab-ci.yml'] };
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
target: 'swift-lockfile',
|
|
89
|
+
description: `Scaffold ${LOCKFILE_NAME} recording the Swift tool choices`,
|
|
90
|
+
appliesTo: ['lockfile'],
|
|
91
|
+
outputs: [LOCKFILE_NAME],
|
|
92
|
+
riskLevel: 'safe-add',
|
|
93
|
+
canFixDrift: false,
|
|
94
|
+
async run({ targetDir }) {
|
|
95
|
+
// A Swift repo has no package.json to infer from, so record what `setup
|
|
96
|
+
// --preset swift-library` would have written — the same config, minus the
|
|
97
|
+
// scaffolding.
|
|
98
|
+
await writeLockfile(targetDir, buildPresetConfig('swift-library', path.basename(targetDir)));
|
|
99
|
+
return { filesWritten: [LOCKFILE_NAME] };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift git hooks (#309). Husky is an npm package, so a SwiftPM repo can't use
|
|
3
|
+
* it without dragging node into a toolchain that otherwise has none. The
|
|
4
|
+
* node-free equivalent is a committed `.githooks/` directory that git is pointed
|
|
5
|
+
* at with `core.hooksPath`.
|
|
6
|
+
*
|
|
7
|
+
* The hooks run the tools directly rather than a `verify` indirection: SwiftPM
|
|
8
|
+
* has no scripts field, and inventing a Makefile target would be a third place
|
|
9
|
+
* to keep the CI commands in sync (they already live in .github/workflows/ci.yml
|
|
10
|
+
* and .swiftlint.yml).
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'node:child_process';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
export const SWIFT_HOOKS_DIR = '.githooks';
|
|
16
|
+
// SwiftLint's --fix is the formatter (see ./checks.ts), so pre-commit formats
|
|
17
|
+
// then lints. --quiet keeps a clean commit from printing a wall of nothing.
|
|
18
|
+
export const SWIFT_PRE_COMMIT = `#!/bin/sh
|
|
19
|
+
set -e
|
|
20
|
+
swiftlint --fix --quiet
|
|
21
|
+
swiftlint lint --quiet
|
|
22
|
+
`;
|
|
23
|
+
export const SWIFT_PRE_PUSH = `#!/bin/sh
|
|
24
|
+
set -e
|
|
25
|
+
echo "🔍 Running pre-push verify..."
|
|
26
|
+
swift build
|
|
27
|
+
swift test
|
|
28
|
+
swiftlint lint --strict
|
|
29
|
+
echo "✅ Verify passed — pushing."
|
|
30
|
+
`;
|
|
31
|
+
/** Best-effort `git config` — a missing git or a non-repo dir is not a failure. */
|
|
32
|
+
function gitConfig(cwd, key, value) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
execFile('git', ['-C', cwd, 'config', key, value], (err) => resolve(!err));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Writes both hooks and points git at them. Returns the files written —
|
|
39
|
+
* `core.hooksPath` is per-clone local config, not a file, so it's reported
|
|
40
|
+
* separately rather than pretending to be a repo change.
|
|
41
|
+
*/
|
|
42
|
+
export async function installSwiftGitHooks(targetDir) {
|
|
43
|
+
const hooksDir = path.join(targetDir, SWIFT_HOOKS_DIR);
|
|
44
|
+
await fs.ensureDir(hooksDir);
|
|
45
|
+
const filesWritten = [];
|
|
46
|
+
for (const [name, content] of [
|
|
47
|
+
['pre-commit', SWIFT_PRE_COMMIT],
|
|
48
|
+
['pre-push', SWIFT_PRE_PUSH],
|
|
49
|
+
]) {
|
|
50
|
+
const hookPath = path.join(hooksDir, name);
|
|
51
|
+
await fs.writeFile(hookPath, content);
|
|
52
|
+
await fs.chmod(hookPath, 0o755);
|
|
53
|
+
filesWritten.push(`${SWIFT_HOOKS_DIR}/${name}`);
|
|
54
|
+
}
|
|
55
|
+
// Guard on .git: `fix --diff` shadow-runs fixers in a temp copy that excludes
|
|
56
|
+
// .git, and that copy can land inside another repo — an unguarded `git config`
|
|
57
|
+
// there would rewrite the *parent* repo's hooksPath during a mere preview.
|
|
58
|
+
const isRepo = await fs.pathExists(path.join(targetDir, '.git'));
|
|
59
|
+
const hooksPathSet = isRepo
|
|
60
|
+
? await gitConfig(targetDir, 'core.hooksPath', SWIFT_HOOKS_DIR)
|
|
61
|
+
: false;
|
|
62
|
+
return { filesWritten, hooksPathSet };
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Swift .gitignore block, shared by the `swift-gitignore` fixer and the
|
|
3
|
+
* scaffolder. Its own module so `scaffold.ts` doesn't have to import from
|
|
4
|
+
* `fixers.ts` — that edge closed an import cycle once `fixers.ts` started
|
|
5
|
+
* reading the lockfile (lockfile → setup-presets → scaffold → fixers → lockfile).
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
/**
|
|
10
|
+
* The Swift build artefacts that must stay out of git. Appended to an existing
|
|
11
|
+
* .gitignore rather than replacing it — a Swift repo's ignore file usually
|
|
12
|
+
* carries project-specific entries worth keeping.
|
|
13
|
+
*/
|
|
14
|
+
const SWIFT_GITIGNORE_BLOCK = `# Swift / SwiftPM
|
|
15
|
+
.DS_Store
|
|
16
|
+
/.build
|
|
17
|
+
/Packages
|
|
18
|
+
/*.xcodeproj
|
|
19
|
+
xcuserdata/
|
|
20
|
+
DerivedData/
|
|
21
|
+
.swiftpm/config/registries.json
|
|
22
|
+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
23
|
+
.netrc
|
|
24
|
+
`;
|
|
25
|
+
/** Entries whose presence means the block (or an equivalent) is already there. */
|
|
26
|
+
const SENTINELS = ['.build', 'DerivedData', 'xcuserdata'];
|
|
27
|
+
export async function ensureSwiftGitignore(targetDir) {
|
|
28
|
+
const filepath = path.join(targetDir, '.gitignore');
|
|
29
|
+
if (!(await fs.pathExists(filepath))) {
|
|
30
|
+
await fs.writeFile(filepath, SWIFT_GITIGNORE_BLOCK);
|
|
31
|
+
return ['.gitignore'];
|
|
32
|
+
}
|
|
33
|
+
const existing = await fs.readFile(filepath, 'utf-8');
|
|
34
|
+
const missing = SENTINELS.filter((entry) => !existing.includes(entry));
|
|
35
|
+
if (missing.length === 0)
|
|
36
|
+
return [];
|
|
37
|
+
// Append only what's absent, so a repo that already ignores .build doesn't
|
|
38
|
+
// get a duplicate entry for it.
|
|
39
|
+
const additions = SWIFT_GITIGNORE_BLOCK.split('\n').filter((line) => line.length > 0 && !existing.includes(line));
|
|
40
|
+
const separator = existing.endsWith('\n') ? '' : '\n';
|
|
41
|
+
await fs.writeFile(filepath, `${existing}${separator}\n${additions.join('\n')}\n`);
|
|
42
|
+
return ['.gitignore'];
|
|
43
|
+
}
|