From d18d350dab2073b5cf6774f96c0b282c67bd927a Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 6 Apr 2023 13:45:07 +0200 Subject: [PATCH] feat(core): Add `compatibility` check to VendurePlugin metadata Relates to #1471 --- .../plugins/writing-a-vendure-plugin.md | 16 +++++++- packages/admin-ui-plugin/src/plugin.ts | 1 + packages/asset-server-plugin/src/plugin.ts | 5 ++- packages/core/package.json | 1 + packages/core/src/bootstrap.ts | 37 ++++++++++++++----- .../default-job-queue-plugin.ts | 1 + .../default-search-plugin.ts | 1 + packages/core/src/plugin/plugin-metadata.ts | 5 ++- packages/core/src/plugin/vendure-plugin.ts | 19 ++++++++++ packages/dev-server/dev-config.ts | 2 +- packages/elasticsearch-plugin/src/plugin.ts | 1 + packages/email-plugin/src/plugin.ts | 1 + packages/harden-plugin/src/harden.plugin.ts | 1 + .../job-queue-plugin/src/bullmq/plugin.ts | 1 + .../job-queue-plugin/src/pub-sub/plugin.ts | 1 + yarn.lock | 2 +- 16 files changed, 79 insertions(+), 16 deletions(-) diff --git a/docs/content/plugins/writing-a-vendure-plugin.md b/docs/content/plugins/writing-a-vendure-plugin.md index 56047c072d..46e8256490 100644 --- a/docs/content/plugins/writing-a-vendure-plugin.md +++ b/docs/content/plugins/writing-a-vendure-plugin.md @@ -173,6 +173,19 @@ Now that we've defined the new mutation and we have a resolver capable of handli export class RandomCatPlugin {} ``` +### Step 9: Specify version compatibility + +Since Vendure v2.0.0, it is possible for a plugin to specify which versions of Vendure core it is compatible with. This is especially +important if the plugin is intended to be made publicly available via npm or another package registry. + +```TypeScript +@VendurePlugin({ + // imports: [ etc. ] + compatibility: '^2.0.0' +}) +export class RandomCatPlugin {} +``` + ### Step 8: Add the plugin to the Vendure config Finally, we need to add an instance of our plugin to the config object with which we bootstrap our Vendure server: @@ -280,7 +293,8 @@ export class RandomCatResolver { name: 'catImageUrl', }); return config; - } + }, + compatibility: '^2.0.0', }) export class RandomCatPlugin {} ``` diff --git a/packages/admin-ui-plugin/src/plugin.ts b/packages/admin-ui-plugin/src/plugin.ts index 6f3425765b..94e82c4242 100644 --- a/packages/admin-ui-plugin/src/plugin.ts +++ b/packages/admin-ui-plugin/src/plugin.ts @@ -99,6 +99,7 @@ export interface AdminUiPluginOptions { @VendurePlugin({ imports: [PluginCommonModule], providers: [], + compatibility: '^2.0.0-beta.0', }) export class AdminUiPlugin implements NestModule { private static options: AdminUiPluginOptions; diff --git a/packages/asset-server-plugin/src/plugin.ts b/packages/asset-server-plugin/src/plugin.ts index 2c2db95104..1598e5ba73 100644 --- a/packages/asset-server-plugin/src/plugin.ts +++ b/packages/asset-server-plugin/src/plugin.ts @@ -139,6 +139,7 @@ import { AssetServerOptions, ImageTransformPreset } from './types'; @VendurePlugin({ imports: [PluginCommonModule], configuration: config => AssetServerPlugin.configure(config), + compatibility: '^2.0.0-beta.0', }) export class AssetServerPlugin implements NestModule, OnApplicationBootstrap { private static assetStorage: AssetStorageStrategy; @@ -246,7 +247,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap { mimeType = (await fromBuffer(file))?.mime || 'application/octet-stream'; } res.contentType(mimeType); - res.setHeader('content-security-policy', 'default-src \'self\''); + res.setHeader('content-security-policy', "default-src 'self'"); res.setHeader('Cache-Control', this.cacheHeader); res.send(file); } catch (e: any) { @@ -291,7 +292,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap { mimeType = (await fromBuffer(imageBuffer))?.mime || 'image/jpeg'; } res.set('Content-Type', mimeType); - res.setHeader('content-security-policy', 'default-src \'self\''); + res.setHeader('content-security-policy', "default-src 'self'"); res.send(imageBuffer); return; } catch (e: any) { diff --git a/packages/core/package.json b/packages/core/package.json index 0f3ae163e0..75f0e6e0bb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -94,6 +94,7 @@ "@types/node": "^14.14.31", "@types/progress": "^2.0.3", "@types/prompts": "^2.0.9", + "@types/semver": "^7.3.13", "better-sqlite3": "^7.1.1", "gulp": "^4.0.2", "mysql": "^2.18.1", diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index fce786c64d..c0c2476217 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -3,6 +3,7 @@ import { NestFactory } from '@nestjs/core'; import { getConnectionToken } from '@nestjs/typeorm'; import { Type } from '@vendure/common/lib/shared-types'; import cookieSession = require('cookie-session'); +import { satisfies } from 'semver'; import { Connection, DataSourceOptions, EntitySubscriberInterface } from 'typeorm'; import { InternalServerError } from './common/error/errors'; @@ -17,9 +18,10 @@ import { runEntityMetadataModifiers } from './entity/run-entity-metadata-modifie import { setEntityIdStrategy } from './entity/set-entity-id-strategy'; import { setMoneyStrategy } from './entity/set-money-strategy'; import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config'; -import { getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata'; +import { getCompatibility, getConfigurationFunction, getEntitiesFromPlugins } from './plugin/plugin-metadata'; import { getPluginStartupMessages } from './plugin/plugin-utils'; import { setProcessContext } from './process-context/process-context'; +import { VENDURE_VERSION } from './version'; import { VendureWorker } from './worker/vendure-worker'; export type VendureBootstrapFunction = (config: VendureConfig) => Promise; @@ -43,6 +45,7 @@ export async function bootstrap(userConfig: Partial): Promise): Promi config.logger.setDefaultContext?.('Vendure Worker'); Logger.useLogger(config.logger); Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`); + checkPluginCompatibility(config); setProcessContext('worker'); DefaultLogger.hideNestBoostrapLogs(); @@ -159,6 +163,28 @@ export async function preBootstrapConfig( return config; } +function checkPluginCompatibility(config: RuntimeVendureConfig): void { + for (const plugin of config.plugins) { + const compatibility = getCompatibility(plugin); + const pluginName = (plugin as any).name as string; + if (!compatibility) { + Logger.info( + `The plugin "${pluginName}" does not specify a compatibility range, so it is not guaranteed to be compatible with this version of Vendure.`, + ); + } else { + if (!satisfies(VENDURE_VERSION, compatibility)) { + Logger.error( + `Plugin "${pluginName}" is not compatible with this version of Vendure. ` + + `It specifies a semver range of "${compatibility}" but the current version is "${VENDURE_VERSION}".`, + ); + throw new InternalServerError( + `Plugin "${pluginName}" is not compatible with this version of Vendure.`, + ); + } + } + } +} + /** * Initialize any configured plugins. */ @@ -223,13 +249,6 @@ function setExposedHeaders(config: Readonly) { } function logWelcomeMessage(config: RuntimeVendureConfig) { - let version: string; - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - version = require('../package.json').version; - } catch (e: any) { - version = ' unknown'; - } const { port, shopApiPath, adminApiPath, hostname } = config.apiOptions; const apiCliGreetings: Array = []; const pathToUrl = (path: string) => `http://${hostname || 'localhost'}:${port}/${path}`; @@ -239,7 +258,7 @@ function logWelcomeMessage(config: RuntimeVendureConfig) { ...getPluginStartupMessages().map(({ label, path }) => [label, pathToUrl(path)] as const), ); const columnarGreetings = arrangeCliGreetingsInColumns(apiCliGreetings); - const title = `Vendure server (v${version}) now running on port ${port}`; + const title = `Vendure server (v${VENDURE_VERSION}) now running on port ${port}`; const maxLineLength = Math.max(title.length, ...columnarGreetings.map(l => l.length)); const titlePadLength = title.length < maxLineLength ? Math.floor((maxLineLength - title.length) / 2) : 0; Logger.info('='.repeat(maxLineLength)); diff --git a/packages/core/src/plugin/default-job-queue-plugin/default-job-queue-plugin.ts b/packages/core/src/plugin/default-job-queue-plugin/default-job-queue-plugin.ts index 3cebf5d953..66a2390cdd 100644 --- a/packages/core/src/plugin/default-job-queue-plugin/default-job-queue-plugin.ts +++ b/packages/core/src/plugin/default-job-queue-plugin/default-job-queue-plugin.ts @@ -188,6 +188,7 @@ export interface DefaultJobQueueOptions { } return config; }, + compatibility: '*', }) export class DefaultJobQueuePlugin { /** @internal */ diff --git a/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts b/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts index eec36db35f..cf381f25c1 100644 --- a/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts +++ b/packages/core/src/plugin/default-search-plugin/default-search-plugin.ts @@ -90,6 +90,7 @@ export interface DefaultSearchReindexResponse extends SearchReindexResponse { resolvers: [ShopFulltextSearchResolver], }, entities: [SearchIndexItem], + compatibility: '*', }) export class DefaultSearchPlugin implements OnApplicationBootstrap, OnApplicationShutdown { static options: DefaultSearchPluginInitOptions = {}; diff --git a/packages/core/src/plugin/plugin-metadata.ts b/packages/core/src/plugin/plugin-metadata.ts index 53798f1b13..0a99a0531e 100644 --- a/packages/core/src/plugin/plugin-metadata.ts +++ b/packages/core/src/plugin/plugin-metadata.ts @@ -10,6 +10,7 @@ export const PLUGIN_METADATA = { SHOP_API_EXTENSIONS: 'shopApiExtensions', ADMIN_API_EXTENSIONS: 'adminApiExtensions', ENTITIES: 'entities', + COMPATIBILITY: 'compatibility', }; export function getEntitiesFromPlugins(plugins?: Array | DynamicModule>): Array> { @@ -45,8 +46,8 @@ export function getPluginAPIExtensions( return extensions.filter(notNullOrUndefined); } -export function getPluginModules(plugins: Array | DynamicModule>): Array> { - return plugins.map(p => (isDynamicModule(p) ? p.module : p)); +export function getCompatibility(plugin: Type | DynamicModule): string | undefined { + return reflectMetadata(plugin, PLUGIN_METADATA.COMPATIBILITY); } export function getConfigurationFunction( diff --git a/packages/core/src/plugin/vendure-plugin.ts b/packages/core/src/plugin/vendure-plugin.ts index 155d5b4cad..b621c45261 100644 --- a/packages/core/src/plugin/vendure-plugin.ts +++ b/packages/core/src/plugin/vendure-plugin.ts @@ -43,6 +43,25 @@ export interface VendurePluginMetadata extends ModuleMetadata { * The plugin may define custom [TypeORM database entities](https://typeorm.io/#/entities). */ entities?: Array> | (() => Array>); + /** + * @description + * The plugin should define a valid [semver version string](https://www.npmjs.com/package/semver) to indicate which versions of + * Vendure core it is compatible with. Attempting to use a plugin with an incompatible + * version of Vendure will result in an error and the server will be unable to bootstrap. + * + * If a plugin does not define this property, a message will be logged on bootstrap that the plugin is not + * guaranteed to be compatible with the current version of Vendure. + * + * To effectively disable this check for a plugin, you can use an overly-permissive string such as `*`. + * + * @example + * ```typescript + * compatibility: '^2.0.0' + * ``` + * + * @since 2.0.0 + */ + compatibility?: string; } /** * @description diff --git a/packages/dev-server/dev-config.ts b/packages/dev-server/dev-config.ts index 8ead251122..72e145a291 100644 --- a/packages/dev-server/dev-config.ts +++ b/packages/dev-server/dev-config.ts @@ -137,7 +137,7 @@ function getDbConfig(): DataSourceOptions { port: 3306, username: 'root', password: '', - database: 'vendure-dev', + database: 'vendure2-dev', }; } } diff --git a/packages/elasticsearch-plugin/src/plugin.ts b/packages/elasticsearch-plugin/src/plugin.ts index 1794f34bdf..4fe62233ed 100644 --- a/packages/elasticsearch-plugin/src/plugin.ts +++ b/packages/elasticsearch-plugin/src/plugin.ts @@ -251,6 +251,7 @@ function getCustomResolvers(options: ElasticsearchRuntimeOptions) { // which looks like possibly a TS/definitions bug. schema: () => generateSchemaExtensions(ElasticsearchPlugin.options as any), }, + compatibility: '^2.0.0-beta.0', }) export class ElasticsearchPlugin implements OnApplicationBootstrap { private static options: ElasticsearchRuntimeOptions; diff --git a/packages/email-plugin/src/plugin.ts b/packages/email-plugin/src/plugin.ts index 0c977da53a..e977e31d69 100644 --- a/packages/email-plugin/src/plugin.ts +++ b/packages/email-plugin/src/plugin.ts @@ -233,6 +233,7 @@ import { @VendurePlugin({ imports: [PluginCommonModule], providers: [{ provide: EMAIL_PLUGIN_OPTIONS, useFactory: () => EmailPlugin.options }, EmailProcessor], + compatibility: '^2.0.0-beta.0', }) export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdown, NestModule { private static options: EmailPluginOptions | EmailPluginDevModeOptions; diff --git a/packages/harden-plugin/src/harden.plugin.ts b/packages/harden-plugin/src/harden.plugin.ts index 593f61c20c..8e44b6e876 100644 --- a/packages/harden-plugin/src/harden.plugin.ts +++ b/packages/harden-plugin/src/harden.plugin.ts @@ -160,6 +160,7 @@ import { HardenPluginOptions } from './types'; return config; }, + compatibility: '^2.0.0-beta.0', }) export class HardenPlugin { static options: HardenPluginOptions; diff --git a/packages/job-queue-plugin/src/bullmq/plugin.ts b/packages/job-queue-plugin/src/bullmq/plugin.ts index d678e2ca08..dc59a900ac 100644 --- a/packages/job-queue-plugin/src/bullmq/plugin.ts +++ b/packages/job-queue-plugin/src/bullmq/plugin.ts @@ -111,6 +111,7 @@ import { BullMQPluginOptions } from './types'; { provide: BULLMQ_PLUGIN_OPTIONS, useFactory: () => BullMQJobQueuePlugin.options }, RedisHealthIndicator, ], + compatibility: '^2.0.0-beta.0', }) export class BullMQJobQueuePlugin { static options: BullMQPluginOptions; diff --git a/packages/job-queue-plugin/src/pub-sub/plugin.ts b/packages/job-queue-plugin/src/pub-sub/plugin.ts index 606f6cdf2e..db4029844b 100644 --- a/packages/job-queue-plugin/src/pub-sub/plugin.ts +++ b/packages/job-queue-plugin/src/pub-sub/plugin.ts @@ -15,6 +15,7 @@ import { PubSubJobQueueStrategy } from './pub-sub-job-queue-strategy'; config.jobQueueOptions.jobQueueStrategy = new PubSubJobQueueStrategy(); return config; }, + compatibility: '^2.0.0-beta.0', }) export class PubSubPlugin { private static options: PubSubOptions; diff --git a/yarn.lock b/yarn.lock index deab643200..0fd1a7271e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4316,7 +4316,7 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.2.3.tgz#5798ecf1bec94eaa64db39ee52808ec0693315aa" integrity sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A== -"@types/semver@^7.3.12": +"@types/semver@^7.3.12", "@types/semver@^7.3.13": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==