Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@babel/traverse": "7.23.2",
"@babel/types": "7.23.0",
"@types/glob": "8.1.0",
"glob": "10.4.3",
"glob": "11.0.0",
"prettier": "3.3.2",
"typedoc": "^0.26.3",
"typescript": "5.5.3",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@types/node": "20.14.10",
"@types/semver": "7.5.8",
"diff": "5.2.0",
"globby": "14.0.2",
"graceful-fs": "4.2.11",
"semver": "7.6.2",
"vite-plugin-dts": "3.9.1"
Expand Down
67 changes: 36 additions & 31 deletions packages/cli/src/commands/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
CodemodTaskInput,
CodemodTaskWorkerResult,
TaskRunnerEnvironment,
UserConfig,
type VersionManifest,
} from '@ag-grid-devtools/types';
import { createFsHelpers } from '@ag-grid-devtools/worker-utils';
Expand All @@ -22,9 +21,9 @@ import semver from 'semver';
import { type CliEnv, type CliOptions } from '../types/cli';
import { type WritableStream } from '../types/io';
import { CliArgsError, CliError } from '../utils/cli';
import { findInDirectory } from '../utils/fs';
import { findInGitRepository, getGitProjectRoot, getUncommittedGitFiles } from '../utils/git';
import { basename, extname, resolve, relative } from '../utils/path';
import { findSourceFiles, loadGitRootAndGitIgnoreFiles } from '../utils/fs';
import { findInGitRepository, getUncommittedGitFiles } from '../utils/git';
import { normalize, sep as pathSeparator, resolve, relative } from 'node:path';
import { getCliCommand, getCliPackageVersion } from '../utils/pkg';
import { green, indentErrorMessage, log } from '../utils/stdio';
import { Worker, WorkerTaskQueue, type WorkerOptions } from '../utils/worker';
Expand Down Expand Up @@ -105,7 +104,8 @@ Options:
See https://ag-grid.com/javascript-data-grid/codemods/#configuration-file

Additional arguments:
[<file>...] List of input files to operate on (defaults to all source files in the current working directory)
[<file>...] List of input files to operate on.
Defaults to all source files in the current working directory excluding patterns in .gitignore

Other options:
--verbose, -v Show additional log output
Expand Down Expand Up @@ -266,32 +266,50 @@ async function migrate(
userConfigPath,
input,
} = args;
const { cwd, env, stdio } = options;
let { cwd, env, stdio } = options;
const { stdout, stderr } = stdio;

const gitProjectRoot = await getGitProjectRoot(cwd);
cwd = resolve(cwd);

if (!allowUntracked && !gitProjectRoot) {
const { hasGitRoot, gitRoot, gitIgnoreFiles } = await loadGitRootAndGitIgnoreFiles(cwd);

if (!allowUntracked && !hasGitRoot) {
throw new CliError(
'No git repository found',
'To run this command outside a git repository, use the --allow-untracked option',
);
}

const gitSourceFilePaths = gitProjectRoot
? (await getGitSourceFiles(gitProjectRoot)).map((path) => resolve(gitProjectRoot, path))
const gitSourceFilePaths = gitRoot
? (await getGitSourceFiles(gitRoot)).map((path) => resolve(gitRoot, path))
: null;

const inputFilePaths =
input.length > 0
? input.map((path) => resolve(cwd, path))
: (await getProjectSourceFiles(cwd)).map((path) => resolve(cwd, path));
const hasCustomFileList = input.length > 0;

const inputFilePaths = hasCustomFileList
? input.map((path) => resolve(cwd, path))
: await findSourceFiles(cwd, SOURCE_FILE_EXTENSIONS, gitIgnoreFiles);

if (!allowUntracked) {
const trackedFilePaths = gitSourceFilePaths ? new Set(gitSourceFilePaths) : null;
const untrackedInputFiles = trackedFilePaths
let untrackedInputFiles = trackedFilePaths
? inputFilePaths.filter((path) => !trackedFilePaths.has(path))
: inputFilePaths;

if (!hasCustomFileList) {
untrackedInputFiles = untrackedInputFiles.filter((path) => {
// check if path is in the gitRoot
const relativePath = normalize(relative(gitRoot ?? '', path));
if (relativePath.startsWith('..')) {
return false; // path is not in our gitRoot
}
if (!relativePath.includes(pathSeparator)) {
return false; // file is in gitRoot
}
return true;
});
}

if (untrackedInputFiles.length > 0)
throw new CliError(
'Untracked input files',
Expand All @@ -303,10 +321,10 @@ async function migrate(
);
}

if (gitProjectRoot && !allowDirty) {
if (hasGitRoot && !allowDirty) {
const inputFileSet = new Set(inputFilePaths);
const uncommittedInputFiles = (await getUncommittedGitFiles(gitProjectRoot))
.map((path) => resolve(gitProjectRoot, path))
const uncommittedInputFiles = (await getUncommittedGitFiles(gitRoot))
.map((path) => resolve(gitRoot, path))
.filter((path) => inputFileSet.has(path));
if (uncommittedInputFiles.length > 0) {
throw new CliError(
Expand Down Expand Up @@ -628,15 +646,6 @@ function formatFileErrors(warningResults: Array<{ path: string; errors: Error[]
.join('\n');
}

function getProjectSourceFiles(projectRoot: string): Promise<Array<string>> {
return findInDirectory(
projectRoot,
(filePath, stats) =>
(stats.isDirectory() && basename(filePath) !== 'node_modules') ||
(stats.isFile() && isSourceFile(filePath)),
);
}

function getGitSourceFiles(projectRoot: string): Promise<Array<string>> {
return findInGitRepository(
SOURCE_FILE_EXTENSIONS.map((extension) => `*${extension}`),
Expand All @@ -646,10 +655,6 @@ function getGitSourceFiles(projectRoot: string): Promise<Array<string>> {
);
}

function isSourceFile(filePath: string): boolean {
return SOURCE_FILE_EXTENSIONS.includes(extname(filePath));
}

function getMinorSemverVersion(version: string): string | null {
const parsed = semver.parse(version);
if (!parsed) return null;
Expand Down
136 changes: 79 additions & 57 deletions packages/cli/src/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,98 @@ import { cli } from '../cli';
import {
TEMP_FOLDER,
loadExpectedSource,
loadInputSource,
loadTempSource,
patchDynamicRequire,
prepareTestDataFiles,
} from './test-utils';
import { CliOptions } from '../types/cli';

describe('cli e2e', () => {
beforeAll(() => {
patchDynamicRequire();
});
describe(
'cli e2e',
() => {
beforeAll(() => {
patchDynamicRequire();
});

const cliOptions: CliOptions = {
cwd: TEMP_FOLDER,
env: {
const cliOptions: CliOptions = {
cwd: TEMP_FOLDER,
},
stdio: {
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
},
};
env: {
cwd: TEMP_FOLDER,
},
stdio: {
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
},
};

test('plain cli single threaded', async () => {
await prepareTestDataFiles();
await cli(['migrate', '--num-threads=0', '--allow-untracked', '--from=30.0.0'], cliOptions);
expect(await loadExpectedSource('plain.js')).toEqual(await loadTempSource('plain.js'));
}, 10000);
test('plain cli single threaded', async () => {
await prepareTestDataFiles();
await cli(['migrate', '--num-threads=0', '--allow-untracked', '--from=30.0.0'], cliOptions);
expect(await loadExpectedSource('plain.js')).toEqual(await loadTempSource('plain.js'));

test('plain cli multi-threaded', async () => {
await prepareTestDataFiles();
await cli(['migrate', '--num-threads=0', '--allow-untracked', '--from=30.0.0'], cliOptions);
expect(await loadExpectedSource('plain.js')).toEqual(await loadTempSource('plain.js'));
}, 10000);
// Test .gitignore support
expect(await loadInputSource('gitignored.js')).toEqual(await loadTempSource('gitignored.js'));
});

test('userConfig single-threaded', async () => {
await prepareTestDataFiles();
test('plain cli multi-threaded', async () => {
await prepareTestDataFiles();
await cli(['migrate', '--num-threads=4', '--allow-untracked', '--from=30.0.0'], cliOptions);
expect(await loadExpectedSource('plain.js')).toEqual(await loadTempSource('plain.js'));

await cli(
[
'migrate',
'--num-threads=0',
'--allow-untracked',
'--from=30.0.0',
'--config=../user-config.cts',
],
cliOptions,
);
// Test .gitignore support
expect(await loadInputSource('gitignored.js')).toEqual(await loadTempSource('gitignored.js'));
});

expect(await loadExpectedSource('custom-imports.js')).toEqual(
await loadTempSource('custom-imports.js'),
);
}, 10000);
test('userConfig single-threaded', async () => {
await prepareTestDataFiles();

test('userConfig multi-threaded', async () => {
await prepareTestDataFiles();
await cli(
[
'migrate',
'--num-threads=0',
'--allow-untracked',
'--from=30.0.0',
'--config=../user-config.cts',
],
cliOptions,
);

await cli(
[
'migrate',
'--num-threads=2',
'--allow-untracked',
'--from=30.0.0',
'--config=../user-config.cts',
],
cliOptions,
);
expect(await loadExpectedSource('custom-imports.js')).toEqual(
await loadTempSource('custom-imports.js'),
);
});

expect(await loadExpectedSource('custom-imports.js')).toEqual(
await loadTempSource('custom-imports.js'),
);
}, 10000);
});
test('userConfig multi-threaded', async () => {
await prepareTestDataFiles();

await cli(
[
'migrate',
'--num-threads=2',
'--allow-untracked',
'--from=30.0.0',
'--config=../user-config.cts',
],
cliOptions,
);

expect(await loadExpectedSource('custom-imports.js')).toEqual(
await loadTempSource('custom-imports.js'),
);
});

test('.gitignore support without --allow-untracked', async () => {
await prepareTestDataFiles();
await cli(['migrate', '--from=30.0.0'], cliOptions);

expect(await loadInputSource('gitignored.js')).toEqual(await loadTempSource('gitignored.js'));

expect(await loadInputSource('gitignored-folder/file.js')).toEqual(
await loadTempSource('gitignored-folder/file.js'),
);
});
},
{ timeout: 20000 },
);
3 changes: 3 additions & 0 deletions packages/cli/src/test/input-files/_.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# this file will be copied as .gitignore in _temp directory

gitignored*
10 changes: 10 additions & 0 deletions packages/cli/src/test/input-files/gitignored-folder/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file will be ignored as is present in .gitignore

import { Grid as AgGrid } from '@ag-grid-community/core';

(() => {
const gridOptions = { foo: 'bar' };
gridOptions.baz = 3;
new AgGrid(document.getQuerySelector('main'), gridOptions);
gridOptions.api.sizeColumnsToFit();
})();
10 changes: 10 additions & 0 deletions packages/cli/src/test/input-files/gitignored.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file will be ignored as is present in .gitignore

import { Grid as AgGrid } from '@ag-grid-community/core';

(() => {
const gridOptions = { foo: 'bar' };
gridOptions.baz = 3;
new AgGrid(document.getQuerySelector('main'), gridOptions);
gridOptions.api.sizeColumnsToFit();
})();
31 changes: 24 additions & 7 deletions packages/cli/src/test/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { dynamicRequire } from '@ag-grid-devtools/utils';
import { cli } from '../cli';
import prettier from 'prettier';

export const ROOT_FOLDER = path.dirname(fileURLToPath(import.meta.url));
export const ROOT_FOLDER = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
export const TEMP_FOLDER = path.resolve(ROOT_FOLDER, '_temp');
export const INPUT_FOLDER = path.resolve(ROOT_FOLDER, 'input-files');
export const EXPECTED_FOLDER = path.resolve(ROOT_FOLDER, 'expected');

export async function loadInputSource(name: string) {
const filepath = path.resolve(INPUT_FOLDER, name);
return prettier.format(await readFile(filepath, 'utf-8'), { filepath });
}

export async function loadExpectedSource(name: string) {
const filepath = path.resolve(EXPECTED_FOLDER, name);
return prettier.format(await readFile(filepath, 'utf-8'), { filepath });
Expand All @@ -27,13 +32,25 @@ export async function prepareTestDataFiles() {
// already deleted
}

await mkdir(TEMP_FOLDER, { recursive: true });
// create a .git directory to simulate a git repository root
await mkdir(path.join(TEMP_FOLDER, '.git'), { recursive: true });

const inputPath = path.resolve(ROOT_FOLDER, INPUT_FOLDER);

await Promise.all([
// copy all files from the input folder to the temp folder
cp(inputPath, TEMP_FOLDER, {
recursive: true,
force: true,
filter: (src) => {
const filename = path.basename(src);
return filename !== '_.gitignore' && filename !== 'README.md';
},
}),

await cp(path.resolve(ROOT_FOLDER, INPUT_FOLDER), TEMP_FOLDER, {
recursive: true,
force: true,
filter: (src) => !src.includes('README.md'),
});
// copy the _.gitignore file renamed as .gitignore
cp(path.resolve(inputPath, '_.gitignore'), path.resolve(TEMP_FOLDER, '.gitignore')),
]);
}

export function patchDynamicRequire() {
Expand Down
Loading