Skip to content

Commit

Permalink
Testing from one of SEEK's internal repos
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronMoat committed Aug 11, 2024
1 parent 1084b80 commit 725fb5b
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 23 deletions.
20 changes: 20 additions & 0 deletions .changeset/rich-chairs-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'skuba': minor
---

Replace `.buildkite/` files with duplicated YAML merge keys, for example:

```yaml
# Before
- <<: *deploy
<<: *docker
label: stuff

# After
- <<: [*deploy, *docker]
label: stuff
```
This should have no functional change, and is to support standardised YAML parsing across different tools, including the latest ESLint upgrades.
This migration will not be capture all cases of this (e.g. if there are keys between the merge keys). If you have other cases, update them following the example above.
22 changes: 20 additions & 2 deletions src/cli/adapter/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,29 @@ export const runESLint = async (

const start = process.hrtime.bigint();

const [formatter, results] = await Promise.all([
const [formatter, { type, results }] = await Promise.all([
engine.loadFormatter(),
engine.lintFiles([]),
engine
.lintFiles([])
.then((r) => ({ type: 'results', results: r }) as const)
.catch((error) => {
if (
error instanceof Error &&
error.message === 'Could not find config file.'
) {
return { type: 'no-config', results: undefined } as const;
}
throw error;
}),
]);

if (type === 'no-config') {
logger.plain(
'skuba could not find an eslint config file. Do you need to run format or configure?',
);
return { ok: false, fixable: false, errors: [], warnings: [], output: '' };
}

const end = process.hrtime.bigint();

logger.plain(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { inspect } from 'util';

import fg from 'fast-glob';
import { readFile, writeFile } from 'fs-extra';

import type { PatchFunction, PatchReturnType } from '../..';
import { log } from '../../../../../../utils/logging';

const collapseDuplicateMergeKeys: PatchFunction = async ({
mode,
}): Promise<PatchReturnType> => {
const buildkiteFiles = await fg(['.buildkite/**/*'], { onlyFiles: true });

if (buildkiteFiles.length === 0) {
return { result: 'skip', reason: 'no Buildkite files found' };
}

const input = await Promise.all(
buildkiteFiles.map((name) => readFile(name, 'utf-8')),
);

const replaced = await Promise.all(
input.map(collapseDuplicateMergeKeysInFile),
);

if (replaced.every((r, i) => r === input[i])) {
return { result: 'skip' };
}

if (mode === 'lint') {
return { result: 'apply' };
}

await Promise.all(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
buildkiteFiles.map((name, i) => writeFile(name, replaced[i]!)),
);

return { result: 'apply' };
};

const collapseDuplicateMergeKeysInFile = (input: string) =>
replaceAllUntilStable(
input,
/^([ \-]*)<<: \[?(\*[^\n\]]+)\]?$\n^( *)<<: \[?(\*[^\n\]]+)\]?$/gm,
(match, a, b, c, d) => {
if (a.length === c.length) {
return `${a}<<: [${b}, ${d}]`;
}
return match;
},
);

const replaceAllUntilStable = (
input: string,
searchValue: RegExp,
replacer: (substring: string, ...args: string[]) => string,
): string => {
let output = input;
let previousOutput;

do {
previousOutput = output;
output = output.replace(searchValue, replacer);
} while (output !== previousOutput);

return output;
};

export const tryCollapseDuplicateMergeKeys: PatchFunction = async (config) => {
try {
return await collapseDuplicateMergeKeys(config);
} catch (err) {
log.warn('Failed to collapse duplicate merge keys.');
log.subtle(inspect(err));
return { result: 'skip', reason: 'due to an error' };
}
};

// TODO: write some tests
5 changes: 5 additions & 0 deletions src/cli/lint/internalLints/upgrade/patches/8.2.1/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { Patches } from '../..';

import { tryCollapseDuplicateMergeKeys } from './collapseDuplicateMergeKeys';
import { tryUpgradeESLint } from './upgradeESLint';

export const patches: Patches = [
{
apply: tryCollapseDuplicateMergeKeys,
description: 'Collapse duplicate merge keys in .buildkite files',
},
{
apply: tryUpgradeESLint,
description: 'Upgrade to ESLint flat config',
Expand Down
60 changes: 39 additions & 21 deletions src/cli/lint/internalLints/upgrade/patches/8.2.1/upgradeESLint.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,37 @@
import { inspect } from 'util';

import { readFile, rm, writeFile } from 'fs-extra';
import { rm, writeFile } from 'fs-extra';

import type { PatchFunction, PatchReturnType } from '../..';
import { createExec } from '../../../../../../utils/exec';
import { log } from '../../../../../../utils/logging';
import { createDestinationFileReader } from '../../../../../configure/analysis/project';
import { mergeWithConfigFile } from '../../../../../configure/processing/configFile';

const upgradeESLint: PatchFunction = async ({
mode,
dir = process.cwd(),
}): Promise<PatchReturnType> => {
let originalIgnoreContents;
const readFile = createDestinationFileReader(dir);
const originalIgnoreContents = await readFile('.eslintignore');

try {
originalIgnoreContents = await readFile('.eslintignore', 'utf8');
} catch (err) {
if (
typeof err === 'object' &&
err !== null &&
'code' in err &&
err.code === 'ENOENT'
) {
return { result: 'skip', reason: 'already migrated' };
}

throw err;
if (originalIgnoreContents === null) {
return { result: 'skip', reason: 'already migrated' };
}

if (mode === 'lint') {
return { result: 'apply' };
}

// Remove managed section of .eslintignore
await writeFile(
'.eslintignore',
mergeWithConfigFile('', 'ignore')(originalIgnoreContents),
);
const merged = mergeWithConfigFile('', 'ignore')(originalIgnoreContents);
let deletedIgnoreFile = false;
if (merged.trim().length === 0) {
await rm('.eslintignore');
deletedIgnoreFile = true;
} else {
await writeFile('.eslintignore', merged);
}

const exec = createExec({
cwd: process.cwd(),
Expand All @@ -44,18 +40,40 @@ const upgradeESLint: PatchFunction = async ({

await exec('eslint-migrate-config', '.eslintrc.js', '--commonjs');

const output = await readFile('eslint.config.cjs', 'utf8');
const output = fiddleWithOutput((await readFile('eslint.config.cjs')) ?? '');
await writeFile('eslint.config.js', output);

await Promise.all([
rm('.eslintignore'),
deletedIgnoreFile ? Promise.resolve() : rm('.eslintignore'),
rm('eslint.config.cjs'),
rm('.eslintrc.js'),
]);

return { result: 'apply' };
};

const fiddleWithOutput = (input: string) => {
let output = input.replace(/compat.extends\(["']skuba["']\)/, 'skuba');

if (!output.includes('eslint-config-skuba')) {
output = `const skuba = require('eslint-config-skuba');\n\n${output}`;
}

if (!output.includes('compat.')) {
output = output.replace(/const compat = new FlatCompat\(\{[^}]+\}\);/m, '');
output = output.replace(
/const \{\s*FlatCompat,?\s*\}\s*=\s*require\(["']@eslint\/eslintrc["']\);/m,
'',
);
}

if (!output.includes('js.')) {
output = output.replace(/const js = require\(['"]@eslint\/js['"]\);/, '');
}

return output;
};

export const tryUpgradeESLint: PatchFunction = async (config) => {
try {
return await upgradeESLint(config);
Expand Down

0 comments on commit 725fb5b

Please sign in to comment.