Skip to content

Feat/parse only single file #187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
68 changes: 67 additions & 1 deletion src/__tests__/migrator.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Project, SourceFile } from 'ts-morph';
import { project, createSourceFile } from './utils';
import { migrateFile } from '../migrator';
import { migrateFile, migrateSingleFile } from '../migrator';
import * as migrator from '../migrator/migrator';

describe('migrateFile()', () => {
afterAll(() => {
Expand Down Expand Up @@ -89,5 +91,69 @@ describe('migrateFile()', () => {
'export default defineComponent({})',
].join('\n'));
});

test('Vue import respected in .vue file', async () => {
const sourceFile = createSourceFile([
'<script lang="ts">',
'import Vue, { mounted } from "vue";',
'@Component',
'export default class {}',
'</script>',
].join('\n'), 'vue');

const migratedFile = await migrateFile(project, sourceFile);
expect(migratedFile.getText())
.toBe([
'<script lang="ts">',
'import Vue, { mounted, defineComponent } from "vue";',
'export default defineComponent({})',
'',
'</script>',
].join('\n'));
});
});
});

describe('migrateSingleFile()', () => {
afterEach(() => {
jest.resetAllMocks();
});

describe('when a file path is neither .vue nor .ts file', () => {
let migrateFileSpy: jest.SpyInstance;
beforeEach(() => {
migrateFileSpy = jest.spyOn(migrator, 'migrateFile');
});

test('should not call `migrateFile`', async () => {
await migrateSingleFile('test.txt', false);

expect(migrateFileSpy).not.toHaveBeenCalled();
});
});

describe('when a file path is a .vue file', () => {
let migrateFileSpy: jest.SpyInstance;
const scriptSource = `'<script lang="ts">',
'import Vue, { mounted } from "vue";',
'@Component',
'export default class {}',
'</script>',
`;
let sourceFile: SourceFile;

beforeEach(() => {
migrateFileSpy = jest.spyOn(migrator, 'migrateFile');
process.cwd = jest.fn(() => '/');
sourceFile = createSourceFile(scriptSource, 'vue');
Project.prototype.addSourceFileAtPath = jest.fn(() => sourceFile);
Project.prototype.getSourceFiles = jest.fn(() => [sourceFile]);
});

test('should call `migrateFile`', async () => {
await migrateSingleFile(sourceFile.getFilePath(), false);

expect(migrateFileSpy).toHaveBeenCalledWith(expect.anything(), sourceFile);
});
});
});
29 changes: 29 additions & 0 deletions src/__tests__/option.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { OptionParser } from '../migrator/option';

describe('OptionParser', () => {
describe('when options are not provided', () => {
it('should throw an error', () => {
expect(() => new OptionParser({}).parse()).toThrowError('Either directory or file should be provided. Not both or none');
});
});

describe('when both directory and file are provided', () => {
it('should throw an error', () => {
expect(() => new OptionParser({ directory: 'dir', file: 'file' }).parse()).toThrowError('Either directory or file should be provided. Not both or none');
});
});

describe('when directory is provided', () => {
it('should return directory mode option', () => {
const options = new OptionParser({ directory: '/home/auto-test', sfc: true }).parse();
expect(options).toStrictEqual({ directory: '/home/auto-test', sfc: true });
});
});

describe('when file is provided', () => {
it('should return file mode option', () => {
const options = new OptionParser({ file: '/home/auto-test/InputNumber.vue', sfc: false }).parse();
expect(options).toStrictEqual({ file: '/home/auto-test/InputNumber.vue', sfc: false });
});
});
});
7 changes: 4 additions & 3 deletions src/migrator-cli.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Command } from 'commander';
import { migrateDirectory } from './migrator';
import { migrate } from './migrator';

const program = new Command()
.requiredOption('-d, --directory <string>', 'Directory to migrate')
.option('-d, --directory <string>', 'Directory to migrate. Either directory or file should be provided, not both or none.')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptionParser guarantees option requirements

.option('-f, --file <string>', 'Directory to migrate, Either directory or file should be provided, not both or none.')
.option(
'-s, --sfc',
'If you would like to generate a SFC and remove the original scss and ts files',
false,
)
.action((options) => migrateDirectory(options.directory, options.sfc))
.action((options) => migrate(options))
.parse(process.argv);

export default program;
2 changes: 2 additions & 0 deletions src/migrator/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export {
migrateDirectory,
migrateFile,
migrate,
migrateSingleFile,
} from './migrator';
74 changes: 66 additions & 8 deletions src/migrator/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import migrateVueClassProperties from './vue-property-decorator';
import migrateVuexDecorators from './vuex';
import { getScriptContent, injectScript, vueFileToSFC } from './migrator-to-sfc';
import { createMigrationManager } from './migratorManager';
import {
canBeCliOptions, DirectoryModeOption, FileModeOption, OptionParser,
} from './option';

const migrateTsFile = async (project: Project, sourceFile: SourceFile): Promise<SourceFile> => {
const filePath = sourceFile.getFilePath();
Expand Down Expand Up @@ -52,7 +55,10 @@ const migrateVueFile = async (project: Project, vueSourceFile: SourceFile) => {
}
};

export const migrateFile = async (project: Project, sourceFile: SourceFile) => {
export const migrateFile = async (
project: Project,
sourceFile: SourceFile,
): Promise<SourceFile> => {
logger.info(`Migrating ${sourceFile.getBaseName()}`);
if (!sourceFile.getText().includes('@Component')) {
throw new Error('File already migrated');
Expand All @@ -71,6 +77,20 @@ export const migrateFile = async (project: Project, sourceFile: SourceFile) => {
throw new Error(`Extension ${ext} not supported`);
};

const migrateEachFile = (
filesToMigrate: SourceFile[],
project: Project,
): Promise<SourceFile>[] => {
const resolveFileMigration = (s: SourceFile, p: Project) => migrateFile(p, s)
.catch((err) => {
logger.error(`Error migrating ${s.getFilePath()}`);
logger.error(err);
return Promise.reject(err);
});

return filesToMigrate.map((sourceFile) => resolveFileMigration(sourceFile, project));
};

export const migrateDirectory = async (directoryPath: string, toSFC: boolean) => {
const directoryToMigrate = path.join(process.cwd(), directoryPath);
const project = new Project({});
Expand All @@ -92,13 +112,7 @@ export const migrateDirectory = async (directoryPath: string, toSFC: boolean) =>
`Migrating directory: ${directoryToMigrate}, ${finalFilesToMigrate.length} Files needs migration`,
);

const migrationPromises = finalFilesToMigrate
.map((sourceFile) => migrateFile(project, sourceFile)
.catch((err) => {
logger.error(`Error migrating ${sourceFile.getFilePath()}`);
logger.error(err);
return Promise.reject(err);
}));
const migrationPromises = migrateEachFile(finalFilesToMigrate, project);

try {
await Promise.all(migrationPromises);
Expand All @@ -117,3 +131,47 @@ export const migrateDirectory = async (directoryPath: string, toSFC: boolean) =>
await Promise.all(vueFiles.map((f) => vueFileToSFC(project, f)));
}
};

export const migrateSingleFile = async (filePath: string, toSFC: boolean): Promise<void> => {
const fileExtensionPattern = /.+\.(vue|ts)$/;
if (!fileExtensionPattern.test(filePath)) {
logger.info(`${filePath} can not migrate. Only .vue files are supported.`);
return;
}

const fileToMigrate = path.join(process.cwd(), filePath);
const project = new Project({});
project.addSourceFileAtPath(fileToMigrate);
const sourceFiles = project.getSourceFiles();

logger.info(`Migrating file: ${fileToMigrate}`);

const migrationPromises = migrateEachFile(sourceFiles, project);
try {
await Promise.all(migrationPromises);
} catch (error) {
return;
}

if (toSFC) {
logger.info(`Migrating file: ${fileToMigrate}, files to SFC`);
await Promise.all(sourceFiles.map((f) => vueFileToSFC(project, f)));
}
};

/**
* Entry function to start migration
*/
export const migrate = async (option: any): Promise<void> => {
if (!canBeCliOptions(option)) {
throw new Error('Cli option should be provided. Run --help for more info');
}
const result = new OptionParser(option).parse();
if (Object.keys(result).includes('file')) {
const fileModeOption = result as FileModeOption;
migrateSingleFile(fileModeOption.file, (fileModeOption.sfc ?? false));
} else {
const directoryModeOption = result as DirectoryModeOption;
migrateDirectory(directoryModeOption.directory, (directoryModeOption.sfc ?? false));
}
};
43 changes: 43 additions & 0 deletions src/migrator/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export type CliOptions = {
directory?: string;
file?: string;
sfc?: boolean;
};

export type DirectoryModeOption = {
directory: string;
sfc?: boolean;
};

export type FileModeOption = {
file: string;
sfc?: boolean;
};

export type OptionMode = DirectoryModeOption | FileModeOption;

export function canBeCliOptions(obj: any): obj is CliOptions {
return obj && typeof obj === 'object' && (obj.directory || obj.file || obj.sfc);
}

export class OptionParser {
constructor(private options: CliOptions) {
if ((!options.directory && !options.file) || (options.directory && options.file)) {
throw new Error('Either directory or file should be provided. Not both or none');
}
}

parse(): OptionMode {
if (this.options.directory) {
return {
directory: this.options.directory,
sfc: this.options.sfc,
};
}

return {
file: this.options.file,
sfc: this.options.sfc,
} as FileModeOption;
}
}