plugin.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import {
  2. Inject,
  3. MiddlewareConsumer,
  4. NestModule,
  5. OnApplicationBootstrap,
  6. OnApplicationShutdown,
  7. } from '@nestjs/common';
  8. import { ModuleRef } from '@nestjs/core';
  9. import { Type } from '@vendure/common/lib/shared-types';
  10. import {
  11. Injector,
  12. Logger,
  13. PluginCommonModule,
  14. ProcessContext,
  15. registerPluginStartupMessage,
  16. VendurePlugin,
  17. } from '@vendure/core';
  18. import { AssetServer } from './asset-server';
  19. import { defaultAssetStorageStrategyFactory } from './config/default-asset-storage-strategy-factory';
  20. import { HashedAssetNamingStrategy } from './config/hashed-asset-naming-strategy';
  21. import { ImageTransformStrategy } from './config/image-transform-strategy';
  22. import { SharpAssetPreviewStrategy } from './config/sharp-asset-preview-strategy';
  23. import { ASSET_SERVER_PLUGIN_INIT_OPTIONS, loggerCtx } from './constants';
  24. import { AssetServerOptions, ImageTransformPreset } from './types';
  25. /**
  26. * @description
  27. * The `AssetServerPlugin` serves assets (images and other files) from the local file system, and can also be configured to use
  28. * other storage strategies (e.g. {@link S3AssetStorageStrategy}. It can also perform on-the-fly image transformations
  29. * and caches the results for subsequent calls.
  30. *
  31. * ## Installation
  32. *
  33. * `yarn add \@vendure/asset-server-plugin`
  34. *
  35. * or
  36. *
  37. * `npm install \@vendure/asset-server-plugin`
  38. *
  39. * @example
  40. * ```ts
  41. * import { AssetServerPlugin } from '\@vendure/asset-server-plugin';
  42. *
  43. * const config: VendureConfig = {
  44. * // Add an instance of the plugin to the plugins array
  45. * plugins: [
  46. * AssetServerPlugin.init({
  47. * route: 'assets',
  48. * assetUploadDir: path.join(__dirname, 'assets'),
  49. * }),
  50. * ],
  51. * };
  52. * ```
  53. *
  54. * The full configuration is documented at [AssetServerOptions](/reference/core-plugins/asset-server-plugin/asset-server-options)
  55. *
  56. * ## Image transformation
  57. *
  58. * Asset preview images can be transformed (resized & cropped) on the fly by appending query parameters to the url:
  59. *
  60. * `http://localhost:3000/assets/some-asset.jpg?w=500&h=300&mode=resize`
  61. *
  62. * The above URL will return `some-asset.jpg`, resized to fit in the bounds of a 500px x 300px rectangle.
  63. *
  64. * ### Preview mode
  65. *
  66. * The `mode` parameter can be either `crop` or `resize`. See the [ImageTransformMode](/reference/core-plugins/asset-server-plugin/image-transform-mode) docs for details.
  67. *
  68. * ### Focal point
  69. *
  70. * When cropping an image (`mode=crop`), Vendure will attempt to keep the most "interesting" area of the image in the cropped frame. It does this
  71. * by finding the area of the image with highest entropy (the busiest area of the image). However, sometimes this does not yield a satisfactory
  72. * result - part or all of the main subject may still be cropped out.
  73. *
  74. * This is where specifying the focal point can help. The focal point of the image may be specified by passing the `fpx` and `fpy` query parameters.
  75. * These are normalized coordinates (i.e. a number between 0 and 1), so the `fpx=0&fpy=0` corresponds to the top left of the image.
  76. *
  77. * For example, let's say there is a very wide landscape image which we want to crop to be square. The main subject is a house to the far left of the
  78. * image. The following query would crop it to a square with the house centered:
  79. *
  80. * `http://localhost:3000/assets/landscape.jpg?w=150&h=150&mode=crop&fpx=0.2&fpy=0.7`
  81. *
  82. * ### Format
  83. *
  84. * Since v1.7.0, the image format can be specified by adding the `format` query parameter:
  85. *
  86. * `http://localhost:3000/assets/some-asset.jpg?format=webp`
  87. *
  88. * This means that, no matter the format of your original asset files, you can use more modern formats in your storefront if the browser
  89. * supports them. Supported values for `format` are:
  90. *
  91. * * `jpeg` or `jpg`
  92. * * `png`
  93. * * `webp`
  94. * * `avif`
  95. *
  96. * The `format` parameter can also be combined with presets (see below).
  97. *
  98. * ### Quality
  99. *
  100. * Since v2.2.0, the image quality can be specified by adding the `q` query parameter:
  101. *
  102. * `http://localhost:3000/assets/some-asset.jpg?q=75`
  103. *
  104. * This applies to the `jpg`, `webp` and `avif` formats. The default quality value for `jpg` and `webp` is 80, and for `avif` is 50.
  105. *
  106. * The `q` parameter can also be combined with presets (see below).
  107. *
  108. * ### Transform presets
  109. *
  110. * Presets can be defined which allow a single preset name to be used instead of specifying the width, height and mode. Presets are
  111. * configured via the AssetServerOptions [presets property](/reference/core-plugins/asset-server-plugin/asset-server-options/#presets).
  112. *
  113. * For example, defining the following preset:
  114. *
  115. * ```ts
  116. * AssetServerPlugin.init({
  117. * // ...
  118. * presets: [
  119. * { name: 'my-preset', width: 85, height: 85, mode: 'crop' },
  120. * ],
  121. * }),
  122. * ```
  123. *
  124. * means that a request to:
  125. *
  126. * `http://localhost:3000/assets/some-asset.jpg?preset=my-preset`
  127. *
  128. * is equivalent to:
  129. *
  130. * `http://localhost:3000/assets/some-asset.jpg?w=85&h=85&mode=crop`
  131. *
  132. * The AssetServerPlugin comes pre-configured with the following presets:
  133. *
  134. * name | width | height | mode
  135. * -----|-------|--------|-----
  136. * tiny | 50px | 50px | crop
  137. * thumb | 150px | 150px | crop
  138. * small | 300px | 300px | resize
  139. * medium | 500px | 500px | resize
  140. * large | 800px | 800px | resize
  141. *
  142. * ### Caching
  143. * By default, the AssetServerPlugin will cache every transformed image, so that the transformation only needs to be performed a single time for
  144. * a given configuration. Caching can be disabled per-request by setting the `?cache=false` query parameter.
  145. *
  146. * ### Limiting transformations
  147. *
  148. * By default, the AssetServerPlugin will allow any transformation to be performed on an image. However, it is possible to restrict the transformations
  149. * which can be performed by using an {@link ImageTransformStrategy}. This can be used to limit the transformations to a known set of presets, for example.
  150. *
  151. * This is advisable in order to prevent abuse of the image transformation feature, as it can be computationally expensive.
  152. *
  153. * Since v3.1.0 we ship with a {@link PresetOnlyStrategy} which allows only transformations using a known set of presets.
  154. *
  155. * @example
  156. * ```ts
  157. * import { AssetServerPlugin, PresetOnlyStrategy } from '\@vendure/core';
  158. *
  159. * // ...
  160. *
  161. * AssetServerPlugin.init({
  162. * //...
  163. * imageTransformStrategy: new PresetOnlyStrategy({
  164. * defaultPreset: 'thumbnail',
  165. * permittedQuality: [0, 50, 75, 85, 95],
  166. * permittedFormats: ['jpg', 'webp', 'avif'],
  167. * allowFocalPoint: false,
  168. * }),
  169. * });
  170. * ```
  171. *
  172. * @docsCategory core plugins/AssetServerPlugin
  173. */
  174. @VendurePlugin({
  175. imports: [PluginCommonModule],
  176. configuration: async config => {
  177. const options = AssetServerPlugin.options;
  178. const storageStrategyFactory = options.storageStrategyFactory || defaultAssetStorageStrategyFactory;
  179. config.assetOptions.assetPreviewStrategy =
  180. options.previewStrategy ??
  181. new SharpAssetPreviewStrategy({
  182. maxWidth: options.previewMaxWidth,
  183. maxHeight: options.previewMaxHeight,
  184. });
  185. config.assetOptions.assetStorageStrategy = await storageStrategyFactory(options);
  186. config.assetOptions.assetNamingStrategy = options.namingStrategy || new HashedAssetNamingStrategy();
  187. return config;
  188. },
  189. providers: [
  190. { provide: ASSET_SERVER_PLUGIN_INIT_OPTIONS, useFactory: () => AssetServerPlugin.options },
  191. AssetServer,
  192. ],
  193. compatibility: '^3.0.0',
  194. })
  195. export class AssetServerPlugin implements NestModule, OnApplicationBootstrap, OnApplicationShutdown {
  196. private static options: AssetServerOptions;
  197. private readonly defaultPresets: ImageTransformPreset[] = [
  198. { name: 'tiny', width: 50, height: 50, mode: 'crop' },
  199. { name: 'thumb', width: 150, height: 150, mode: 'crop' },
  200. { name: 'small', width: 300, height: 300, mode: 'resize' },
  201. { name: 'medium', width: 500, height: 500, mode: 'resize' },
  202. { name: 'large', width: 800, height: 800, mode: 'resize' },
  203. ];
  204. /**
  205. * @description
  206. * Set the plugin options.
  207. */
  208. static init(options: AssetServerOptions): Type<AssetServerPlugin> {
  209. AssetServerPlugin.options = options;
  210. return this;
  211. }
  212. constructor(
  213. @Inject(ASSET_SERVER_PLUGIN_INIT_OPTIONS) private options: AssetServerOptions,
  214. private processContext: ProcessContext,
  215. private moduleRef: ModuleRef,
  216. private assetServer: AssetServer,
  217. ) {}
  218. /** @internal */
  219. async onApplicationBootstrap() {
  220. if (this.processContext.isWorker) {
  221. return;
  222. }
  223. if (this.options.imageTransformStrategy != null) {
  224. const injector = new Injector(this.moduleRef);
  225. for (const strategy of this.getImageTransformStrategyArray()) {
  226. if (typeof strategy.init === 'function') {
  227. await strategy.init(injector);
  228. }
  229. }
  230. }
  231. }
  232. /** @internal */
  233. async onApplicationShutdown() {
  234. if (this.processContext.isWorker) {
  235. return;
  236. }
  237. if (this.options.imageTransformStrategy != null) {
  238. for (const strategy of this.getImageTransformStrategyArray()) {
  239. if (typeof strategy.destroy === 'function') {
  240. await strategy.destroy();
  241. }
  242. }
  243. }
  244. }
  245. configure(consumer: MiddlewareConsumer) {
  246. if (this.processContext.isWorker) {
  247. return;
  248. }
  249. const presets = [...this.defaultPresets];
  250. if (this.options.presets) {
  251. for (const preset of this.options.presets) {
  252. const existingIndex = presets.findIndex(p => p.name === preset.name);
  253. if (-1 < existingIndex) {
  254. presets.splice(existingIndex, 1, preset);
  255. } else {
  256. presets.push(preset);
  257. }
  258. }
  259. }
  260. Logger.info('Creating asset server middleware', loggerCtx);
  261. const assetServerRouter = this.assetServer.createAssetServer({
  262. presets,
  263. imageTransformStrategies: this.getImageTransformStrategyArray(),
  264. });
  265. consumer.apply(assetServerRouter).forRoutes(this.options.route);
  266. registerPluginStartupMessage('Asset server', this.options.route);
  267. }
  268. private getImageTransformStrategyArray(): ImageTransformStrategy[] {
  269. return this.options.imageTransformStrategy
  270. ? Array.isArray(this.options.imageTransformStrategy)
  271. ? this.options.imageTransformStrategy
  272. : [this.options.imageTransformStrategy]
  273. : [];
  274. }
  275. }