Browse Source

feat(core): Add `compatibility` check to VendurePlugin metadata

Relates to #1471
Michael Bromley 2 năm trước cách đây
mục cha
commit
d18d350dab

+ 15 - 1
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 {}
 ```

+ 1 - 0
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;

+ 3 - 2
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) {

+ 1 - 0
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",

+ 28 - 9
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<INestApplication>;
@@ -43,6 +45,7 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
     const config = await preBootstrapConfig(userConfig);
     Logger.useLogger(config.logger);
     Logger.info(`Bootstrapping Vendure Server (pid: ${process.pid})...`);
+    checkPluginCompatibility(config);
 
     // The AppModule *must* be loaded only after the entities have been set in the
     // config, so that they are available when the AppModule decorator is evaluated.
@@ -102,6 +105,7 @@ export async function bootstrapWorker(userConfig: Partial<VendureConfig>): 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<RuntimeVendureConfig>) {
 }
 
 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<readonly [string, string]> = [];
     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));

+ 1 - 0
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 */

+ 1 - 0
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 = {};

+ 3 - 2
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<Type<any> | DynamicModule>): Array<Type<any>> {
@@ -45,8 +46,8 @@ export function getPluginAPIExtensions(
     return extensions.filter(notNullOrUndefined);
 }
 
-export function getPluginModules(plugins: Array<Type<any> | DynamicModule>): Array<Type<any>> {
-    return plugins.map(p => (isDynamicModule(p) ? p.module : p));
+export function getCompatibility(plugin: Type<any> | DynamicModule): string | undefined {
+    return reflectMetadata(plugin, PLUGIN_METADATA.COMPATIBILITY);
 }
 
 export function getConfigurationFunction(

+ 19 - 0
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<Type<any>> | (() => Array<Type<any>>);
+    /**
+     * @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

+ 1 - 1
packages/dev-server/dev-config.ts

@@ -137,7 +137,7 @@ function getDbConfig(): DataSourceOptions {
                 port: 3306,
                 username: 'root',
                 password: '',
-                database: 'vendure-dev',
+                database: 'vendure2-dev',
             };
     }
 }

+ 1 - 0
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;

+ 1 - 0
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;

+ 1 - 0
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;

+ 1 - 0
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;

+ 1 - 0
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;

+ 1 - 1
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==