diff --git a/CHANGELOG.md b/CHANGELOG.md index 623693e..fd24f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] - Added: Generator command for a ViewModel class +- Added: Generator command for data patches - Added: Jump-to-definition for magento modules (in module.xml and routes.xml) - Fixed: Method plugin hover messages are now grouped and include a link to di.xml diff --git a/package.json b/package.json index f3e63fc..5bf15f0 100644 --- a/package.json +++ b/package.json @@ -195,6 +195,11 @@ "title": "Generate Sample config.xml", "category": "Magento Toolbox" }, + { + "command": "magento-toolbox.generateDataPatch", + "title": "Generate Data Patch", + "category": "Magento Toolbox" + }, { "command": "magento-toolbox.jumpToModule", "title": "Jump to Module", @@ -328,6 +333,10 @@ { "command": "magento-toolbox.generateConfigXmlFile", "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateDataPatch", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" } ] } diff --git a/src/command/GenerateDataPatchCommand.ts b/src/command/GenerateDataPatchCommand.ts new file mode 100644 index 0000000..cc620d4 --- /dev/null +++ b/src/command/GenerateDataPatchCommand.ts @@ -0,0 +1,58 @@ +import { Command } from 'command/Command'; +import DataPatchWizard, { DataPatchWizardData } from 'wizard/DataPatchWizard'; +import WizzardClosedError from 'webview/error/WizzardClosedError'; +import FileGeneratorManager from 'generator/FileGeneratorManager'; +import Common from 'util/Common'; +import { Uri, window } from 'vscode'; +import DataPatchGenerator from 'generator/dataPatch/DataPatchGenerator'; +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; + +export default class GenerateDataPatchCommand extends Command { + constructor() { + super('magento-toolbox.generateDataPatch'); + } + + public async execute(uri?: Uri): Promise { + const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); + let contextModule: string | undefined; + + const contextUri = uri || window.activeTextEditor?.document.uri; + + if (moduleIndex && contextUri) { + const module = moduleIndex.getModuleByUri(contextUri); + + if (module) { + contextModule = module.name; + } + } + + const dataPatchWizard = new DataPatchWizard(); + + let data: DataPatchWizardData; + + try { + data = await dataPatchWizard.show(contextModule); + } catch (error) { + if (error instanceof WizzardClosedError) { + return; + } + + throw error; + } + + const manager = new FileGeneratorManager([new DataPatchGenerator(data)]); + + const workspaceFolder = Common.getActiveWorkspaceFolder(); + + if (!workspaceFolder) { + window.showErrorMessage('No active workspace folder'); + return; + } + + await manager.generate(workspaceFolder.uri); + await manager.writeFiles(); + await manager.refreshIndex(workspaceFolder); + manager.openAllFiles(); + } +} diff --git a/src/command/index.ts b/src/command/index.ts index 97c6332..bdae859 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -27,3 +27,4 @@ export { default as GenerateExtensionAttributesXmlFileCommand } from './Generate export { default as GenerateSystemXmlFileCommand } from './GenerateSystemXmlFileCommand'; export { default as GenerateConfigXmlFileCommand } from './GenerateConfigXmlFileCommand'; export { default as JumpToModuleCommand } from './JumpToModuleCommand'; +export { default as GenerateDataPatchCommand } from './GenerateDataPatchCommand'; diff --git a/src/generator/dataPatch/DataPatchGenerator.ts b/src/generator/dataPatch/DataPatchGenerator.ts new file mode 100644 index 0000000..5523829 --- /dev/null +++ b/src/generator/dataPatch/DataPatchGenerator.ts @@ -0,0 +1,91 @@ +import FileHeader from 'common/php/FileHeader'; +import PhpNamespace from 'common/PhpNamespace'; +import GeneratedFile from 'generator/GeneratedFile'; +import FileGenerator from 'generator/FileGenerator'; +import { PhpFile, PsrPrinter } from 'node-php-generator'; +import { Uri } from 'vscode'; +import { DataPatchWizardData } from 'wizard/DataPatchWizard'; +import Magento from 'util/Magento'; +import * as fs from 'fs'; +import * as path from 'path'; + +export default class DataPatchGenerator extends FileGenerator { + private static readonly DATA_PATCH_INTERFACE = + 'Magento\\Framework\\Setup\\Patch\\DataPatchInterface'; + private static readonly PATCH_REVERTABLE_INTERFACE = + 'Magento\\Framework\\Setup\\Patch\\PatchRevertableInterface'; + + public constructor(protected data: DataPatchWizardData) { + super(); + } + + public async generate(workspaceUri: Uri): Promise { + const [vendor, module] = this.data.module.split('_'); + const setupDir = 'Setup/Patch/Data'; + const namespaceParts = [vendor, module, 'Setup', 'Patch', 'Data']; + const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); + + // Create setup directory if it doesn't exist + const setupDirPath = path.join(moduleDirectory.fsPath, setupDir); + if (!fs.existsSync(setupDirPath)) { + fs.mkdirSync(setupDirPath, { recursive: true }); + } + + const phpFile = new PhpFile(); + phpFile.setStrictTypes(true); + + const header = FileHeader.getHeader(this.data.module); + + if (header) { + phpFile.addComment(header); + } + + const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString()); + namespace.addUse(DataPatchGenerator.DATA_PATCH_INTERFACE); + + if (this.data.revertable) { + namespace.addUse(DataPatchGenerator.PATCH_REVERTABLE_INTERFACE); + } + + const patchClass = namespace.addClass(this.data.className); + patchClass.addImplement(DataPatchGenerator.DATA_PATCH_INTERFACE); + + if (this.data.revertable) { + patchClass.addImplement(DataPatchGenerator.PATCH_REVERTABLE_INTERFACE); + } + + // Add apply method + const applyMethod = patchClass.addMethod('apply'); + applyMethod.addComment('@inheritdoc'); + applyMethod.setReturnType('self'); + applyMethod.setBody('// TODO: Implement apply() method.\n\nreturn $this;'); + + // Add revert method if revertable + if (this.data.revertable) { + const revertMethod = patchClass.addMethod('revert'); + revertMethod.addComment('@inheritdoc'); + revertMethod.setReturnType('void'); + revertMethod.setBody('// TODO: Implement revert() method.'); + } + + // Add getAliases method + const getAliasesMethod = patchClass.addMethod('getAliases'); + getAliasesMethod.addComment('@inheritdoc'); + getAliasesMethod.setReturnType('array'); + getAliasesMethod.setBody('return [];'); + + // Add getDependencies method + const getDependenciesMethod = patchClass.addMethod('getDependencies'); + getDependenciesMethod.setStatic(true); + getDependenciesMethod.addComment('@inheritdoc'); + getDependenciesMethod.setReturnType('array'); + getDependenciesMethod.setBody('return [];'); + + const printer = new PsrPrinter(); + + return new GeneratedFile( + Uri.joinPath(moduleDirectory, setupDir, `${this.data.className}.php`), + printer.printFile(phpFile) + ); + } +} diff --git a/src/test/generator/dataPatch/DataPatchGenerator.test.ts b/src/test/generator/dataPatch/DataPatchGenerator.test.ts new file mode 100644 index 0000000..8c53199 --- /dev/null +++ b/src/test/generator/dataPatch/DataPatchGenerator.test.ts @@ -0,0 +1,89 @@ +import { DataPatchWizardData } from 'wizard/DataPatchWizard'; +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import DataPatchGenerator from 'generator/dataPatch/DataPatchGenerator'; +import { describe, it, before, afterEach } from 'mocha'; +import { setup } from 'test/setup'; +import { getReferenceFile, getTestWorkspaceUri } from 'test/util'; +import FileHeader from 'common/php/FileHeader'; +import sinon from 'sinon'; + +describe('DataPatchGenerator Tests', () => { + const basePatchWizardData: DataPatchWizardData = { + module: 'Foo_Bar', + className: 'TestDataPatch', + revertable: false, + }; + + const revertablePatchWizardData: DataPatchWizardData = { + module: 'Foo_Bar', + className: 'TestRevertableDataPatch', + revertable: true, + }; + + before(async () => { + await setup(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should generate a standard data patch file', async () => { + // Mock the FileHeader.getHeader method to return a consistent header + sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar'); + // Create the generator with test data + const generator = new DataPatchGenerator(basePatchWizardData); + + // Use a test workspace URI + const workspaceUri = getTestWorkspaceUri(); + + // Generate the file + const generatedFile = await generator.generate(workspaceUri); + + // Get the reference file content + const referenceContent = getReferenceFile('generator/dataPatch/patch.php'); + + // Compare the generated content with reference + assert.strictEqual(generatedFile.content, referenceContent); + }); + + it('should generate a revertable data patch file', async () => { + // Mock the FileHeader.getHeader method to return a consistent header + sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar'); + + // Create the generator with test data + const generator = new DataPatchGenerator(revertablePatchWizardData); + + // Use a test workspace URI + const workspaceUri = getTestWorkspaceUri(); + + // Generate the file + const generatedFile = await generator.generate(workspaceUri); + + // Get the reference file content + const referenceContent = getReferenceFile('generator/dataPatch/patchRevertable.php'); + + // Compare the generated content with reference + assert.strictEqual(generatedFile.content, referenceContent); + }); + + it('should generate file in correct location', async () => { + // Create the generator with test data + const generator = new DataPatchGenerator(basePatchWizardData); + + // Use a test workspace URI + const workspaceUri = getTestWorkspaceUri(); + + // Generate the file + const generatedFile = await generator.generate(workspaceUri); + + // Expected path + const expectedPath = Uri.joinPath( + workspaceUri, + 'app/code/Foo/Bar/Setup/Patch/Data/TestDataPatch.php' + ).fsPath; + + assert.strictEqual(generatedFile.uri.fsPath, expectedPath); + }); +}); diff --git a/src/wizard/DataPatchWizard.ts b/src/wizard/DataPatchWizard.ts new file mode 100644 index 0000000..4a810a2 --- /dev/null +++ b/src/wizard/DataPatchWizard.ts @@ -0,0 +1,65 @@ +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import { GeneratorWizard } from 'webview/GeneratorWizard'; +import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; +import { WizardFormBuilder } from 'webview/WizardFormBuilder'; +import { WizardTabBuilder } from 'webview/WizardTabBuilder'; +import Validation from 'common/Validation'; + +export interface DataPatchWizardData { + module: string; + className: string; + revertable: boolean; +} + +export default class DataPatchWizard extends GeneratorWizard { + public async show(contextModule?: string): Promise { + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + throw new Error('Module index data not found'); + } + + const modules = moduleIndexData.getModuleOptions(module => module.location === 'app'); + + const builder = new WizardFormBuilder(); + + builder.setTitle('Generate a new Data Patch'); + builder.setDescription('Generates a new Data Patch for a module.'); + + const tab = new WizardTabBuilder(); + tab.setId('dataPatch'); + tab.setTitle('Data Patch'); + + tab.addField( + WizardFieldBuilder.select('module', 'Module') + .setDescription(['Module where data patch will be generated in']) + .setOptions(modules) + .setInitialValue(contextModule || modules[0].value) + .build() + ); + + tab.addField( + WizardFieldBuilder.text('className', 'Class Name') + .setDescription(['The class name for the data patch']) + .setPlaceholder('YourPatchName') + .build() + ); + + tab.addField( + WizardFieldBuilder.checkbox('revertable', 'Revertable').setInitialValue(false).build() + ); + + builder.addTab(tab.build()); + + builder.addValidation('module', 'required'); + builder.addValidation('className', [ + 'required', + `regex:/${Validation.CLASS_NAME_REGEX.source}/`, + ]); + + const data = await this.openWizard(builder.build()); + + return data; + } +} diff --git a/test-resources/reference/generator/dataPatch/patch.php b/test-resources/reference/generator/dataPatch/patch.php new file mode 100644 index 0000000..9d55831 --- /dev/null +++ b/test-resources/reference/generator/dataPatch/patch.php @@ -0,0 +1,40 @@ +