-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): Implement "add" command for ui extensions
- Loading branch information
1 parent
6981c73
commit 795b013
Showing
26 changed files
with
5,280 additions
and
4,544 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { cancel, isCancel, select } from '@clack/prompts'; | ||
import { Command } from 'commander'; | ||
|
||
import { addUiExtensions } from './ui-extensions/add-ui-extensions'; | ||
|
||
const cancelledMessage = 'Add feature cancelled.'; | ||
|
||
export function registerAddCommand(program: Command) { | ||
program | ||
.command('add') | ||
.description('Add a feature to your Vendure project') | ||
.action(async () => { | ||
const featureType = await select({ | ||
message: 'Which feature would you like to add?', | ||
options: [ | ||
{ value: 'uiExtensions', label: 'Set up Admin UI extensions' }, | ||
{ value: 'other', label: 'Other' }, | ||
], | ||
}); | ||
if (isCancel(featureType)) { | ||
cancel(cancelledMessage); | ||
process.exit(0); | ||
} | ||
if (featureType === 'uiExtensions') { | ||
await addUiExtensions(); | ||
} | ||
process.exit(0); | ||
}); | ||
} |
109 changes: 109 additions & 0 deletions
109
packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { note, outro, spinner, log } from '@clack/prompts'; | ||
import path from 'path'; | ||
import { ClassDeclaration } from 'ts-morph'; | ||
import { Logger } from '../../../utilities/logger'; | ||
import { determineVendureVersion, installRequiredPackages } from '../../../utilities/package-utils'; | ||
|
||
import { Scaffolder } from '../../../utilities/scaffolder'; | ||
import { getTsMorphProject, getVendureConfig, selectPluginClass } from '../../../utilities/utils'; | ||
|
||
import { addUiExtensionStaticProp } from './codemods/add-ui-extension-static-prop/add-ui-extension-static-prop'; | ||
import { updateAdminUiPluginInit } from './codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init'; | ||
import { renderProviders } from './scaffold/providers'; | ||
import { renderRoutes } from './scaffold/routes'; | ||
|
||
export async function addUiExtensions() { | ||
const projectSpinner = spinner(); | ||
projectSpinner.start('Analyzing project...'); | ||
|
||
await new Promise(resolve => setTimeout(resolve, 100)); | ||
const project = getTsMorphProject(); | ||
projectSpinner.stop('Project analyzed'); | ||
|
||
const pluginClass = await selectPluginClass(project, 'Add UI extensions cancelled'); | ||
if (pluginAlreadyHasUiExtensionProp(pluginClass)) { | ||
outro('This plugin already has a UI extension configured'); | ||
return; | ||
} | ||
addUiExtensionStaticProp(pluginClass); | ||
|
||
log.success('Updated the plugin class'); | ||
const installSpinner = spinner(); | ||
installSpinner.start(`Installing dependencies...`); | ||
try { | ||
const version = determineVendureVersion(); | ||
await installRequiredPackages([ | ||
{ | ||
pkg: '@vendure/ui-devkit', | ||
isDevDependency: true, | ||
version, | ||
}, | ||
]); | ||
} catch (e: any) { | ||
log.error( | ||
`Failed to install dependencies: ${ | ||
e.message as string | ||
}. Run with --log-level=verbose to see more details.`, | ||
); | ||
Logger.verbose(e.stack); | ||
} | ||
installSpinner.stop('Dependencies installed'); | ||
|
||
const scaffolder = new Scaffolder(); | ||
scaffolder.addFile(renderProviders, 'providers.ts'); | ||
scaffolder.addFile(renderRoutes, 'routes.ts'); | ||
log.success('Created UI extension scaffold'); | ||
|
||
const pluginDir = pluginClass.getSourceFile().getDirectory().getPath(); | ||
scaffolder.createScaffold({ | ||
dir: path.join(pluginDir, 'ui'), | ||
context: {}, | ||
}); | ||
const vendureConfig = getVendureConfig(project); | ||
if (!vendureConfig) { | ||
log.warning( | ||
`Could not find the VendureConfig declaration in your project. You will need to manually set up the compileUiExtensions function.`, | ||
); | ||
} else { | ||
const pluginClassName = pluginClass.getName() as string; | ||
const pluginPath = convertPathToRelativeImport( | ||
path.relative( | ||
vendureConfig.getSourceFile().getDirectory().getPath(), | ||
pluginClass.getSourceFile().getFilePath(), | ||
), | ||
); | ||
const updated = updateAdminUiPluginInit(vendureConfig, { pluginClassName, pluginPath }); | ||
if (updated) { | ||
log.success('Updated VendureConfig file'); | ||
} else { | ||
log.warning(`Could not update \`AdminUiPlugin.init()\` options.`); | ||
note( | ||
`You will need to manually set up the compileUiExtensions function,\nadding ` + | ||
`the \`${pluginClassName}.ui\` object to the \`extensions\` array.`, | ||
'Info', | ||
); | ||
} | ||
} | ||
|
||
project.saveSync(); | ||
outro('✅ Done!'); | ||
} | ||
|
||
function pluginAlreadyHasUiExtensionProp(pluginClass: ClassDeclaration) { | ||
const uiProperty = pluginClass.getProperty('ui'); | ||
if (!uiProperty) { | ||
return false; | ||
} | ||
if (uiProperty.isStatic()) { | ||
return true; | ||
} | ||
} | ||
|
||
function convertPathToRelativeImport(filePath: string) { | ||
// Normalize the path separators | ||
const normalizedPath = filePath.replace(/\\/g, '/'); | ||
|
||
// Remove the file extension | ||
const parsedPath = path.parse(normalizedPath); | ||
return `./${parsedPath.dir}/${parsedPath.name}`; | ||
} |
25 changes: 25 additions & 0 deletions
25
.../ui-extensions/codemods/add-ui-extension-static-prop/add-ui-extension-static-prop.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import fs from 'fs-extra'; | ||
import path from 'path'; | ||
import { Project, QuoteKind } from 'ts-morph'; | ||
import { describe, expect, it } from 'vitest'; | ||
import { defaultManipulationSettings } from '../../../../../constants'; | ||
|
||
import { getPluginClasses } from '../../../../../utilities/utils'; | ||
|
||
import { addUiExtensionStaticProp } from './add-ui-extension-static-prop'; | ||
|
||
describe('addUiExtensionStaticProp', () => { | ||
it('add ui prop and imports', () => { | ||
const project = new Project({ | ||
manipulationSettings: defaultManipulationSettings, | ||
}); | ||
project.addSourceFileAtPath(path.join(__dirname, 'fixtures', 'no-ui-prop.fixture.ts')); | ||
const pluginClasses = getPluginClasses(project); | ||
expect(pluginClasses.length).toBe(1); | ||
addUiExtensionStaticProp(pluginClasses[0]); | ||
|
||
const result = pluginClasses[0].getSourceFile().getText(); | ||
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'no-ui-prop.expected'), 'utf-8'); | ||
expect(result).toBe(expected); | ||
}); | ||
}); |
39 changes: 39 additions & 0 deletions
39
...s/add/ui-extensions/codemods/add-ui-extension-static-prop/add-ui-extension-static-prop.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { ClassDeclaration } from 'ts-morph'; | ||
|
||
import { addImportsToFile, kebabize } from '../../../../../utilities/utils'; | ||
|
||
/** | ||
* @description | ||
* Adds the static `ui` property to the plugin class, and adds the required imports. | ||
*/ | ||
export function addUiExtensionStaticProp(pluginClass: ClassDeclaration) { | ||
const adminUiExtensionType = 'AdminUiExtension'; | ||
const extensionId = kebabize(pluginClass.getName() as string).replace(/-plugin$/, ''); | ||
pluginClass | ||
.addProperty({ | ||
name: 'ui', | ||
isStatic: true, | ||
type: adminUiExtensionType, | ||
initializer: `{ | ||
id: '${extensionId}-ui', | ||
extensionPath: path.join(__dirname, 'ui'), | ||
routes: [{ route: '${extensionId}', filePath: 'routes.ts' }], | ||
providers: ['providers.ts'], | ||
}`, | ||
}) | ||
.formatText(); | ||
|
||
// Add the AdminUiExtension import if it doesn't already exist | ||
addImportsToFile(pluginClass.getSourceFile(), { | ||
moduleSpecifier: '@vendure/ui-devkit/compiler', | ||
namedImports: [adminUiExtensionType], | ||
order: 0, | ||
}); | ||
|
||
// Add the path import if it doesn't already exist | ||
addImportsToFile(pluginClass.getSourceFile(), { | ||
moduleSpecifier: 'path', | ||
namespaceImport: 'path', | ||
order: 0, | ||
}); | ||
} |
24 changes: 24 additions & 0 deletions
24
...ands/add/ui-extensions/codemods/add-ui-extension-static-prop/fixtures/no-ui-prop.expected
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import * as path from 'path'; | ||
import { AdminUiExtension } from '@vendure/ui-devkit/compiler'; | ||
import { PluginCommonModule, Type, VendurePlugin } from '@vendure/core'; | ||
type PluginInitOptions = any; | ||
|
||
@VendurePlugin({ | ||
imports: [PluginCommonModule], | ||
compatibility: '^2.0.0', | ||
}) | ||
export class TestOnePlugin { | ||
static options: PluginInitOptions; | ||
|
||
static init(options: PluginInitOptions): Type<TestOnePlugin> { | ||
this.options = options; | ||
return TestOnePlugin; | ||
} | ||
|
||
static ui: AdminUiExtension = { | ||
id: 'test-one-ui', | ||
extensionPath: path.join(__dirname, 'ui'), | ||
routes: [{ route: 'test-one', filePath: 'routes.ts' }], | ||
providers: ['providers.ts'], | ||
}; | ||
} |
16 changes: 16 additions & 0 deletions
16
...ds/add/ui-extensions/codemods/add-ui-extension-static-prop/fixtures/no-ui-prop.fixture.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { PluginCommonModule, Type, VendurePlugin } from '@vendure/core'; | ||
|
||
type PluginInitOptions = any; | ||
|
||
@VendurePlugin({ | ||
imports: [PluginCommonModule], | ||
compatibility: '^2.0.0', | ||
}) | ||
export class TestOnePlugin { | ||
static options: PluginInitOptions; | ||
|
||
static init(options: PluginInitOptions): Type<TestOnePlugin> { | ||
this.options = options; | ||
return TestOnePlugin; | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
...dd/ui-extensions/codemods/update-admin-ui-plugin-init/fixtures/existing-app-prop.expected
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { AdminUiPlugin } from '@vendure/admin-ui-plugin'; | ||
import { DefaultJobQueuePlugin, dummyPaymentHandler, VendureConfig } from '@vendure/core'; | ||
import { compileUiExtensions } from '@vendure/ui-devkit/compiler'; | ||
import path from 'path'; | ||
import { TestPlugin } from './plugins/test-plugin/test.plugin'; | ||
|
||
export const config: VendureConfig = { | ||
apiOptions: { | ||
port: 3000, | ||
adminApiPath: 'admin-api', | ||
}, | ||
authOptions: { | ||
tokenMethod: ['bearer', 'cookie'], | ||
}, | ||
dbConnectionOptions: { | ||
synchronize: true, | ||
type: 'mariadb', | ||
host: '127.0.0.1', | ||
port: 3306, | ||
username: 'root', | ||
password: '', | ||
database: 'vendure-dev', | ||
}, | ||
paymentOptions: { | ||
paymentMethodHandlers: [dummyPaymentHandler], | ||
}, | ||
// When adding or altering custom field definitions, the database will | ||
// need to be updated. See the "Migrations" section in README.md. | ||
customFields: {}, | ||
plugins: [ | ||
DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }), | ||
AdminUiPlugin.init({ | ||
route: 'admin', | ||
port: 3002, | ||
adminUiConfig: { | ||
apiPort: 3000, | ||
}, | ||
app: compileUiExtensions({ | ||
outputPath: path.join(__dirname, '../admin-ui'), | ||
extensions: [ | ||
{ | ||
id: 'existing-ui-plugin', | ||
extensionPath: path.join(__dirname, 'existing-ui-plugin'), | ||
providers: ['providers.ts'], | ||
}, | ||
TestPlugin.ui, | ||
], | ||
devMode: true, | ||
}), | ||
}), | ||
], | ||
}; |
Oops, something went wrong.