@mui/internal-code-infra 0.0.4-canary.4 → 0.0.4-canary.41
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 +19 -8
- package/build/babel-config.d.mts +11 -3
- package/build/brokenLinksChecker/crawlWorker.d.mts +1 -0
- package/build/brokenLinksChecker/index.d.mts +45 -2
- package/build/changelog/types.d.ts +1 -1
- package/build/cli/cmdArgosPush.d.mts +2 -2
- package/build/cli/cmdBuild.d.mts +2 -2
- package/build/cli/cmdCopyFiles.d.mts +2 -2
- package/build/cli/cmdExtractErrorCodes.d.mts +2 -2
- package/build/cli/cmdGenerateChangelog.d.mts +2 -2
- package/build/cli/cmdGithubAuth.d.mts +2 -2
- package/build/cli/cmdListWorkspaces.d.mts +4 -2
- package/build/cli/cmdNetlifyIgnore.d.mts +2 -2
- package/build/cli/cmdPublish.d.mts +4 -2
- package/build/cli/cmdPublishCanary.d.mts +3 -2
- package/build/cli/cmdPublishNewPackage.d.mts +4 -2
- package/build/cli/cmdSetVersionOverrides.d.mts +2 -2
- package/build/cli/cmdVale.d.mts +46 -0
- package/build/cli/cmdValidateBuiltTypes.d.mts +2 -2
- package/build/eslint/baseConfig.d.mts +3 -1
- package/build/eslint/mui/rules/disallow-react-api-in-server-components.d.mts +2 -2
- package/build/eslint/mui/rules/docgen-ignore-before-comment.d.mts +2 -2
- package/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
- package/build/eslint/mui/rules/no-restricted-resolved-imports.d.mts +2 -2
- package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
- package/build/markdownlint/duplicate-h1.d.mts +1 -1
- package/build/markdownlint/git-diff.d.mts +1 -1
- package/build/markdownlint/index.d.mts +1 -1
- package/build/markdownlint/straight-quotes.d.mts +1 -1
- package/build/markdownlint/table-alignment.d.mts +1 -1
- package/build/markdownlint/terminal-language.d.mts +1 -1
- package/build/remark/config.d.mts +43 -0
- package/build/remark/createLintTester.d.mts +10 -0
- package/build/remark/firstBlockHeading.d.mts +4 -0
- package/build/remark/gitDiff.d.mts +2 -0
- package/build/remark/noSpaceInLinks.d.mts +2 -0
- package/build/remark/straightQuotes.d.mts +2 -0
- package/build/remark/tableAlignment.d.mts +2 -0
- package/build/remark/terminalLanguage.d.mts +2 -0
- package/build/utils/build.d.mts +3 -3
- package/build/utils/github.d.mts +1 -1
- package/build/utils/pnpm.d.mts +68 -2
- package/build/utils/testUtils.d.mts +7 -0
- package/package.json +59 -32
- package/src/babel-config.mjs +9 -3
- package/src/brokenLinksChecker/__fixtures__/static-site/index.html +1 -0
- package/src/brokenLinksChecker/__fixtures__/static-site/invalid-html.html +15 -0
- package/src/brokenLinksChecker/crawlWorker.mjs +212 -0
- package/src/brokenLinksChecker/index.mjs +215 -164
- package/src/brokenLinksChecker/index.test.ts +43 -13
- package/src/changelog/categorizeCommits.test.ts +5 -5
- package/src/changelog/fetchChangelogs.mjs +6 -2
- package/src/changelog/parseCommitLabels.test.ts +5 -5
- package/src/changelog/renderChangelog.mjs +1 -1
- package/src/changelog/types.ts +1 -1
- package/src/cli/cmdListWorkspaces.mjs +9 -2
- package/src/cli/cmdNetlifyIgnore.mjs +4 -88
- package/src/cli/cmdPublish.mjs +51 -14
- package/src/cli/cmdPublishCanary.mjs +139 -107
- package/src/cli/cmdPublishNewPackage.mjs +27 -6
- package/src/cli/cmdVale.mjs +513 -0
- package/src/cli/cmdVale.test.mjs +644 -0
- package/src/cli/index.mjs +2 -0
- package/src/eslint/baseConfig.mjs +45 -20
- package/src/eslint/docsConfig.mjs +2 -1
- package/src/eslint/jsonConfig.mjs +2 -1
- package/src/eslint/mui/config.mjs +20 -1
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
- package/src/eslint/mui/rules/no-guarded-throw.test.mjs +206 -0
- package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
- package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
- package/src/eslint/testConfig.mjs +2 -1
- package/src/estree-typescript.d.ts +1 -1
- package/src/remark/config.mjs +157 -0
- package/src/remark/createLintTester.mjs +19 -0
- package/src/remark/firstBlockHeading.mjs +87 -0
- package/src/remark/firstBlockHeading.test.mjs +107 -0
- package/src/remark/gitDiff.mjs +43 -0
- package/src/remark/gitDiff.test.mjs +45 -0
- package/src/remark/noSpaceInLinks.mjs +42 -0
- package/src/remark/noSpaceInLinks.test.mjs +22 -0
- package/src/remark/straightQuotes.mjs +31 -0
- package/src/remark/straightQuotes.test.mjs +25 -0
- package/src/remark/tableAlignment.mjs +23 -0
- package/src/remark/tableAlignment.test.mjs +28 -0
- package/src/remark/terminalLanguage.mjs +19 -0
- package/src/remark/terminalLanguage.test.mjs +17 -0
- package/src/untyped-plugins.d.ts +11 -11
- package/src/utils/build.test.mjs +546 -575
- package/src/utils/pnpm.mjs +192 -3
- package/src/utils/pnpm.test.mjs +580 -0
- package/src/utils/testUtils.mjs +18 -0
- package/src/utils/typescript.test.mjs +249 -272
- package/vale/.vale.ini +1 -0
- package/vale/styles/MUI/CorrectReferenceAllCases.yml +43 -0
- package/vale/styles/MUI/CorrectRererenceCased.yml +14 -0
- package/vale/styles/MUI/GoogleLatin.yml +11 -0
- package/vale/styles/MUI/MuiBrandName.yml +22 -0
- package/vale/styles/MUI/NoBritish.yml +112 -0
- package/vale/styles/MUI/NoCompanyName.yml +17 -0
|
@@ -16,17 +16,34 @@ import { getWorkspacePackages } from '../utils/pnpm.mjs';
|
|
|
16
16
|
/**
|
|
17
17
|
* @typedef {Object} Args
|
|
18
18
|
* @property {boolean} [dryRun] If true, will only log the commands without executing them
|
|
19
|
+
* @property {string} [otp] 6 digit auth code to forward to npm for two-factor authentication
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
22
23
|
command: 'publish-new-package [pkg...]',
|
|
23
24
|
describe: 'Publish new empty package(s) to the npm registry.',
|
|
24
25
|
builder: (yargs) =>
|
|
25
|
-
yargs
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
yargs
|
|
27
|
+
.option('dryRun', {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
default: false,
|
|
30
|
+
description: 'If true, will only log the commands without executing them.',
|
|
31
|
+
})
|
|
32
|
+
.option('otp', {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: '6 digit auth code to forward to npm for two-factor authentication.',
|
|
35
|
+
coerce: (value) => {
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!/^\d{6}$/.test(value)) {
|
|
41
|
+
throw new Error('The --otp option must be a 6 digit number.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return value;
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
30
47
|
async handler(args) {
|
|
31
48
|
console.log(`🔍 Detecting new packages to publish in workspace...`);
|
|
32
49
|
const newPackages = await getWorkspacePackages({ nonPublishedOnly: true });
|
|
@@ -62,7 +79,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
62
79
|
version: '0.0.1',
|
|
63
80
|
repository: {
|
|
64
81
|
type: 'git',
|
|
65
|
-
url: `git+https://github.com/${repo.owner}/${repo.
|
|
82
|
+
url: `git+https://github.com/${repo.owner}/${repo.repo}.git`,
|
|
66
83
|
directory: toPosixPath(path.relative(workspaceDir, pkg.path)),
|
|
67
84
|
},
|
|
68
85
|
};
|
|
@@ -78,8 +95,12 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
|
78
95
|
if (args.dryRun) {
|
|
79
96
|
publishArgs.push('--dry-run');
|
|
80
97
|
}
|
|
98
|
+
if (args.otp) {
|
|
99
|
+
publishArgs.push('--otp', args.otp);
|
|
100
|
+
}
|
|
81
101
|
await $({
|
|
82
102
|
cwd: newPkgDir,
|
|
103
|
+
stdio: 'inherit',
|
|
83
104
|
})`npm publish --access public --tag=canary ${publishArgs}`;
|
|
84
105
|
console.log(
|
|
85
106
|
`✅ ${args.dryRun ? '[Dry run] ' : ''}Published ${chalk.bold(`${pkg.name}@${packageJson.version}`)} to npm registry.`,
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
|
|
9
|
+
import { $ } from 'execa';
|
|
10
|
+
import { mapConcurrently } from '../utils/build.mjs';
|
|
11
|
+
|
|
12
|
+
const LATEST_RELEASE_URL = 'https://api.github.com/repos/vale-cli/vale/releases/latest';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetches the latest vale release version tag from GitHub.
|
|
16
|
+
* @returns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
async function fetchLatestVersion() {
|
|
19
|
+
const response = await fetchOrThrow(LATEST_RELEASE_URL);
|
|
20
|
+
const data = /** @type {{ tag_name: string }} */ (await response.json());
|
|
21
|
+
// tag_name is like "v3.14.1", strip the leading "v"
|
|
22
|
+
return data.tag_name.replace(/^v/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {'Linux' | 'macOS' | 'Windows'} ValeOS
|
|
27
|
+
* @typedef {'64-bit' | 'arm64'} ValeArch
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detects the OS name used in vale release filenames.
|
|
32
|
+
* @returns {ValeOS}
|
|
33
|
+
*/
|
|
34
|
+
function detectOS() {
|
|
35
|
+
const platform = os.platform();
|
|
36
|
+
if (platform === 'linux') {
|
|
37
|
+
return 'Linux';
|
|
38
|
+
}
|
|
39
|
+
if (platform === 'darwin') {
|
|
40
|
+
return 'macOS';
|
|
41
|
+
}
|
|
42
|
+
if (platform === 'win32') {
|
|
43
|
+
return 'Windows';
|
|
44
|
+
}
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Unsupported platform: ${platform}. Vale only supports Linux, macOS, and Windows.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detects the CPU architecture used in vale release filenames.
|
|
52
|
+
* Exits with an error for 32-bit systems.
|
|
53
|
+
* @returns {ValeArch}
|
|
54
|
+
*/
|
|
55
|
+
function detectArch() {
|
|
56
|
+
const arch = os.arch();
|
|
57
|
+
if (arch === 'arm64') {
|
|
58
|
+
return 'arm64';
|
|
59
|
+
}
|
|
60
|
+
if (arch === 'x64') {
|
|
61
|
+
return '64-bit';
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unsupported architecture: ${arch}. Vale requires a 64-bit system (x64 or arm64).`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the archive filename for the current platform.
|
|
70
|
+
* @param {ValeOS} valeOS
|
|
71
|
+
* @param {ValeArch} valeArch
|
|
72
|
+
* @returns {{ filename: string; isZip: boolean }}
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Returns the base GitHub release download URL for a given vale version.
|
|
76
|
+
* @param {string} version
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function getReleasesBase(version) {
|
|
80
|
+
return `https://github.com/vale-cli/vale/releases/download/v${version}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the archive filename for the current platform.
|
|
85
|
+
* @param {string} version
|
|
86
|
+
* @param {ValeOS} valeOS
|
|
87
|
+
* @param {ValeArch} valeArch
|
|
88
|
+
* @returns {{ filename: string; isZip: boolean }}
|
|
89
|
+
*/
|
|
90
|
+
function getArchiveInfo(version, valeOS, valeArch) {
|
|
91
|
+
const isZip = valeOS === 'Windows';
|
|
92
|
+
const ext = isZip ? '.zip' : '.tar.gz';
|
|
93
|
+
const filename = `vale_${version}_${valeOS}_${valeArch}${ext}`;
|
|
94
|
+
return { filename, isZip };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fetches a URL and returns the Response, throwing on non-OK status.
|
|
99
|
+
* @param {string} url
|
|
100
|
+
* @returns {Promise<Response>}
|
|
101
|
+
*/
|
|
102
|
+
async function fetchOrThrow(url) {
|
|
103
|
+
const response = await fetch(url);
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
106
|
+
}
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Downloads the checksums file and parses it into a map of filename -> sha256 hex.
|
|
112
|
+
* @param {string} version
|
|
113
|
+
* @returns {Promise<Map<string, string>>}
|
|
114
|
+
*/
|
|
115
|
+
async function fetchChecksums(version) {
|
|
116
|
+
const checksumsUrl = `${getReleasesBase(version)}/vale_${version}_checksums.txt`;
|
|
117
|
+
const response = await fetchOrThrow(checksumsUrl);
|
|
118
|
+
const text = await response.text();
|
|
119
|
+
/** @type {Map<string, string>} */
|
|
120
|
+
const map = new Map();
|
|
121
|
+
for (const line of text.trim().split('\n')) {
|
|
122
|
+
const [hash, name] = line.trim().split(/\s+/);
|
|
123
|
+
if (hash && name) {
|
|
124
|
+
map.set(name, hash);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return map;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Computes the SHA-256 hex digest of a file.
|
|
132
|
+
* @param {string} filePath
|
|
133
|
+
* @returns {Promise<string>}
|
|
134
|
+
*/
|
|
135
|
+
async function sha256File(filePath) {
|
|
136
|
+
const hash = crypto.createHash('sha256');
|
|
137
|
+
const fileHandle = await fs.open(filePath, 'r');
|
|
138
|
+
const stream = fileHandle.createReadStream();
|
|
139
|
+
await pipeline(stream, hash);
|
|
140
|
+
return hash.digest('hex');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Downloads a URL to a local file path, showing progress.
|
|
145
|
+
* @param {string} url
|
|
146
|
+
* @param {string} destPath
|
|
147
|
+
* @param {string} version
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
async function downloadFile(url, destPath, version) {
|
|
151
|
+
const response = await fetchOrThrow(url);
|
|
152
|
+
const totalBytes = Number(response.headers.get('content-length') ?? 0);
|
|
153
|
+
let downloadedBytes = 0;
|
|
154
|
+
|
|
155
|
+
if (!response.body) {
|
|
156
|
+
throw new Error(`No response body for ${url}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const writeStream = createWriteStream(destPath);
|
|
160
|
+
|
|
161
|
+
// Pipe the web ReadableStream into a Node.js WriteStream, tracking progress
|
|
162
|
+
const nodeReadable = /** @type {import('node:stream').Readable} */ (
|
|
163
|
+
/** @type {unknown} */ (response.body)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await pipeline(
|
|
167
|
+
nodeReadable,
|
|
168
|
+
async function* trackProgress(source) {
|
|
169
|
+
for await (const chunk of source) {
|
|
170
|
+
const bytes = /** @type {Buffer} */ (chunk);
|
|
171
|
+
downloadedBytes += bytes.length;
|
|
172
|
+
if (totalBytes > 0) {
|
|
173
|
+
const pct = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
174
|
+
process.stdout.write(`\rDownloading vale v${version}... ${pct}%`);
|
|
175
|
+
}
|
|
176
|
+
yield bytes;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
writeStream,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (totalBytes > 0) {
|
|
183
|
+
process.stdout.write('\n');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extracts a .tar.gz archive to a destination directory using the built-in tar command.
|
|
189
|
+
* @param {string} archivePath
|
|
190
|
+
* @param {string} destDir
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
async function extractTarGz(archivePath, destDir) {
|
|
194
|
+
await $({ stdio: 'inherit' })`tar -xzf ${archivePath} -C ${destDir}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extracts a .zip archive to a destination directory using the built-in unzip command.
|
|
199
|
+
* @param {string} archivePath
|
|
200
|
+
* @param {string} destDir
|
|
201
|
+
* @returns {Promise<void>}
|
|
202
|
+
*/
|
|
203
|
+
async function extractZip(archivePath, destDir) {
|
|
204
|
+
await $({ stdio: 'inherit' })`unzip -o ${archivePath} -d ${destDir}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns the path to the vale binary, downloading it first if necessary.
|
|
209
|
+
* The binary is cached under `node_modules/.cache/mui-vale/<version>/`.
|
|
210
|
+
* @param {string} version
|
|
211
|
+
* @returns {Promise<string>}
|
|
212
|
+
*/
|
|
213
|
+
async function ensureValeBinary(version) {
|
|
214
|
+
const valeOS = detectOS();
|
|
215
|
+
const valeArch = detectArch();
|
|
216
|
+
const { filename, isZip } = getArchiveInfo(version, valeOS, valeArch);
|
|
217
|
+
|
|
218
|
+
const binaryName = valeOS === 'Windows' ? 'vale.exe' : 'vale';
|
|
219
|
+
const workspaceDir = await findWorkspaceDir(process.cwd());
|
|
220
|
+
if (!workspaceDir) {
|
|
221
|
+
throw new Error('Could not find pnpm workspace root');
|
|
222
|
+
}
|
|
223
|
+
const cacheDir = path.join(workspaceDir, 'node_modules', '.cache', 'mui-vale', version);
|
|
224
|
+
const binaryPath = path.join(cacheDir, binaryName);
|
|
225
|
+
|
|
226
|
+
// Return cached binary if it already exists
|
|
227
|
+
const binaryExists = await fs
|
|
228
|
+
.stat(binaryPath)
|
|
229
|
+
.then((s) => s.isFile())
|
|
230
|
+
.catch(() => false);
|
|
231
|
+
if (binaryExists) {
|
|
232
|
+
return binaryPath;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(`Vale v${version} not found in cache. Downloading...`);
|
|
236
|
+
|
|
237
|
+
// Fetch checksums first
|
|
238
|
+
const checksums = await fetchChecksums(version);
|
|
239
|
+
const expectedChecksum = checksums.get(filename);
|
|
240
|
+
if (!expectedChecksum) {
|
|
241
|
+
throw new Error(`No checksum found for ${filename} in checksums file.`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
245
|
+
|
|
246
|
+
const archivePath = path.join(cacheDir, filename);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const downloadUrl = `${getReleasesBase(version)}/${filename}`;
|
|
250
|
+
await downloadFile(downloadUrl, archivePath, version);
|
|
251
|
+
|
|
252
|
+
// Verify checksum
|
|
253
|
+
console.log('Verifying checksum...');
|
|
254
|
+
const actualChecksum = await sha256File(archivePath);
|
|
255
|
+
if (actualChecksum !== expectedChecksum) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Checksum mismatch for ${filename}.\n Expected: ${expectedChecksum}\n Got: ${actualChecksum}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
console.log('Checksum verified.');
|
|
261
|
+
|
|
262
|
+
// Extract archive
|
|
263
|
+
console.log(`Extracting ${filename}...`);
|
|
264
|
+
if (isZip) {
|
|
265
|
+
await extractZip(archivePath, cacheDir);
|
|
266
|
+
} else {
|
|
267
|
+
await extractTarGz(archivePath, cacheDir);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Make the binary executable on Unix
|
|
271
|
+
if (valeOS !== 'Windows') {
|
|
272
|
+
await fs.chmod(binaryPath, 0o755);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`Vale v${version} ready at ${binaryPath}`);
|
|
276
|
+
} finally {
|
|
277
|
+
// Clean up the downloaded archive regardless of success or failure
|
|
278
|
+
await fs.rm(archivePath, { force: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return binaryPath;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Runs the vale binary, forwarding all extra args and inheriting stdio.
|
|
286
|
+
* @param {string} binaryPath
|
|
287
|
+
* @param {string[]} valeArgs
|
|
288
|
+
* @returns {Promise<void>}
|
|
289
|
+
*/
|
|
290
|
+
async function runVale(binaryPath, valeArgs) {
|
|
291
|
+
const result = await $({ stdio: 'inherit', reject: false })`${binaryPath} ${valeArgs}`;
|
|
292
|
+
process.exitCode = result.exitCode;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Runs vale with JSON output and captures the results.
|
|
297
|
+
* @param {string} binaryPath
|
|
298
|
+
* @param {string[]} valeArgs
|
|
299
|
+
* @returns {Promise<Record<string, Array<{ Action: { Name: string; Params: string[] | null }; Span: [number, number]; Check: string; Message: string; Severity: string; Match: string; Line: number }>>>}
|
|
300
|
+
*/
|
|
301
|
+
async function runValeJSON(binaryPath, valeArgs) {
|
|
302
|
+
const result = await $({ reject: false })`${binaryPath} --output JSON ${valeArgs}`;
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(result.stdout);
|
|
305
|
+
} catch {
|
|
306
|
+
throw new Error(`Failed to parse vale JSON output: ${result.stdout}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Extracts the replacement text from a vale alert.
|
|
312
|
+
* Returns null if no replacement can be determined.
|
|
313
|
+
* @param {{ Action: { Name: string; Params: string[] | null }; Message: string }} alert
|
|
314
|
+
* @returns {string | null}
|
|
315
|
+
*/
|
|
316
|
+
export function getReplacementText(alert) {
|
|
317
|
+
// If vale provides an explicit action with replacement params, use that
|
|
318
|
+
if (alert.Action.Name === 'replace' && alert.Action.Params && alert.Action.Params.length > 0) {
|
|
319
|
+
return alert.Action.Params[0];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Otherwise, try to extract from the message pattern.
|
|
323
|
+
// Vale messages follow patterns like:
|
|
324
|
+
// "Use 'X' instead of 'Y'"
|
|
325
|
+
// "Use the US spelling 'X' instead of the British 'Y'"
|
|
326
|
+
// "Use a non-breaking space ... ('X' instead of 'Y')"
|
|
327
|
+
const match = alert.Message.match(/'([^']+)'\s+instead\s+of\s+.*?'([^']+)'/);
|
|
328
|
+
if (match) {
|
|
329
|
+
return match[1];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Applies auto-fixes from vale alerts to the source files.
|
|
337
|
+
* Processes alerts in reverse order within each line to preserve column positions.
|
|
338
|
+
* @param {Record<string, Array<{ Action: { Name: string; Params: string[] | null }; Span: [number, number]; Check: string; Message: string; Severity: string; Match: string; Line: number }>>} results
|
|
339
|
+
* @param {'all' | 'error'} fixLevel
|
|
340
|
+
* @returns {Promise<{ fixed: number; skipped: number }>}
|
|
341
|
+
*/
|
|
342
|
+
export async function applyFixes(results, fixLevel) {
|
|
343
|
+
const entries = Object.entries(results);
|
|
344
|
+
|
|
345
|
+
const perFileResults = await mapConcurrently(
|
|
346
|
+
entries,
|
|
347
|
+
async ([filePath, alerts]) => {
|
|
348
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
349
|
+
const lines = content.split('\n');
|
|
350
|
+
|
|
351
|
+
// Filter alerts by severity and whether we can determine a replacement
|
|
352
|
+
const fixableAlerts = alerts.filter((alert) => {
|
|
353
|
+
if (fixLevel === 'error' && alert.Severity !== 'error') {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
return getReplacementText(alert) !== null;
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (fixableAlerts.length === 0) {
|
|
360
|
+
return { fixed: 0, skipped: alerts.length };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Deduplicate alerts at the same location (same line + span), keeping the first one
|
|
364
|
+
/** @type {Map<string, typeof fixableAlerts[0]>} */
|
|
365
|
+
const uniqueAlerts = new Map();
|
|
366
|
+
for (const alert of fixableAlerts) {
|
|
367
|
+
const key = `${alert.Line}:${alert.Span[0]}:${alert.Span[1]}`;
|
|
368
|
+
if (!uniqueAlerts.has(key)) {
|
|
369
|
+
uniqueAlerts.set(key, alert);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Sort by line ascending, then by span start descending (so we apply right-to-left)
|
|
374
|
+
const sortedAlerts = [...uniqueAlerts.values()].sort((a, b) => {
|
|
375
|
+
if (a.Line !== b.Line) {
|
|
376
|
+
return a.Line - b.Line;
|
|
377
|
+
}
|
|
378
|
+
return b.Span[0] - a.Span[0];
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Group alerts by line number
|
|
382
|
+
/** @type {Map<number, typeof sortedAlerts>} */
|
|
383
|
+
const alertsByLine = new Map();
|
|
384
|
+
for (const alert of sortedAlerts) {
|
|
385
|
+
const lineAlerts = alertsByLine.get(alert.Line) ?? [];
|
|
386
|
+
lineAlerts.push(alert);
|
|
387
|
+
alertsByLine.set(alert.Line, lineAlerts);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Apply fixes line by line, right-to-left within each line
|
|
391
|
+
let fileFixed = 0;
|
|
392
|
+
for (const [lineNum, lineAlerts] of alertsByLine) {
|
|
393
|
+
let line = lines[lineNum - 1]; // Line is 1-based
|
|
394
|
+
for (const alert of lineAlerts) {
|
|
395
|
+
const replacement = /** @type {string} */ (getReplacementText(alert));
|
|
396
|
+
// Span is 1-based [start, end] inclusive
|
|
397
|
+
const start = alert.Span[0] - 1;
|
|
398
|
+
const end = alert.Span[1];
|
|
399
|
+
line = line.slice(0, start) + replacement + line.slice(end);
|
|
400
|
+
fileFixed += 1;
|
|
401
|
+
}
|
|
402
|
+
lines[lineNum - 1] = line;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
406
|
+
|
|
407
|
+
return { fixed: fileFixed, skipped: alerts.length - fixableAlerts.length };
|
|
408
|
+
},
|
|
409
|
+
10,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
let fixed = 0;
|
|
413
|
+
let skipped = 0;
|
|
414
|
+
for (const result of perFileResults) {
|
|
415
|
+
if (result instanceof Error) {
|
|
416
|
+
throw result;
|
|
417
|
+
}
|
|
418
|
+
fixed += result.fixed;
|
|
419
|
+
skipped += result.skipped;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { fixed, skipped };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @typedef {{ 'vale-version': string; 'auto-fix': 'all' | 'error' | undefined; }} Args
|
|
427
|
+
*/
|
|
428
|
+
|
|
429
|
+
export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
|
|
430
|
+
command: 'vale',
|
|
431
|
+
describe:
|
|
432
|
+
'Download and run vale (a prose linter). All arguments are forwarded to the vale binary.',
|
|
433
|
+
builder: (yargs) => {
|
|
434
|
+
return (
|
|
435
|
+
yargs
|
|
436
|
+
// because `version` is taken over yargs itself
|
|
437
|
+
.option('vale-version', {
|
|
438
|
+
type: 'string',
|
|
439
|
+
demandOption: true,
|
|
440
|
+
description: 'The vale version to use (e.g. "3.14.1" or "latest").',
|
|
441
|
+
default: 'latest',
|
|
442
|
+
})
|
|
443
|
+
.option('auto-fix', {
|
|
444
|
+
type: 'string',
|
|
445
|
+
choices: ['all', 'error'],
|
|
446
|
+
description:
|
|
447
|
+
'Automatically apply fixes suggested by vale. "all" fixes both errors and warnings, "error" fixes only errors.',
|
|
448
|
+
})
|
|
449
|
+
.example('$0 vale --vale-version 3.14.1 docs/', 'Lint all files in the docs/ directory')
|
|
450
|
+
.example(
|
|
451
|
+
'$0 vale --vale-version 3.14.1 --auto-fix=all docs/',
|
|
452
|
+
'Lint and auto-fix all issues in docs/',
|
|
453
|
+
)
|
|
454
|
+
.strict(false)
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
handler: async (args) => {
|
|
458
|
+
const version = args.valeVersion === 'latest' ? await fetchLatestVersion() : args.valeVersion;
|
|
459
|
+
const binaryPath = await ensureValeBinary(version);
|
|
460
|
+
|
|
461
|
+
// Collect everything from process.argv that follows the "vale" token,
|
|
462
|
+
// excluding our own flags and their values.
|
|
463
|
+
const argvAfterVale = process.argv.slice(process.argv.indexOf('vale') + 1);
|
|
464
|
+
const valeArgs = [];
|
|
465
|
+
for (let i = 0; i < argvAfterVale.length; i += 1) {
|
|
466
|
+
const arg = argvAfterVale[i];
|
|
467
|
+
if (arg === '--vale-version' || arg.startsWith('--vale-version=')) {
|
|
468
|
+
// consumed by this wrapper; skip the next arg if it's the value
|
|
469
|
+
if (
|
|
470
|
+
arg === '--vale-version' &&
|
|
471
|
+
argvAfterVale[i + 1] &&
|
|
472
|
+
!argvAfterVale[i + 1].startsWith('--')
|
|
473
|
+
) {
|
|
474
|
+
i += 1;
|
|
475
|
+
}
|
|
476
|
+
} else if (arg === '--auto-fix' || arg.startsWith('--auto-fix=')) {
|
|
477
|
+
// consumed by this wrapper; skip the next arg if it's the value
|
|
478
|
+
if (
|
|
479
|
+
arg === '--auto-fix' &&
|
|
480
|
+
argvAfterVale[i + 1] &&
|
|
481
|
+
!argvAfterVale[i + 1].startsWith('--')
|
|
482
|
+
) {
|
|
483
|
+
i += 1;
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
valeArgs.push(arg);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (args.autoFix) {
|
|
491
|
+
const fixLevel = /** @type {'all' | 'error'} */ (args.autoFix);
|
|
492
|
+
const results = await runValeJSON(binaryPath, valeArgs);
|
|
493
|
+
const totalAlerts = Object.values(results).reduce((sum, alerts) => sum + alerts.length, 0);
|
|
494
|
+
|
|
495
|
+
if (totalAlerts === 0) {
|
|
496
|
+
console.log('No issues found by vale.');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { fixed, skipped } = await applyFixes(results, fixLevel);
|
|
501
|
+
console.log(`Auto-fix complete: ${fixed} fixed, ${skipped} skipped.`);
|
|
502
|
+
|
|
503
|
+
if (fixed > 0) {
|
|
504
|
+
// Re-run vale to show remaining issues
|
|
505
|
+
console.log('\nRemaining issues after auto-fix:');
|
|
506
|
+
await runVale(binaryPath, valeArgs);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
await runVale(binaryPath, valeArgs);
|
|
512
|
+
},
|
|
513
|
+
});
|