Explorar o código

docs: Add complete plugin tutorial

Michael Bromley %!s(int64=2) %!d(string=hai) anos
pai
achega
2645afb855
Modificáronse 45 ficheiros con 1221 adicións e 535 borrados
  1. 2 0
      docs/.gitignore
  2. 0 156
      docs/docs/guides/developer-guide/configuration.md
  3. 0 35
      docs/docs/guides/developer-guide/overview/index.md
  4. 0 0
      docs/docs/guides/extending-the-admin-ui/add-actions-to-pages/bulk-actions-screenshot.webp
  5. 0 0
      docs/docs/guides/extending-the-admin-ui/add-actions-to-pages/index.md
  6. 0 0
      docs/docs/guides/extending-the-admin-ui/add-actions-to-pages/ui-extensions-actionbar.webp
  7. 0 0
      docs/docs/guides/extending-the-admin-ui/adding-ui-translations/index.md
  8. 0 0
      docs/docs/guides/extending-the-admin-ui/adding-ui-translations/ui-translations-01.webp
  9. 0 0
      docs/docs/guides/extending-the-admin-ui/admin-ui-theming-branding/index.md
  10. 0 0
      docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md
  11. 0 0
      docs/docs/guides/extending-the-admin-ui/creating-list-views/index.md
  12. 0 0
      docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md
  13. 0 0
      docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md
  14. 0 0
      docs/docs/guides/extending-the-admin-ui/custom-form-inputs/ui-extensions-custom-field-default.webp
  15. 0 0
      docs/docs/guides/extending-the-admin-ui/custom-form-inputs/ui-extensions-custom-field-slider.webp
  16. 0 0
      docs/docs/guides/extending-the-admin-ui/custom-timeline-components/index.md
  17. 0 0
      docs/docs/guides/extending-the-admin-ui/custom-timeline-components/timeline-entry.webp
  18. 0 0
      docs/docs/guides/extending-the-admin-ui/dashboard-widgets/dashboard-widgets.webp
  19. 0 0
      docs/docs/guides/extending-the-admin-ui/dashboard-widgets/index.md
  20. 0 1
      docs/docs/guides/extending-the-admin-ui/extending-the-admin-ui.md
  21. 0 0
      docs/docs/guides/extending-the-admin-ui/modifying-navigation-items/index.md
  22. 0 0
      docs/docs/guides/extending-the-admin-ui/modifying-navigation-items/ui-extensions-navbar.webp
  23. 0 0
      docs/docs/guides/extending-the-admin-ui/modifying-navigation-items/ui-extensions-tabs.webp
  24. 0 0
      docs/docs/guides/extending-the-admin-ui/using-angular/index.md
  25. 0 0
      docs/docs/guides/extending-the-admin-ui/using-angular/ui-extensions-greeter.webp
  26. 0 0
      docs/docs/guides/extending-the-admin-ui/using-other-frameworks/index.md
  27. 0 0
      docs/docs/guides/extending-the-admin-ui/using-other-frameworks/ui-extensions-cra.jpg
  28. 226 0
      docs/docs/guides/getting-started/configuration.md
  29. 42 28
      docs/docs/guides/getting-started/installation.md
  30. 37 0
      docs/docs/guides/getting-started/key-concepts/index.md
  31. 0 0
      docs/docs/guides/getting-started/key-concepts/vendure_architecture.png
  32. 639 0
      docs/docs/guides/getting-started/writing-a-vendure-plugin.md
  33. 0 300
      docs/docs/guides/plugins/writing-a-vendure-plugin.md
  34. 5 0
      docs/docusaurus.config.js
  35. 4 0
      docs/scraper/.env.example
  36. 32 0
      docs/scraper/readme.md
  37. 32 13
      docs/sidebars.js
  38. 2 2
      docs/src/css/custom.css
  39. 3 0
      packages/dev-server/example-plugins/wishlist-plugin/README.md
  40. 20 0
      packages/dev-server/example-plugins/wishlist-plugin/api/api-extensions.ts
  41. 33 0
      packages/dev-server/example-plugins/wishlist-plugin/api/wishlist.resolver.ts
  42. 15 0
      packages/dev-server/example-plugins/wishlist-plugin/entities/wishlist-item.entity.ts
  43. 92 0
      packages/dev-server/example-plugins/wishlist-plugin/service/wishlist.service.ts
  44. 9 0
      packages/dev-server/example-plugins/wishlist-plugin/types.ts
  45. 28 0
      packages/dev-server/example-plugins/wishlist-plugin/wishlist.plugin.ts

+ 2 - 0
docs/.gitignore

@@ -1,3 +1,5 @@
+/scraper/*.txt
+
 # Dependencies
 /node_modules
 

+ 0 - 156
docs/docs/guides/developer-guide/configuration.md

@@ -1,156 +0,0 @@
----
-title: "Configuration"
-showtoc: true
----
-
-# Configuration
-
-Every aspect of the Vendure server is configured via a single, central `VendureConfig` object. This object is passed into the [`bootstrap`]({{< relref "bootstrap" >}}) and [`bootstrapWorker`]({{< relref "bootstrap-worker" >}}) functions to start up the Vendure server and worker respectively.
-
-The `VendureConfig` object is organised into sections, grouping related settings together. For example, [`VendureConfig.apiOptions`]({{< relref "api-options" >}}) contains all the config for the GraphQL APIs, whereas [`VendureConfig.authOptions`]({{< relref "auth-options" >}}) deals with authentication.
-
-## Working with the VendureConfig object
-
-Since the `VendureConfig` is just a JavaScript object, it can be managed and manipulated according to your needs. For example:
-
-### Using environment variables
-
-Environment variables can be used when you don't want to hard-code certain values which may change, e.g. depending on whether running locally, in staging or in production:
-
-```ts
-export const config: VendureConfig = {
-  apiOptions: {
-    hostname: process.env.HOSTNAME,
-    port: process.env.PORT,
-  }
-  // ...
-};
-```
-
-They are also useful so that sensitive credentials do not need to be hard-coded and committed to source control:
-
-```ts
-export const config: VendureConfig = {
-  dbConnectionOptions: {
-    type: 'postgres',
-    username: process.env.DB_USERNAME,
-    password: process.env.DB_PASSWORD,
-    database: 'vendure',
-  },
-  // ...
-}
-```
-
-### Splitting config across files
-
-If the config object grows too large, you can split it across several files. For example, the `plugins` array in a real-world project can easily grow quite big:
-
-```ts
-// vendure-config-plugins.ts
-
-export const plugins: VendureConfig['plugins'] = [
-  CustomPlugin,
-  AssetServerPlugin.init({
-      route: 'assets',
-      assetUploadDir: path.join(__dirname, 'assets'),
-      port: 5002,
-  }),
-  DefaultJobQueuePlugin,
-  ElasticsearchPlugin.init({
-      host: 'localhost',
-      port: 9200,
-  }),
-  EmailPlugin.init({
-    // ...lots of lines of config
-  }),
-];
-```
-
-```ts
-// vendure-config.ts
-
-import { plugins } from 'vendure-config-plugins';
-
-export const config: VendureConfig = {
-  plugins,
-  // ...
-}
-```
-
-## Important Configuration Settings
-
-In this guide, we will take a look at those configuration options needed for getting the server up and running.
-
-{{< alert "primary" >}}
-A description of every available configuration option can be found in the [VendureConfig API documentation]({{< relref "vendure-config" >}}).
-{{< /alert >}}
-
-### Specifying API hostname & port etc
-
-The [`VendureConfig.apiOptions`]({{< relref "api-options" >}}) object is used to set the hostname, port, as well as other API-related concerns. Express middleware and Apollo Server plugins may also be specified here.
-
-Example:
-
-```ts
-export const config: VendureConfig = {
-  apiOptions: {
-    hostname: 'localhost',
-    port: 3000,
-    adminApiPath: '/admin',
-    shopApiPath: '/shop',
-    middleware: [{
-      // add some Express middleware to the Shop API route
-      handler: timeout('5s'),
-      route: 'shop',
-    }]
-  },
-  // ...
-}
-```
-
-### Connecting to the database
-
-The database connection is configured with the `VendureConfig.dbConnectionOptions` object. This object is actually the [TypeORM DataSourceOptions object](https://typeorm.io/data-source-options) and is passed directly to TypeORM.
-
-Example:
-
-```ts
-export const config: VendureConfig = {
-  dbConnectionOptions: {
-    type: 'postgres',
-    host: process.env.DB_HOST,
-    port: process.env.DB_PORT,
-    synchronize: false,
-    username: process.env.DB_USERNAME,
-    password: process.env.DB_PASSWORD,
-    database: 'vendure',
-    migrations: [path.join(__dirname, 'migrations/*.ts')],
-  },
-  // ...
-}
-```
-
-### Configuring authentication
-
-Authentication settings are configured with [`VendureConfig.authOptions`]({{< relref "auth-options" >}}). The most important setting here is whether the storefront client will use cookies or bearer tokens to manage user sessions. For more detail on this topic, see [the Managing Sessions guide]({{< relref "managing-sessions" >}}).
-
-The username and default password of the superadmin user can also be specified here. In production, it is advisable to use environment variables for these settings.
-
-Example:
-
-```ts
-export const config: VendureConfig = {
-  authOptions: {
-    tokenMethod: 'cookie',
-    requireVerification: true,
-    cookieOptions: {
-      secret: process.env.COOKIE_SESSION_SECRET,
-    },
-    superadminCredentials: {
-      identifier: process.env.SUPERADMIN_USERNAME,
-      password: process.env.SUPERADMIN_PASSWORD,
-    },
-  },
-  // ...
-}
-```

+ 0 - 35
docs/docs/guides/developer-guide/overview/index.md

@@ -1,35 +0,0 @@
----
-title: "Architecture Overview"
-weight: 0
-showtoc: true
----
-
-# Vendure Architecture Overview
-
-Vendure is built with an internal architecture based on [NestJS modules](https://docs.nestjs.com/modules). It is not necessary to be familiar with all the internal modules, but a simplified overview can help to see how the major parts fit together.
-Here is a simplified diagram of the Vendure application architecture:
-
-![./vendure_architecture.png](./vendure_architecture.png) 
-
-## Entry Points
-
-As you can see in the diagram, there are two entry points into the application: [`bootstrap()`]({{< relref "bootstrap" >}}) and [`bootstrapWorker()`]({{< relref "bootstrap-worker" >}}), which start the main server and the [worker]({{< relref "/guides/developer-guide/vendure-worker" >}}) respectively. Communication between server and worker(s) is done via the [Job Queue]({{< relref "/guides/developer-guide/job-queue" >}}).
-
-## GraphQL APIs
-
-There are 2 separate GraphQL APIs: shop and admin. 
-
-* The **Shop API** is used by public-facing client applications (web shops, e-commerce apps, mobile apps etc.) to allow customers to find products and place orders. 
-    
-    [Shop API Documentation]({{< relref "/reference/graphql-api/shop" >}}).
-* The **Admin API** is used by administrators to manage products, customers and orders. 
-
-    [Admin API Documentation]({{< relref "/reference/graphql-api/admin" >}}).
-
-## Database
-
-Vendure officially supports multiple databases: MySQL/MariaDB, PostgreSQL, SQLite and SQL.js, plus API-compatible cloud versions of these such as Amazon Aurora. Since Vendure uses [TypeORM](https://typeorm.io/#/) to manage data access, it can theoretically also work with other relational databases supported by TypeORM such as CockroachDB, Microsoft SQL Server, though these are not guaranteed to work in all cases, as they are not covered in our testing.
-
-## Custom Business Logic (Plugins)
-
-Not shown on the diagram (for the sake of simplicity) are plugins. Plugins are the mechanism by which you extend Vendure with your own business logic and functionality. See [the Plugins docs]({{< relref "/guides/plugins" >}})

+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/add-actions-to-pages/bulk-actions-screenshot.webp → docs/docs/guides/extending-the-admin-ui/add-actions-to-pages/bulk-actions-screenshot.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/add-actions-to-pages/index.md → docs/docs/guides/extending-the-admin-ui/add-actions-to-pages/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/add-actions-to-pages/ui-extensions-actionbar.webp → docs/docs/guides/extending-the-admin-ui/add-actions-to-pages/ui-extensions-actionbar.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/adding-ui-translations/index.md → docs/docs/guides/extending-the-admin-ui/adding-ui-translations/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/adding-ui-translations/ui-translations-01.webp → docs/docs/guides/extending-the-admin-ui/adding-ui-translations/ui-translations-01.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/admin-ui-theming-branding/index.md → docs/docs/guides/extending-the-admin-ui/admin-ui-theming-branding/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/creating-detail-views/index.md → docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/creating-list-views/index.md → docs/docs/guides/extending-the-admin-ui/creating-list-views/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/custom-detail-components/index.md → docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/custom-form-inputs/index.md → docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/custom-form-inputs/ui-extensions-custom-field-default.webp → docs/docs/guides/extending-the-admin-ui/custom-form-inputs/ui-extensions-custom-field-default.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/custom-form-inputs/ui-extensions-custom-field-slider.webp → docs/docs/guides/extending-the-admin-ui/custom-form-inputs/ui-extensions-custom-field-slider.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/custom-timeline-components/index.md → docs/docs/guides/extending-the-admin-ui/custom-timeline-components/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/custom-timeline-components/timeline-entry.webp → docs/docs/guides/extending-the-admin-ui/custom-timeline-components/timeline-entry.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/dashboard-widgets/dashboard-widgets.webp → docs/docs/guides/extending-the-admin-ui/dashboard-widgets/dashboard-widgets.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/dashboard-widgets/index.md → docs/docs/guides/extending-the-admin-ui/dashboard-widgets/index.md


+ 0 - 1
docs/docs/guides/plugins/extending-the-admin-ui/index.md → docs/docs/guides/extending-the-admin-ui/extending-the-admin-ui.md

@@ -1,6 +1,5 @@
 ---
 title: 'Extending the Admin UI'
-weight: 5
 ---
 
 # Extending the Admin UI

+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/modifying-navigation-items/index.md → docs/docs/guides/extending-the-admin-ui/modifying-navigation-items/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/modifying-navigation-items/ui-extensions-navbar.webp → docs/docs/guides/extending-the-admin-ui/modifying-navigation-items/ui-extensions-navbar.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/modifying-navigation-items/ui-extensions-tabs.webp → docs/docs/guides/extending-the-admin-ui/modifying-navigation-items/ui-extensions-tabs.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/using-angular/index.md → docs/docs/guides/extending-the-admin-ui/using-angular/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/using-angular/ui-extensions-greeter.webp → docs/docs/guides/extending-the-admin-ui/using-angular/ui-extensions-greeter.webp


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/using-other-frameworks/index.md → docs/docs/guides/extending-the-admin-ui/using-other-frameworks/index.md


+ 0 - 0
docs/docs/guides/plugins/extending-the-admin-ui/using-other-frameworks/ui-extensions-cra.jpg → docs/docs/guides/extending-the-admin-ui/using-other-frameworks/ui-extensions-cra.jpg


+ 226 - 0
docs/docs/guides/getting-started/configuration.md

@@ -0,0 +1,226 @@
+---
+title: "Configuration"
+sidebar_position: 3
+---
+
+# Configuration
+
+Every aspect of the Vendure server is configured via a single, central [`VendureConfig`](/reference/typescript-api/configuration/vendure-config/) object. This object is passed into the [`bootstrap`](/reference/typescript-api/common/bootstrap/) and [`bootstrapWorker`](/reference/typescript-api/worker/bootstrap-worker/) functions to start up the Vendure server and worker respectively.
+
+The `VendureConfig` object is organised into sections, grouping related settings together. For example, [`VendureConfig.apiOptions`](/reference/typescript-api/configuration/api-options/) contains all the config for the GraphQL APIs, whereas [`VendureConfig.authOptions`](/reference/typescript-api/auth/auth-options/) deals with authentication.
+
+## Important Configuration Settings
+
+In this guide, we will take a look at those configuration options needed for getting the server up and running.
+
+:::tip
+A description of every available configuration option can be found in the [`VendureConfig` reference docs](/reference/typescript-api/configuration/vendure-config/).
+:::
+
+### Specifying API hostname & port etc
+
+The [`VendureConfig.apiOptions`](/reference/typescript-api/configuration/api-options/) object is used to set the hostname, port, as well as other API-related concerns. Express middleware and Apollo Server plugins may also be specified here.
+
+Example:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  apiOptions: {
+    hostname: 'localhost',
+    port: 3000,
+    adminApiPath: '/admin',
+    shopApiPath: '/shop',
+    middleware: [{
+      // add some Express middleware to the Shop API route
+      handler: timeout('5s'),
+      route: 'shop',
+    }]
+  },
+  // ...
+}
+```
+
+### Connecting to the database
+
+The database connection is configured with the `VendureConfig.dbConnectionOptions` object. This object is actually the [TypeORM DataSourceOptions object](https://typeorm.io/data-source-options) and is passed directly to TypeORM.
+
+Example:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  dbConnectionOptions: {
+    type: 'postgres',
+    host: process.env.DB_HOST,
+    port: process.env.DB_PORT,
+    synchronize: false,
+    username: process.env.DB_USERNAME,
+    password: process.env.DB_PASSWORD,
+    database: 'vendure',
+    migrations: [path.join(__dirname, 'migrations/*.ts')],
+  },
+  // ...
+}
+```
+
+### Configuring authentication
+
+Authentication settings are configured with [`VendureConfig.authOptions`](/reference/typescript-api/auth/auth-options/). The most important setting here is whether the storefront client will use cookies or bearer tokens to manage user sessions. For more detail on this topic, see [the Managing Sessions guide](TODO).
+
+The username and default password of the superadmin user can also be specified here. In production, it is advisable to use environment variables for these settings (see the following section on usage of environment variables).
+
+Example:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  authOptions: {
+    tokenMethod: 'cookie',
+    requireVerification: true,
+    cookieOptions: {
+      secret: process.env.COOKIE_SESSION_SECRET,
+    },
+    superadminCredentials: {
+      identifier: process.env.SUPERADMIN_USERNAME,
+      password: process.env.SUPERADMIN_PASSWORD,
+    },
+  },
+  // ...
+}
+```
+
+## Working with the VendureConfig object
+
+Since the `VendureConfig` is just a JavaScript object, it can be managed and manipulated according to your needs. For example:
+
+### Using environment variables
+
+Environment variables can be used when you don't want to hard-code certain values which may change, e.g. depending on whether running locally, in staging or in production:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  apiOptions: {
+    hostname: process.env.HOSTNAME,
+    port: process.env.PORT,
+  }
+  // ...
+};
+```
+
+They are also useful so that sensitive credentials do not need to be hard-coded and committed to source control:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  dbConnectionOptions: {
+    type: 'postgres',
+    username: process.env.DB_USERNAME,
+    password: process.env.DB_PASSWORD,
+    database: 'vendure',
+  },
+  // ...
+}
+```
+
+When you create a Vendure project with `@vendure/create`, it comes with the [dotenv](https://www.npmjs.com/package/dotenv) package installed, which allows you to store environment variables in a `.env` file in the root of your project.
+
+To define new environment variables, you can add them to the `.env` file. For instance, if you are using a plugin that requires
+an API key, you can
+
+```txt title=".env"
+APP_ENV=dev
+COOKIE_SECRET=toh8soqdlj
+SUPERADMIN_USERNAME=superadmin
+SUPERADMIN_PASSWORD=superadmin
+// highlight-next-line
+MY_API_KEY=12345
+```
+
+In order to tell TypeScript about the existence of this new variable, you can add it to the `src/environment.d.ts` file:
+
+```ts title="src/environment.d.ts"
+export {};
+
+// Here we declare the members of the process.env object, so that we
+// can use them in our application code in a type-safe manner.
+declare global {
+    namespace NodeJS {
+        interface ProcessEnv {
+            APP_ENV: string;
+            COOKIE_SECRET: string;
+            SUPERADMIN_USERNAME: string;
+            SUPERADMIN_PASSWORD: string;
+            // highlight-next-line
+            MY_API_KEY: string;
+        }
+    }
+}
+````
+
+You can then use the environment variable in your config file:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  plugins: [
+    MyPlugin.init({
+      apiKey: process.env.MY_API_KEY,
+    }),
+  ],
+  // ...
+}
+```
+
+:::info
+
+In production, the way you manage environment variables will depend on your hosting provider. Read more about this in our [Production Configuration guide](/guides/deployment/production-configuration/).
+
+:::
+
+
+
+### Splitting config across files
+
+If the config object grows too large, you can split it across several files. For example, the `plugins` array in a real-world project can easily grow quite big:
+
+```ts title="src/vendure-config-plugins.ts"
+import { AssetServerPlugin, DefaultJobQueuePlugin, VendureConfig } from '@vendure/core';
+import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
+import { EmailPlugin } from '@vendure/email-plugin';
+import { CustomPlugin } from './plugins/custom-plugin';
+
+export const plugins: VendureConfig['plugins'] = [
+  CustomPlugin,
+  AssetServerPlugin.init({
+      route: 'assets',
+      assetUploadDir: path.join(__dirname, 'assets'),
+      port: 5002,
+  }),
+  DefaultJobQueuePlugin,
+  ElasticsearchPlugin.init({
+      host: 'localhost',
+      port: 9200,
+  }),
+  EmailPlugin.init({
+    // ...lots of lines of config
+  }),
+];
+```
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { plugins } from 'vendure-config-plugins';
+
+export const config: VendureConfig = {
+  plugins,
+  // ...
+}
+```

+ 42 - 28
docs/docs/guides/getting-started.md → docs/docs/guides/getting-started/installation.md

@@ -1,8 +1,11 @@
 ---
-title: "Getting Started"
-weight: 0
+title: "Installation"
+sidebar_position: 1
 ---
 
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
 # Getting Started
 
 ## Requirements
@@ -15,40 +18,43 @@ weight: 0
 
 The recommended way to get started with Vendure is by using the [@vendure/create](https://github.com/vendure-ecommerce/vendure/tree/master/packages/create) tool. This is a command-line tool which will scaffold and configure your new Vendure project and install all dependencies.
 
-{{% tab "npx" %}}
-```sh
-npx @vendure/create my-app
+
+<Tabs>
+<TabItem value="npx" label="npx" default>
+
+```
+npx @vendure/create my-shop
 ```
 
-{{< alert primary >}}
-[npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) comes with npm 5.2+ and higher.
-{{< /alert >}}
-{{% /tab %}}
-{{% tab "npm init" %}}
-```sh
-npm init @vendure my-app
+</TabItem>
+<TabItem value="npm init" label="npm init">
+
 ```
-{{< alert primary >}}
-`npm init <initializer>` is available in npm 6+
-{{< /alert >}}
-{{% /tab %}}
-{{% tab "yarn create" %}}
-```sh
-yarn create @vendure my-app
+npm init @vendure my-shop
+```
+
+</TabItem>
+<TabItem value="yarn create" label="yarn create">
+
 ```
-{{< alert primary >}}
-`yarn create` is available in Yarn 0.25+
-{{< /alert >}}
-{{% /tab %}}
+yarn create @vendure my-shop
+``` 
+
+</TabItem>
+</Tabs>
+
+:::note
+By default, the `@vendure/create` tool will use [Yarn](https://yarnpkg.com/) to manage your dependencies if you have it installed. If you want to force it to use npm, use the `--use-npm` flag.
+:::
 
 *For other installation options see the [@vendure/create documentation](https://github.com/vendure-ecommerce/vendure/blob/master/packages/create/README.md).*
 
 
-"my-app" in the above command would be replaced by whatever you'd like to name your new project.
+"my-shop" in the above command would be replaced by whatever you'd like to name your new project.
 Vendure Create will guide you through the setup. When done, you can run:
 
 ```sh
-cd my-app
+cd my-shop
 
 yarn dev
 # or
@@ -61,12 +67,20 @@ Assuming the default config settings, you can now access:
 * The Vendure Shop GraphQL API: [http://localhost:3000/shop-api](http://localhost:3000/shop-api)
 * The Vendure Admin UI: [http://localhost:3000/admin](http://localhost:3000/admin)
 
-{{< alert primary >}}
-Log in with the superadmin credentials you specified, which default to:
+:::tip
+Open [http://localhost:3000/admin](http://localhost:3000/admin) in your browser and log in with the superadmin credentials you specified, which default to:
 
 * **username**: superadmin
 * **password**: superadmin
-{{< /alert >}}
+:::
+
+## Troubleshooting
+
+If you encounter any issues during installation, you can get a more detailed output by setting the log level to `verbose`:
+
+```sh
+npx @vendure/create my-shop --log-level verbose
+```
 
 ## Next Steps
 

+ 37 - 0
docs/docs/guides/getting-started/key-concepts/index.md

@@ -0,0 +1,37 @@
+---
+title: "Vendure Key Concepts"
+sidebar_position: 2
+---
+
+Read this page to gain a high-level understanding of Vendure and concepts you will need to know to build your application.
+
+## Vendure Architecture Overview
+
+Vendure is a headless e-commerce platform. By "headless" we mean that it exposes all of its functionality via APIs. Specifically, Vendure features two GraphQL APIs: one for storefronts (Shop API) and the other for administrative functions (Admin API).
+
+These are the major parts of a Vendure application:
+
+* **Server**: The Vendure server is the part that handles requests coming in to the GraphQL APIs. It serves both the [Shop API](/reference/graphql-api/shop/queries) and [Admin API](/reference/graphql-api/admin/queries), and can send jobs to the Job Queue to be processed by the Worker.
+* **Worker**: The Worker runs in the background and deals with tasks such as updating the search index, sending emails, and other tasks which may be long-running, resource-intensive or require retries.
+* **Admin UI**: The Admin UI is how shop administrators manage orders, customers, products, settings and so on. It is not actually part of the Vendure core, but is provided as a plugin (the [AdminUiPlugin](reference/typescript-api/core-plugins/admin-ui-plugin/)) which is installed for you in a standard Vendure installation. The Admin UI can be further extended to support custom functionality, as detailed in the 
+* **Storefront**: With headless commerce, you are free to implement your storefront exactly as you see fit, unconstrained by the back-end, using any technologies that you like. To make this process easier, we have created a number of storefront starter kits, as well as [guides on building a storefront](/guides/storefront/building-a-storefront/).
+
+![./vendure_architecture.png](./vendure_architecture.png) 
+
+## Vendure's technology stack
+
+Vendure is built on the following open-source technologies:
+
+- **SQL Database**: Vendure requires an SQL database compatible with [TypeORM](https://typeorm.io/). Officially we support **PostgreSQL**, **MySQL/MariaDB** and **SQLite** but Vendure can also be used with API-compatible variants such [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [CockroachDB](https://www.cockroachlabs.com/), or [PlanetScale](https://planetscale.com/).
+- **TypeScript & Node.js**: Vendure is written in [TypeScript](https://www.typescriptlang.org/) and runs on [Node.js](https://nodejs.org).
+- **NestJS**: The underlying framework is [NestJS](https://nestjs.com/), which is a full-featured application development framework for Node.js. Building on NestJS means that Vendure from the well-defined structure and rich feature-set and ecosystem that NestJS provides. If you are unfamiliar with NestJS, see our [NestJS for Vendure Developers guide](TODO).
+- **GraphQL**: The Shop and Admin APIs use [GraphQL](https://graphql.org/), which is a modern API technology which allows you to specify the exact data that your client application needs in a convenient and type-safe way. Internally we use [Apollo Server](https://www.apollographql.com/docs/apollo-server/) to power our GraphQL APIs.
+- **Angular**: The Admin UI is built with [Angular](https://angular.io/), a popular, stable application framework from Google. Note that you do not need to know Angular to use Vendure, and UI extensions can even be written in the front-end framework of your choice, such as React or Vue.
+
+## Key Concepts
+
+- Configuration
+- Entities
+- Services
+- Plugins
+

+ 0 - 0
docs/docs/guides/developer-guide/overview/vendure_architecture.png → docs/docs/guides/getting-started/key-concepts/vendure_architecture.png


+ 639 - 0
docs/docs/guides/getting-started/writing-a-vendure-plugin.md

@@ -0,0 +1,639 @@
+---
+title: 'Writing your first plugin'
+sidebar_position: 4
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+In Vendure **plugins** are used to extend the core functionality of the server. Plugins can be pre-made functionality that you can install via npm, or they can be custom plugins that you write yourself.
+
+For any unit of functionality that you need to add to your project, you'll be writing creating a Vendure plugin. By convention, plugins are stored in the `plugins` directory of your project. However, this is not a requirement, and you are free to arrange your plugin files in any way you like.
+
+```txt
+├──src
+    ├── index.ts
+    ├── vendure-config.ts
+    ├── plugins
+        ├── reviews-plugin
+        ├── cms-plugin
+        ├── wishlist-plugin
+        ├── stock-sync-plugin
+```
+
+:::info
+For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
+
+If you intend to write a shared plugin to be distributed as an npm package, see the [vendure plugin-template repo](https://github.com/vendure-ecommerce/plugin-template)
+:::
+
+In this guide, we will implement a simple but fully-functional **wishlist plugin** step-by-step. The goal of this plugin is to allow signed-in customers to add products to a wishlist, and to view and manage their wishlist.
+
+## Step 1: Create the plugin file
+
+We'll start by creating a new directory to house our plugin, add create the main plugin file:
+
+```txt
+├──src
+    ├── index.ts
+    ├── vendure-config.ts
+    ├── plugins
+        // highlight-next-line
+        ├── wishlist-plugin
+            // highlight-next-line
+            ├── wishlist.plugin.ts
+```
+
+```ts title="src/plugins/reviews-plugin/reviews.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+})
+export class WishlistPlugin {}
+```
+
+The `PluginCommonModule` will be required in all plugins that you create. It contains the common services that are exposed by Vendure Core, allowing you to inject them into your plugin's services and resolvers.
+
+## Step 2: Define an entity
+
+Next we will define a new database entity to store the wishlist items. Vendure uses [TypeORM](https://typeorm.io/) to manage the database schema, and an Entity corresponds to a database table.
+
+First let's create the file to house the entity:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── entities
+        // highlight-next-line
+        ├── wishlist-item.entity.ts
+```
+
+By convention, we'll store the entity definitions in the `entities` directory of the plugin. Again, this is not a requirement, but it is a good way to keep your plugin organized.
+
+```ts title="src/plugins/wishlist-plugin/entities/wishlist-item.entity.ts"
+import { DeepPartial, ID, ProductVariant, VendureEntity } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+@Entity()
+export class WishlistItem extends VendureEntity {
+    constructor(input?: DeepPartial<WishlistItem>) {
+        super(input);
+    }
+
+    @ManyToOne(type => ProductVariant)
+    productVariant: ProductVariant;
+
+    @Column()
+    productVariantId: ID;
+}
+```
+
+Let's break down what's happening here:
+
+-   The `WishlistItem` entity extends the [`VendureEntity` class](/reference/typescript-api/entities/vendure-entity/). This is a base class which provides the `id`, `createdAt` and `updatedAt` fields, and all custom entities should extend it.
+-   The `@Entity()` decorator marks this class as a TypeORM entity.
+-   The `@ManyToOne()` decorator defines a many-to-one relationship with the `ProductVariant` entity. This means that each `WishlistItem` will be associated with a single `ProductVariant`.
+-   The `productVariantId` column is not strictly necessary, but it allows us to always have access to the ID of the related `ProductVariant` without having to load the entire `ProductVariant` entity from the database.
+-   The `constructor()` is used to create a new instance of the entity. This is not strictly necessary, but it is a good practice to define a constructor which takes a `DeepPartial` of the entity as an argument. This allows us to create new instances of the entity using the `new` keyword, passing in a plain object with the desired properties.
+
+Next we need to register this entity with our plugin:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [WishlistItem],
+})
+export class WishlistPlugin {}
+```
+
+## Step 3: Add a custom field to the Customer entity
+
+We'll now define a new custom field on the Customer entity which will store a list of WishlistItems. This will allow us to easily query for all wishlist items associated with a particular customer.
+
+Custom fields are defined in the VendureConfig object, and in a plugin we use the `configuration` function to modify the config object:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [WishlistItem],
+    configuration: config => {
+        config.customFields.Customer.push({
+            name: 'wishlistItems',
+            type: 'relation',
+            list: true,
+            entity: WishlistItem,
+            internal: true,
+        });
+        return config;
+    },
+})
+export class WishlistPlugin {}
+```
+
+In this snippet we are pushing a new custom field definition onto the `Customer` entity's `customFields` array, and defining this new field as a list (array) of `WishlistItem` entities. Internally, this will tell TypeORM to update the database schema to store this new field. We set `internal: true` to indicate that this field should not be directly exposed to the GraphQL API as `Customer.customFields.wishlistItems`, but instead should be accessed via a custom resolver we will define later.
+
+In order to make use of this custom field in a type-safe way, we can tell TypeScript about this field in a new file:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    // highlight-next-line
+    ├── types.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/types.ts"
+import { CustomCustomerFields } from '@vendure/core/dist/entity/custom-entity-fields';
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+  interface CustomCustomerFields {
+    wishlistItems: WishlistItem[];
+  }
+}
+```
+
+We can then import this types file in our plugin's main file:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+// highlight-next-line
+import './types';
+```
+
+## Step 4: Create a service
+
+A "service" is a class which houses the bulk of the business logic of any plugin. A plugin can define multiple services if needed, but each service should be responsible for a single unit of functionality, such as dealing with a particular entity, or performing a particular task.
+
+Let's create a service to handle the wishlist functionality:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── services
+        // highlight-next-line
+        ├── wishlist.service.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/wishlist.service.ts"
+import { Injectable } from '@nestjs/common';
+import {
+    Customer,
+    ForbiddenError,
+    ID,
+    InternalServerError,
+    ProductVariantService,
+    RequestContext,
+    TransactionalConnection,
+    UserInputError,
+} from '@vendure/core';
+
+import { WishlistItem } from '../entities/wishlist-item.entity';
+
+@Injectable()
+export class WishlistService {
+    constructor(
+        private connection: TransactionalConnection,
+        private productVariantService: ProductVariantService,
+    ) {}
+
+    async getWishlistItems(ctx: RequestContext): Promise<WishlistItem[]> {
+        try {
+            const customer = await this.getCustomerWithWishlistItems(ctx);
+            return customer.customFields.wishlistItems;
+        } catch (err: any) {
+            return [];
+        }
+    }
+
+    /**
+     * Adds a new item to the active Customer's wishlist.
+     */
+    async addItem(ctx: RequestContext, variantId: ID): Promise<WishlistItem[]> {
+        const customer = await this.getCustomerWithWishlistItems(ctx);
+        const variant = this.productVariantService.findOne(ctx, variantId);
+        if (!variant) {
+            throw new UserInputError(`No ProductVariant with the id ${variantId} could be found`);
+        }
+        const existingItem = customer.customFields.wishlistItems.find(i => i.productVariantId === variantId);
+        if (existingItem) {
+            // Item already exists in wishlist, do not
+            // add it again
+            return customer.customFields.wishlistItems;
+        }
+        const wishlistItem = await this.connection
+            .getRepository(ctx, WishlistItem)
+            .save(new WishlistItem({ productVariantId: variantId }));
+        customer.customFields.wishlistItems.push(wishlistItem);
+        await this.connection.getRepository(ctx, Customer).save(customer, { reload: false });
+        return this.getWishlistItems(ctx);
+    }
+
+    /**
+     * Removes an item from the active Customer's wishlist.
+     */
+    async removeItem(ctx: RequestContext, itemId: ID): Promise<WishlistItem[]> {
+        const customer = await this.getCustomerWithWishlistItems(ctx);
+        const itemToRemove = customer.customFields.wishlistItems.find(i => i.id === itemId);
+        if (itemToRemove) {
+            await this.connection.getRepository(ctx, WishlistItem).remove(itemToRemove);
+            customer.customFields.wishlistItems = customer.customFields.wishlistItems.filter(
+                i => i.id !== itemId,
+            );
+        }
+        await this.connection.getRepository(ctx, Customer).save(customer);
+        return this.getWishlistItems(ctx);
+    }
+
+    /**
+     * Gets the active Customer from the context and loads the wishlist items.
+     */
+    private async getCustomerWithWishlistItems(ctx: RequestContext): Promise<Customer> {
+        if (!ctx.activeUserId) {
+            throw new ForbiddenError();
+        }
+        const customer = await this.connection.getRepository(ctx, Customer).findOne({
+            where: { user: { id: ctx.activeUserId } },
+            relations: {
+                customFields: {
+                    wishlistItems: {
+                        productVariant: true,
+                    },
+                },
+            },
+        });
+        if (!customer) {
+            throw new InternalServerError(`Customer was not found`);
+        }
+        return customer;
+    }
+}
+```
+
+Let's break down what's happening here:
+
+- The `WishlistService` class is decorated with the `@Injectable()` decorator. This is a standard NestJS decorator which tells the NestJS dependency injection (DI) system that this class can be injected into other classes. All your services should be decorated with this decorator.
+- The arguments passed to the constructor will be injected by the NestJS DI system. The `connection` argument is a [TransactionalConnection](/reference/typescript-api/data-access/transactional-connection/) instance, which is used to access and manipulate data in the database. The [`ProductVariantService`](/reference/typescript-api/services/product-variant-service/) argument is a built-in Vendure service which contains methods relating to ProductVariants.
+- The [`RequestContext`](/reference/typescript-api/request/request-context/) object is usually the first argument to any service method, and contains information and context about the current request as well as any open database transactions. It should always be passed to the methods of the `TransactionalConnection`.
+
+The service is then registered with the plugin metadata as a provider:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { WishlistService } from './services/wishlist.service';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    // highlight-next-line
+    providers: [WishlistService],
+    entities: [WishlistItem],
+    configuration: config => {
+        // ...
+    },
+})
+export class WishlistPlugin {}
+```
+
+## Step 5: Extend the GraphQL API
+
+This plugin will need to extend the Shop API, adding new mutations and queries to enable the customer to view and manage their wishlist.
+
+First we will create a new file to hold the GraphQL schema extensions:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── api
+        // highlight-next-line
+        ├── api-extensions.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+    type WishlistItem implements Node {
+        id: ID!
+        createdAt: DateTime!
+        updatedAt: DateTime!
+        productVariant: ProductVariant!
+        productVariantId: ID!
+    }
+
+    extend type Query {
+        activeCustomerWishlist: [WishlistItem!]!
+    }
+
+    extend type Mutation {
+        addToWishlist(productVariantId: ID!): [WishlistItem!]!
+        removeFromWishlist(itemId: ID!): [WishlistItem!]!
+    }
+`;
+```
+
+:::note
+
+The `graphql-tag` package is a dependency of the Vendure core package. Depending on the package manager you are using, you may need to install it separately with `yarn add graphql-tag` or `npm install graphql-tag`.
+
+:::
+
+The `api-extensions.ts` file is where we define the extensions we will be making to the Shop API GraphQL schema. We are defining a new `WishlistItem` type; a new query: `activeCustomerWishlist`; and two new mutations: `addToWishlist` and `removeFromWishlist`. This definition is written in [schema definition language](https://graphql.org/learn/schema/) (SDL), a convenient syntax for defining GraphQL schemas.
+
+Next we need to pass these extensions to our plugin's metadata:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { shopApiExtensions } from './api/api-extensions';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [],
+    },
+})
+export class WishlistPlugin {}
+```
+
+## Step 6: Create a resolver
+
+Now that we have defined the GraphQL schema extensions, we need to create a resolver to handle the new queries and mutations. A resolver in GraphQL is a function which actually implements the query or mutation defined in the schema. This is done by creating a new file in the `api` directory:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── api
+        ├── api-extensions.ts
+        // highlight-next-line
+        ├── wishlist.resolver.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/api/wishlist.resolver.ts"
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, Permission, RequestContext, Transaction } from '@vendure/core';
+
+import { WishlistItem } from '../entities/wishlist-item.entity';
+import { WishlistService } from '../service/wishlist.service';
+
+@Resolver()
+export class WishlistShopResolver {
+    constructor(private wishlistService: WishlistService) {}
+
+    @Query()
+    @Allow(Permission.Owner)
+    activeCustomerWishlist(@Ctx() ctx: RequestContext) {
+        return this.wishlistService.getWishlistItems(ctx);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Owner)
+    async addToWishlist(
+        @Ctx() ctx: RequestContext,
+        @Args() { productVariantId }: { productVariantId: string },
+    ) {
+        return this.wishlistService.addItem(ctx, productVariantId);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Owner)
+    async removeFromWishlist(@Ctx() ctx: RequestContext, @Args() { itemId }: { itemId: string }) {
+        return this.wishlistService.removeItem(ctx, itemId);
+    }
+}
+```
+
+Resolvers are usually "thin" functions that delegate the actual work to a service. Vendure, like NestJS itself, makes heavy use of decorators at the API layer to define various aspects of the resolver. Let's break down what's happening here:
+
+- The `@Resolver()` decorator tells the NestJS DI system that this class is a resolver. Since a Resolver is part of the NestJS DI system, we can also inject dependencies into its constructor. In this case we are injecting the `WishlistService` which we created in the previous step.
+- The `@Mutation()` decorator tells Vendure that this is a mutation resolver. Similarly, `@Query()` decorator defines a query resolver. The name of the method is the name of the query or mutation in the schema.
+- The `@Transaction()` decorator tells Vendure that this resolver method should be wrapped in a database transaction. This is important because we are performing multiple database operations in this method, and we want them to be atomic.
+- The `@Allow()` decorator tells Vendure that this mutation is only allowed for users with the `Owner` permission. The `Owner` permission is a special permission which indicates that the active user should be the owner of this operation. 
+- The `@Ctx()` decorator tells Vendure that this method requires access to the `RequestContext` object. Every resolver should have this as the first argument, as it is required throughout the Vendure request lifecycle.
+
+This resolver is then registered with the plugin metadata:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { shopApiExtensions } from './api/api-extensions';
+import { WishlistShopResolver } from './api/wishlist.resolver';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        // highlight-next-line
+        resolvers: [WishlistShopResolver],
+    },
+    configuration: config => {
+        // ...
+    },
+})
+export class WishlistPlugin {}
+```
+
+:::info
+
+More information about resolvers can be found in the [NestJS docs](https://docs.nestjs.com/graphql/resolvers).
+
+:::
+
+## Step 7: Specify 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.
+
+The compatibility is specified via the `compatibility` property in the plugin metadata:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+@VendurePlugin({
+    // ...
+    // highlight-next-line
+    compatibility: '^2.0.0',
+})
+export class WishlistPlugin {}
+```
+
+The value of this property is a [semver range](https://docs.npmjs.com/about-semantic-versioning) which specifies the range of compatible versions. In this case, we are saying that this plugin is compatible with any version of Vendure core which is `>= 2.0.0 < 3.0.0`.
+
+## Step 8: Add the plugin to the VendureConfig
+
+The final step is to add the plugin to the `VendureConfig` object. This is done in the `vendure-config.ts` file:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { WishlistPlugin } from './plugins/wishlist-plugin/wishlist.plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        // ...
+        // highlight-next-line
+        WishlistPlugin,
+    ],
+};
+```
+
+## Test the plugin
+
+Now that the plugin is installed, we can test it out. Since we have defined a custom field, we'll need to generate and run a migration to add the new column to the database:
+
+```bash
+npm run migration:generate wishlist-plugin
+```
+
+Then start the server:
+
+```bash
+npm run dev
+```
+
+Once the server is running, we should be able to log in as an existing Customer, and then add a product to the wishlist:
+
+<Tabs>
+<TabItem value="Login mutation" label="Login mutation" default>
+
+```graphql
+mutation Login {
+    login(username: "alec.breitenberg@gmail.com", password: "test") {
+        ... on CurrentUser {
+            id
+            identifier
+        }
+        ... on ErrorResult {
+            errorCode
+            message
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "login": {
+      "id": "9",
+      "identifier": "alec.breitenberg@gmail.com"
+    }
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
+
+
+<Tabs>
+<TabItem value="AddToWishlist mutation" label="AddToWishlist mutation" default>
+
+```graphql
+mutation AddToWishlist {
+    addToWishlist(productVariantId: "7") {
+        id
+        productVariant {
+            id
+            name
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "addToWishlist": [
+      {
+        "id": "4",
+        "productVariant": {
+          "id": "7",
+          "name": "Wireless Optical Mouse"
+        }
+      }
+    ]
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
+
+We can then query the wishlist items:
+
+
+<Tabs>
+<TabItem value="GetWishlist mutation" label="GetWishlist mutation" default>
+
+```graphql
+query GetWishlist {
+    activeCustomerWishlist {
+        id
+        productVariant {
+            id
+            name
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "activeCustomerWishlist": [
+      {
+        "id": "4",
+        "productVariant": {
+          "id": "7",
+          "name": "Wireless Optical Mouse"
+        }
+      }
+    ]
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
+
+And finally, we can test removing an item from the wishlist:
+
+<Tabs>
+<TabItem value="RemoveFromWishlist mutation" label="RemoveFromWishlist mutation" default>
+
+```graphql
+mutation RemoveFromWishlist {
+    removeFromWishlist(itemId: "4") {
+        id
+        productVariant {
+            name
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "removeFromWishlist": []
+  }
+}
+```
+
+  </TabItem>
+</Tabs>

+ 0 - 300
docs/docs/guides/plugins/writing-a-vendure-plugin.md

@@ -1,300 +0,0 @@
----
-title: "Writing a Vendure Plugin"
-weight: 2
----
-
-# Writing a Vendure Plugin
-
-This is a complete example of how to implement a simple plugin step-by-step.
-
-{{< alert "primary" >}}
-  For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
-  
-  If you intend to write a shared plugin to be distributed as an npm package, see the [vendure plugin-template repo](https://github.com/vendure-ecommerce/plugin-template)
-{{< /alert >}}
-
-## Example: RandomCatPlugin
-
-Let's learn about Vendure plugins by writing a plugin that defines a new database entity and a GraphQL mutation.
-
-This plugin will add a new mutation, `addRandomCat`, to the GraphQL API which allows us to conveniently link a random cat image from [http://random.cat](http://random.cat) to any product in our catalog.
-
-### Step 1: Define a new custom field
-
-We need a place to store the url of the cat image, so we will add a custom field to the Product entity. This is done by modifying the VendureConfig object via the plugin's [`configuration` metadata property]({{< relref "vendure-plugin-metadata" >}}#configuration):
-
-```ts
-import { VendurePlugin } from '@vendure/core';
-
-@VendurePlugin({
-  configuration: config => {
-    config.customFields.Product.push({
-      type: 'string',
-      name: 'catImageUrl',
-    });
-    return config;
-  }
-})
-export class RandomCatPlugin {}
-```
-
-Changing the database schema by `CustomFields` configuration requires a synchronized database. See [Customizing Models With Custom Fields]({{< relref "customizing-models" >}}) for more details.
-
-### Step 2: Create a service to fetch the data
-
-Now we will create a service which is responsible for making the HTTP call to the random.cat API and returning the URL of a random cat image:
-
-```ts
-import http from 'http';
-import { Injectable } from '@nestjs/common';
-
-@Injectable()
-export class CatFetcher {
-  /** Fetch a random cat image url from random.cat */
-  fetchCat(): Promise<string> {
-    return new Promise((resolve) => {
-      http.get('http://aws.random.cat/meow', (resp) => {
-        let data = '';
-        resp.on('data', chunk => data += chunk);
-        resp.on('end', () => resolve(JSON.parse(data).file));
-      });
-    });
-  }
-}
-```
-
-{{% alert %}}
-The `@Injectable()` decorator is part of the underlying [Nest framework](https://nestjs.com/), and allows us to make use of Nest's powerful dependency injection features. In this case, we'll be able to inject the `CatFetcher` service into the resolver which we will soon create.
-{{< /alert >}}
-
-
-{{< alert "warning" >}}
-To use decorators with TypeScript, you must set the "emitDecoratorMetadata" and "experimentalDecorators" compiler options to `true` in your tsconfig.json file.
-{{< /alert >}}
-
-### Step 3: Define the new mutation
-
-Next, we will define how the GraphQL API should be extended:
-
-```ts
-import gql from 'graphql-tag';
-
-const schemaExtension = gql`
-  extend type Mutation {
-    addRandomCat(id: ID!): Product!
-  }
-`;
-```
-
-We will use this `schemaExtension` variable in a later step.
-
-### Step 4: Create a resolver
-
-Now that we've defined the new mutation, we'll need a resolver function to handle it. To do this, we'll create a new resolver class, following the [Nest GraphQL resolver architecture](https://docs.nestjs.com/graphql/resolvers-map). In short, this will be a class which has the `@Resolver()` decorator and features a method to handle our new mutation.
-
-```ts
-import { Args, Mutation, Resolver } from '@nestjs/graphql';
-import { Ctx, Allow, ProductService, RequestContext, Transaction } from '@vendure/core';
-import { Permission } from '@vendure/common/lib/generated-types';
-
-@Resolver()
-export class RandomCatResolver {
-
-  constructor(private productService: ProductService, private catFetcher: CatFetcher) {}
-
-  @Transaction()
-  @Mutation()
-  @Allow(Permission.UpdateCatalog)
-  async addRandomCat(@Ctx() ctx: RequestContext, @Args() args) {
-    const catImageUrl = await this.catFetcher.fetchCat();
-    return this.productService.update(ctx, {
-      id: args.id,
-      customFields: { catImageUrl },
-    });
-  }
-}
-```
-
-Some explanations of this code are in order:
-
-* The `@Resolver()` decorator tells Nest that this class contains GraphQL resolvers.
-* We are able to use Nest's dependency injection to inject an instance of our `CatFetcher` class into the constructor of the resolver. We are also injecting an instance of the built-in [ProductService class]({{< relref "product-service" >}}), which is responsible for operations on Products.
-* We use the `@Transaction()` decorator to ensure that all database operations in this resolver are run within a transaction. This ensures that if any part of it fails, all changes will be rolled back, keeping our data in a consistent state. For more on this, see the [Transaction Decorator docs]({{< relref "transaction-decorator" >}}).
-* We use the `@Mutation()` decorator to mark this method as a resolver for the GraphQL mutation with the corresponding name.
-* The `@Allow()` decorator enables us to define permissions restrictions on the mutation. Only those users whose permissions include `UpdateCatalog` may perform this operation. For a full list of available permissions, see the [Permission enum]({{< relref "/reference/graphql-api/admin/enums" >}}#permission). Plugins may also define custom permissions, see [Defining custom permissions]({{< relref "defining-custom-permissions" >}}).
-* The `@Ctx()` decorator injects the current [RequestContext]({{< relref "request-context" >}}) into the resolver. This provides information about the current request such as the current Session, User and Channel. It is required by most of the internal service methods.
-* The `@Args()` decorator injects the arguments passed to the mutation as an object.
-
-### Step 5: Import the PluginCommonModule
-
-In the RandomCatResolver we make use of the ProductService. This is one of the built-in services which Vendure uses internally to interact with the database. It can be injected into our resolver only if we first import the [PluginCommonModule]({{< relref "plugin-common-module" >}}), which is a module which exports a number of providers which are usually needed by plugins.
-
-```ts
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  configuration: config => {
-    // omitted
-  }
-})
-export class RandomCatPlugin {}
-``` 
-
-### Step 6: Declare any providers used in the resolver
-
-In order that our resolver is able to use Nest's dependency injection to inject an instance of `CatFetcher`, we must add it to the `providers` array in our plugin:
-
-```ts
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  providers: [CatFetcher],
-  configuration: config => {
-    // omitted
-  }
-})
-export class RandomCatPlugin {}
-```
-
-### Step 7: Extend the GraphQL API
-
-Now that we've defined the new mutation and we have a resolver capable of handling it, we just need to tell Vendure to extend the API. This is done with the [`adminApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#adminapiextensions). If we wanted to extend the Shop API, we'd use the [`shopApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#shopapiextensions) instead.
-
-```ts
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  providers: [CatFetcher],
-  adminApiExtensions: {
-    schema: schemaExtension,
-    resolvers: [RandomCatResolver],
-  },
-  configuration: config => {
-    // omitted
-  }
-})
-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.
-
-```ts
-@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:
-
-```ts
-import { bootstrap } from '@vendure/core';
-
-bootstrap({
-  // .. config options
-  plugins: [
-      RandomCatPlugin,
-  ],
-});
-```
-
-### Step 9: Test the plugin
-
-Once we have started the Vendure server with the new config, we should be able to send the following GraphQL query to the Admin API:
-
-```graphql
-mutation {
-  addRandomCat(id: "1") {
-    id
-    name
-    customFields {
-      catImageUrl
-    }
-  }
-}
-```
-
-which should yield the following response:
-
-```JSON
-{
-  "data": {
-    "addRandomCat": {
-      "id": "1",
-      "name": "Spiky Cactus",
-      "customFields": {
-        "catImageUrl": "https://purr.objects-us-east-1.dream.io/i/OoNx6.jpg"
-      }
-    }
-  }
-}
-```
-
-### Full example plugin
-
-```ts
-import { Injectable } from '@nestjs/common';
-import { Args, Mutation, Resolver } from '@nestjs/graphql';
-import gql from 'graphql-tag';
-import http from 'http';
-import { Allow, Ctx, PluginCommonModule, ProductService, 
-    RequestContext, VendureConfig, VendurePlugin } from '@vendure/core';
-import { Permission } from '@vendure/common/lib/generated-types';
-
-const schemaExtension = gql`
-    extend type Mutation {
-        addRandomCat(id: ID!): Product!
-    }
-`;
-
-@Injectable()
-export class CatFetcher {
-  /** Fetch a random cat image url from random.cat */
-  fetchCat(): Promise<string> {
-    return new Promise((resolve) => {
-      http.get('http://aws.random.cat/meow', (resp) => {
-        let data = '';
-        resp.on('data', chunk => data += chunk);
-        resp.on('end', () => resolve(JSON.parse(data).file));
-      });
-    });
-  }
-}
-
-@Resolver()
-export class RandomCatResolver {
-  constructor(private productService: ProductService, 
-              private catFetcher: CatFetcher) {}
-
-  @Mutation()
-  @Allow(Permission.UpdateCatalog)
-  async addRandomCat(@Ctx() ctx: RequestContext, @Args() args) {
-    const catImageUrl = await this.catFetcher.fetchCat();
-    return this.productService.update(ctx, {
-      id: args.id,
-      customFields: { catImageUrl },
-    });
-  }
-}
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  providers: [CatFetcher],
-  adminApiExtensions: {
-    schema: schemaExtension,
-    resolvers: [RandomCatResolver],
-  },
-  configuration: config => {
-    config.customFields.Product.push({
-      type: 'string',
-      name: 'catImageUrl',
-    });
-    return config;
-  },
-  compatibility: '^2.0.0',
-})
-export class RandomCatPlugin {}
-```

+ 5 - 0
docs/docusaurus.config.js

@@ -85,6 +85,11 @@ const config = {
                     },
                 ],
             },
+            docs: {
+                sidebar: {
+                    autoCollapseCategories: true,
+                },
+            },
             footer: {
                 style: 'dark',
                 links: [

+ 4 - 0
docs/scraper/.env.example

@@ -0,0 +1,4 @@
+TYPESENSE_API_KEY=123456
+TYPESENSE_HOST=localhost
+TYPESENSE_PORT=8108
+TYPESENSE_PROTOCOL=http

+ 32 - 0
docs/scraper/readme.md

@@ -0,0 +1,32 @@
+# Scraper
+
+This directory contains the config to run the Typesense Docsearch scraper.
+
+First you need to copy the `.env.example` file and fill in the values for your Typesense instance.
+
+```bash
+
+### Local usage
+
+To populate a locally-running Typesense instance, change the `config.json` file to point to the locally-running docs site:
+
+```json
+{
+  "start_urls": [
+    "http://host.docker.internal:3000/"
+  ],
+  "sitemap_urls": [
+    "http://host.docker.internal:3000/sitemap.xml"
+  ],
+  "allowed_domains": [
+    "host.docker.internal"
+  ]
+}
+```
+
+Then run:
+
+```bash
+docker run -it --env-file=.env -e "CONFIG=$(cat config.json | jq -r tostring)" typesense/docsearch-scraper:0.7.0
+```
+

+ 32 - 13
docs/sidebars.js

@@ -14,7 +14,38 @@
 /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
 const sidebars = {
     // By default, Docusaurus generates a sidebar from the docs folder structure
-    learnSidebar: [{ type: 'autogenerated', dirName: 'guides' }],
+    learnSidebar: [
+        {
+            type: 'category',
+            label: 'Getting Started',
+            items: [{ type: 'autogenerated', dirName: 'guides/getting-started' }],
+        },
+        {
+            type: 'category',
+            label: 'How-to Guides',
+            items: [{ type: 'autogenerated', dirName: 'guides/how-to' }],
+        },
+        {
+            type: 'category',
+            label: 'Extending the Admin UI',
+            items: [{ type: 'autogenerated', dirName: 'guides/extending-the-admin-ui' }],
+        },
+        {
+            type: 'category',
+            label: 'Building a Storefront',
+            items: [{ type: 'autogenerated', dirName: 'guides/storefront' }],
+        },
+        {
+            type: 'category',
+            label: 'Advanced Topics',
+            items: [{ type: 'autogenerated', dirName: 'guides/advanced-topics' }],
+        },
+        {
+            type: 'category',
+            label: 'Deployment',
+            items: [{ type: 'autogenerated', dirName: 'guides/deployment' }],
+        },
+    ],
     referenceSidebar: [
         {
             type: 'category',
@@ -43,18 +74,6 @@ const sidebars = {
             items: [{ type: 'autogenerated', dirName: 'reference/admin-ui-api' }],
         },
     ],
-    // But you can create a sidebar manually
-    /*
-  tutorialSidebar: [
-    'intro',
-    'hello',
-    {
-      type: 'category',
-      label: 'Tutorial',
-      items: ['tutorial-basics/create-a-document'],
-    },
-  ],
-   */
 };
 
 module.exports = sidebars;

+ 2 - 2
docs/src/css/custom.css

@@ -33,8 +33,8 @@
     --color-graphql-identifier: rgb(255, 121, 198);
 }
 
-.markdown {
-    --ifm-h2-vertical-rhythm-top: 6;
+.markdown h2:not(:first-of-type) {
+    --ifm-h2-vertical-rhythm-top: 5;
 }
 
 

+ 3 - 0
packages/dev-server/example-plugins/wishlist-plugin/README.md

@@ -0,0 +1,3 @@
+# Example Wishlist plugin
+
+This is the complete plugin described in the [Writing your first plugin tutorial](https://docs.vendure.io/guides/getting-started/writing-a-vendure-plugin) in the Vendure docs.

+ 20 - 0
packages/dev-server/example-plugins/wishlist-plugin/api/api-extensions.ts

@@ -0,0 +1,20 @@
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+    type WishlistItem implements Node {
+        id: ID!
+        createdAt: DateTime!
+        updatedAt: DateTime!
+        productVariant: ProductVariant!
+        productVariantId: ID!
+    }
+
+    extend type Query {
+        activeCustomerWishlist: [WishlistItem!]!
+    }
+
+    extend type Mutation {
+        addToWishlist(productVariantId: ID!): [WishlistItem!]!
+        removeFromWishlist(itemId: ID!): [WishlistItem!]!
+    }
+`;

+ 33 - 0
packages/dev-server/example-plugins/wishlist-plugin/api/wishlist.resolver.ts

@@ -0,0 +1,33 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, Permission, RequestContext, Transaction } from '@vendure/core';
+
+import { WishlistItem } from '../entities/wishlist-item.entity';
+import { WishlistService } from '../service/wishlist.service';
+
+@Resolver()
+export class WishlistShopResolver {
+    constructor(private wishlistService: WishlistService) {}
+
+    @Query()
+    @Allow(Permission.Owner)
+    activeCustomerWishlist(@Ctx() ctx: RequestContext) {
+        return this.wishlistService.getWishlistItems(ctx);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Owner)
+    async addToWishlist(
+        @Ctx() ctx: RequestContext,
+        @Args() { productVariantId }: { productVariantId: string },
+    ) {
+        return this.wishlistService.addItem(ctx, productVariantId);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Owner)
+    async removeFromWishlist(@Ctx() ctx: RequestContext, @Args() { itemId }: { itemId: string }) {
+        return this.wishlistService.removeItem(ctx, itemId);
+    }
+}

+ 15 - 0
packages/dev-server/example-plugins/wishlist-plugin/entities/wishlist-item.entity.ts

@@ -0,0 +1,15 @@
+import { DeepPartial, ID, ProductVariant, VendureEntity } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+@Entity()
+export class WishlistItem extends VendureEntity {
+    constructor(input?: DeepPartial<WishlistItem>) {
+        super(input);
+    }
+
+    @ManyToOne(type => ProductVariant)
+    productVariant: ProductVariant;
+
+    @Column()
+    productVariantId: ID;
+}

+ 92 - 0
packages/dev-server/example-plugins/wishlist-plugin/service/wishlist.service.ts

@@ -0,0 +1,92 @@
+import { Injectable } from '@nestjs/common';
+import {
+    Customer,
+    ForbiddenError,
+    ID,
+    InternalServerError,
+    ProductVariantService,
+    RequestContext,
+    TransactionalConnection,
+    UserInputError,
+} from '@vendure/core';
+
+import { WishlistItem } from '../entities/wishlist-item.entity';
+
+@Injectable()
+export class WishlistService {
+    constructor(
+        private connection: TransactionalConnection,
+        private productVariantService: ProductVariantService,
+    ) {}
+
+    async getWishlistItems(ctx: RequestContext): Promise<WishlistItem[]> {
+        try {
+            const customer = await this.getCustomerWithWishlistItems(ctx);
+            return customer.customFields.wishlistItems;
+        } catch (err: any) {
+            return [];
+        }
+    }
+
+    /**
+     * Adds a new item to the active Customer's wishlist.
+     */
+    async addItem(ctx: RequestContext, variantId: ID): Promise<WishlistItem[]> {
+        const customer = await this.getCustomerWithWishlistItems(ctx);
+        const variant = this.productVariantService.findOne(ctx, variantId);
+        if (!variant) {
+            throw new UserInputError(`No ProductVariant with the id ${variantId} could be found`);
+        }
+        const existingItem = customer.customFields.wishlistItems.find(i => i.productVariantId === variantId);
+        if (existingItem) {
+            // Item already exists in wishlist, do not
+            // add it again
+            return customer.customFields.wishlistItems;
+        }
+        const wishlistItem = await this.connection
+            .getRepository(ctx, WishlistItem)
+            .save(new WishlistItem({ productVariantId: variantId }));
+        customer.customFields.wishlistItems.push(wishlistItem);
+        await this.connection.getRepository(ctx, Customer).save(customer, { reload: false });
+        return this.getWishlistItems(ctx);
+    }
+
+    /**
+     * Removes an item from the active Customer's wishlist.
+     */
+    async removeItem(ctx: RequestContext, itemId: ID): Promise<WishlistItem[]> {
+        const customer = await this.getCustomerWithWishlistItems(ctx);
+        const itemToRemove = customer.customFields.wishlistItems.find(i => i.id === itemId);
+        if (itemToRemove) {
+            await this.connection.getRepository(ctx, WishlistItem).remove(itemToRemove);
+            customer.customFields.wishlistItems = customer.customFields.wishlistItems.filter(
+                i => i.id !== itemId,
+            );
+        }
+        await this.connection.getRepository(ctx, Customer).save(customer);
+        return this.getWishlistItems(ctx);
+    }
+
+    /**
+     * Gets the active Customer from the context and loads the wishlist items.
+     */
+    private async getCustomerWithWishlistItems(ctx: RequestContext): Promise<Customer> {
+        if (!ctx.activeUserId) {
+            throw new ForbiddenError();
+        }
+        const customer = await this.connection.getRepository(ctx, Customer).findOne({
+            where: { user: { id: ctx.activeUserId } },
+            relations: {
+                customFields: {
+                    wishlistItems: {
+                        productVariant: true,
+                    },
+                },
+            },
+        });
+        if (!customer) {
+            throw new InternalServerError(`Customer was not found`);
+        }
+        return customer;
+    }
+}

+ 9 - 0
packages/dev-server/example-plugins/wishlist-plugin/types.ts

@@ -0,0 +1,9 @@
+import { CustomCustomerFields } from '@vendure/core/dist/entity/custom-entity-fields';
+
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+  interface CustomCustomerFields {
+    wishlistItems: WishlistItem[];
+  }
+}

+ 28 - 0
packages/dev-server/example-plugins/wishlist-plugin/wishlist.plugin.ts

@@ -0,0 +1,28 @@
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { shopApiExtensions } from './api/api-extensions';
+import { WishlistShopResolver } from './api/wishlist.resolver';
+import { WishlistItem } from './entities/wishlist-item.entity';
+import { WishlistService } from './service/wishlist.service';
+import './types';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [WishlistItem],
+    providers: [WishlistService],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [WishlistShopResolver],
+    },
+    configuration: config => {
+        config.customFields.Customer.push({
+            name: 'wishlistItems',
+            type: 'relation',
+            list: true,
+            entity: WishlistItem,
+            internal: true,
+        });
+        return config;
+    },
+})
+export class WishlistPlugin {}