diff --git a/.eslintignore b/.eslintignore index 885f0c60e6..ab184126a0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,5 @@ **/GraphQL.ts !**/.vuepress/**/* packages/cache/nuxt/plugin.js +packages/nuxt-module/plugins/i18n-cookies.js +packages/nuxt-module/plugins/logger.js diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 7fa6f78abc..1b1ee36164 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -25,9 +25,7 @@ jobs: id: get_version run: echo ::set-output name=VERSION::$(cat version.txt) - name: Build and publish docker image - # uses: elgohr/Publish-Docker-Github-Action@master - # 3.04 is hardcoded as a workaround for https://github.com/elgohr/Publish-Docker-Github-Action/issues/134 - uses: elgohr/Publish-Docker-Github-Action@3.04 + uses: elgohr/Publish-Docker-Github-Action@master with: name: docs-storefrontcloud-io/v2:${{ steps.get_version.outputs.VERSION }} registry: registry.storefrontcloud.io diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index a904b6e0ce..450131561f 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -6,13 +6,15 @@ # https://github.com/actions/labeler name: Labeler -on: [pull_request] +on: +- pull_request_target jobs: label: - + permissions: + contents: read + pull-requests: write runs-on: ubuntu-latest - steps: - uses: actions/labeler@master with: diff --git a/README.md b/README.md index c1e69f6d80..95155eae55 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,158 @@ -
- Vue Storefront -
- -# Vue Storefront 2 - -[![Coverage Status](https://coveralls.io/repos/github/vuestorefront/vue-storefront/badge.svg?branch=next) ](https://coveralls.io/github/vuestorefront/vue-storefront/?branch=next) -[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) -[![Discord](https://img.shields.io/discord/770285988244750366?label=join%20discord&logo=Discord&logoColor=white)](https://discord.vuestorefront.io) - -Vue Storefront is the most popular and most advanced Frontend Platform for eCommerce -- [Documentation](https://docs.vuestorefront.io/v2/) -- [Demo](https://demo-ct.vuestorefront.io) -- [Installation](https://docs.vuestorefront.io/v2/general/installation.html) +
+

+ Vue Storefront +

+ +

+ Lightning-Fast Frontend Platform for Headless Commerce +

+

+ Vue Storefront is the most popular and most advanced Frontend Platform for eCommerce. +

+ +

+ + GitHub Repository Stars Count + + + Follow Us on Twitter + + + Subscribe on our Youtube Channel + + + Chat with us on Discord + +

+

+ + Commitizen friendly + + + License + + + PRs Welcome + + + Coverage Status + +

+ +> # #TechForUkraine +>
+>

+> +>

+>

Russiaโ€™s military aggression against Ukraine.

+>
+>

How can you support Ukrainian civil society?

+> All help is needed. If you are not able to help locally, by sheltering a fellow Ukrainian, you can also: +>
+>
![Screenshots](https://blog.vuestorefront.io/wp-content/uploads/2020/03/3-views-Vue-Storefront-.png) # Supported platforms -- [commercetools](https://github.com/vuestorefront/commercetools) -- [Shopify](https://github.com/vuestorefront/shopify) -- [Magento 2](https://github.com/vuestorefront/magento2) [Beta] -- [Salesforce Commerce Cloud](https://github.com/vuestorefront/salesforce-commerce-cloud) [Beta] -- [Spryker](https://github.com/vuestorefront/spryker) [Beta] -- [Vendure](https://github.com/vuestorefront/vendure) [Beta] -- [Odoo](https://github.com/vuestorefront/odoo) [Beta] -- [Spree](https://github.com/vuestorefront/spree) [Beta] -[Learn more about available integrations](https://docs.vuestorefront.io/v2/integrations/) +

+ + commercetools + +    + + Shopware + +    + + Shopify + +    + + Magento + +    + + Salesforce Commerce Cloud + +    + + Spree + +
+    + + BigCommerce + +    + + Spryker + +    + + Vendure + +    + + Odoo + +    + + Prestashop + +    + + nopCommerce + +
+    + + kiboCommerce + +    + + Sylius + +    + Swell +    + + WooCommerce + +

+ Learn more about available integrations +

+ ## Links -- ๐Ÿ“˜ Documentation: [docs.vuestorefront.io](https://docs.vuestorefront.io/v2/) -- ๐Ÿ‘ฅ Discord Community: [discord.vuestorefront.io](https://discord.vuestorefront.io/) -- ๐Ÿฆ Twitter: [@VueStorefront](https://twitter.com/VueStorefront) -- ๐Ÿ’ฌ Forum: [forum.vuestorefront.io](https://forum.vuestorefront.io/) -- ๐ŸŒŸ [Live Projects List](https://www.vuestorefront.io/live-projects/?utm_source=github.com&utm_medium=referral&utm_campaign=readme) +- ๐Ÿ–ฅ Demo: https://demo.vuestorefront.io/ +- ๐Ÿ“ฆ Installation: https://docs.vuestorefront.io/v2/getting-started/installation.html +- ๐Ÿ“˜ Documentation: https://docs.vuestorefront.io/v2/ +- ๐Ÿ‘ฅ Discord Community: https://discord.gg/vuestorefront/ +- ๐Ÿฆ Twitter: https://twitter.com/VueStorefront +- ๐ŸŽฅ YouTube: https://www.youtube.com/c/VueStorefront +- ๐ŸŒŸ [Over 1000+ Live Stores, check the list!](https://www.vuestorefront.io/live-projects/?utm_source=github.com&utm_medium=referral&utm_campaign=readme) ## The business challenges @@ -49,15 +167,15 @@ Vue Storefront solves a set of key business challenges from the world of the sho ## The headless architecture -![Vue Storefront - Headless Architecture](https://user-images.githubusercontent.com/1626923/137323687-c63cd6fa-a018-4491-bea7-1802649499ca.jpg) +![Vue Storefront - Headless Architecture](https://user-images.githubusercontent.com/1626923/156937729-bab22505-89f5-488b-9dd1-d2d7c7ad9600.jpg) ## Contributing -If you like the ideas behind Vue Storefront and want to become a contributor - join our [Discord server](https://discord.vuestorefront.io), check the [list of the active issues](https://github.com/vuestorefront/vue-storefront/issues) or contact us directly via contributors@vuestorefront.io. +If you like the ideas behind Vue Storefront and want to become a contributor - join our [Discord server](https://discord.vuestorefront.io), check the [list of the active issues](https://github.com/vuestorefront/vue-storefront/issues) or contact us directly via contributors(at)vuestorefront(dot)io. -If you have discovered a ๐Ÿœ or have feature suggestion, feel free to create an issue on Github. +If you have discovered a ๐Ÿœ or have feature suggestion, feel free to [create an issue](https://github.com/vuestorefront/vue-storefront/issues/new/choose) on Github. -## Support us! +## Support us **Vue Storefront is and always will be Open Source, released under MIT Licence.** @@ -68,7 +186,7 @@ You can support us in various ways: ## Partners -Vue Storefront is a Community effort brought to You by our great Core Team and supported by the following companies. +Vue Storefront is a Community effort brought to You by our great Core Team and supported by the following companies. [**See Vue Storefront partners directory**](https://www.vuestorefront.io/partner-agencies?utm_source=github.com&utm_medium=referral&utm_campaign=readme) diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 0000000000..4c6a9b8da2 --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + target-branch: "develop" + labels: + - "dependencies" diff --git a/package.json b/package.json index 864e87d7a7..f2d6e77cec 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,12 @@ "name": "root", "private": true, "scripts": { - "build": "yarn build:core && yarn build:cache && yarn build:middleware", + "build": "yarn build:core && yarn build:cache && yarn build:middleware && yarn build:nuxt-module", "build:core": "cd packages/core && yarn build", "build:cache": "cd packages/cache && yarn build", "build:docs": "cd packages/docs && yarn build", "build:middleware": "cd packages/middleware && yarn build", + "build:nuxt-module": "cd packages/nuxt-module && yarn build", "cli": "cd packages/cli && yarn cli", "commit": "cz", "core:changelog": "cd packages/docs/scripts && node changelog --in ../changelog --out ../reference/changelog.md", @@ -14,6 +15,7 @@ "link-packages": "lerna link --force-local", "lint": "eslint . --ext .js,.ts,.vue", "test:cache": "cd packages/cache && yarn test", + "test:http-cache": "cd packages/http-cache && yarn test", "test:cli": "cd packages/cli && yarn test", "test:core": "cd packages/core && yarn test", "test:middleware": "cd packages/middleware && yarn test" diff --git a/packages/cache/package.json b/packages/cache/package.json index 3001d52366..5328794e55 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/cache", - "version": "2.5.5", + "version": "2.5.13", "license": "MIT", "main": "lib/index.cjs.js", "module": "lib/index.es.js", @@ -12,7 +12,7 @@ "prepublish": "yarn build" }, "dependencies": { - "@vue-storefront/core": "~2.5.5" + "@vue-storefront/core": "~2.5.13" }, "peerDependencies": { "@nuxtjs/composition-api": "^0.29.3" diff --git a/packages/core/__tests__/factories/proxyUtils.spec.ts b/packages/core/__tests__/factories/proxyUtils.spec.ts deleted file mode 100644 index 983fae58f3..0000000000 --- a/packages/core/__tests__/factories/proxyUtils.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ - -import * as utils from '../../src/utils/nuxt/_proxyUtils'; -import isHttps from 'is-https'; - -jest.mock('is-https'); - -describe('[CORE - factories] apiFactory/_proxyUtils', () => { - it('returns base url based on incomming headers', () => { - expect(utils.getBaseUrl(null)).toEqual('/api/') - - ;(isHttps as jest.Mock).mockReturnValue(true); - expect(utils.getBaseUrl({ headers: { host: 'some-domain' } } as any)).toEqual('https://some-domain/api/') - - ;(isHttps as jest.Mock).mockReturnValue(false); - expect(utils.getBaseUrl({ headers: { host: 'some-domain' } } as any)).toEqual('http://some-domain/api/') - - ;(isHttps as jest.Mock).mockReturnValue(true); - expect(utils.getBaseUrl({ headers: { host: 'some-domain', 'x-forwarded-host': 'forwarded-host' } } as any)).toEqual('https://forwarded-host/api/') - - ;(isHttps as jest.Mock).mockReturnValue(false); - expect(utils.getBaseUrl({ headers: { host: 'some-domain', 'x-forwarded-host': 'forwarded-host' } } as any)).toEqual('http://forwarded-host/api/'); - }); - - it('returns proxy for defined api', () => { - const givenApi = { - getProduct: jest.fn() - }; - - const client = { - post: jest.fn(() => ({ then: jest.fn() })) - }; - - const proxiedApi = utils.createProxiedApi({ givenApi, client, tag: 'ct' }); - - proxiedApi.getProduct({ product: 1 }); - proxiedApi.getCategory({ category: 1 }); - - expect(givenApi.getProduct).toBeCalled(); - expect(client.post).toBeCalledWith('/ct/getCategory', [{ category: 1 }]); - }); - - it('reads cookies from incomming request', () => { - expect(utils.getCookies(null)).toEqual(''); - expect(utils.getCookies({} as any)).toEqual(''); - expect(utils.getCookies({ req: { headers: {} } } as any)).toEqual(''); - expect(utils.getCookies({ req: { headers: { cookie: { someCookie: 1 } } } } as any)).toEqual({ someCookie: 1 }); - }); - - it('it cobines config with the current one', () => { - jest.spyOn(utils, 'getCookies').mockReturnValue(''); - jest.spyOn(utils, 'getBaseUrl').mockReturnValue('some-url'); - - expect(utils.getIntegrationConfig( - null, - { someGivenOption: 1 } - )).toEqual({ - axios: { - baseURL: 'some-url', - headers: {} - }, - someGivenOption: 1 - }); - }); - - it('it cobines config with the current one and adds a cookie', () => { - jest.spyOn(utils, 'getCookies').mockReturnValue('xxx'); - jest.spyOn(utils, 'getBaseUrl').mockReturnValue('some-url'); - - expect(utils.getIntegrationConfig( - null, - {} - )).toEqual({ - axios: { - baseURL: 'some-url', - headers: { - cookie: 'xxx' - } - } - }); - }); -}); diff --git a/packages/core/__tests__/utils/i18n-redirects.spec.ts b/packages/core/__tests__/utils/i18n-redirects.spec.ts index 62e0694794..2d563fa30c 100644 --- a/packages/core/__tests__/utils/i18n-redirects.spec.ts +++ b/packages/core/__tests__/utils/i18n-redirects.spec.ts @@ -5,7 +5,8 @@ const defaultParams = { defaultLocale: 'en', availableLocales: ['ch/de', 'en', 'de'], cookieLocale: '', - acceptedLanguages: ['ch/de', 'en', 'de'] + acceptedLanguages: ['ch/de', 'en', 'de'], + autoRedirectByLocale: true }; describe('i18n redirects util', () => { @@ -119,5 +120,28 @@ describe('i18n redirects util', () => { expect(util.getTargetLocale()).toEqual('ch/de'); }); + + it('should return default language with autoRedirectByLocale set to false', async () => { + const util = i18nRedirectsUtil({ + ...defaultParams, + cookieLocale: 'de', + acceptedLanguages: ['de', 'es'], + autoRedirectByLocale: false + }); + + expect(util.getTargetLocale()).toEqual('en'); + }); + + it('should return language from path with autoRedirectByLocale set to false', async () => { + const util = i18nRedirectsUtil({ + ...defaultParams, + path: '/de', + cookieLocale: 'en', + acceptedLanguages: ['en', 'es'], + autoRedirectByLocale: false + }); + + expect(util.getTargetLocale()).toEqual('de'); + }); }); }); diff --git a/packages/core/__tests__/utils/nuxt/proxyUtils.spec.ts b/packages/core/__tests__/utils/nuxt/proxyUtils.spec.ts new file mode 100644 index 0000000000..fffca72f75 --- /dev/null +++ b/packages/core/__tests__/utils/nuxt/proxyUtils.spec.ts @@ -0,0 +1,69 @@ +import * as utils from '../../../src/utils/nuxt/_proxyUtils'; + +describe('[CORE - utils] _proxyUtils', () => { + process.server = true; + + it('returns proxy for defined api', () => { + const givenApi = { + getProduct: jest.fn() + }; + + const client = { + post: jest.fn(() => ({ then: jest.fn() })) + }; + + const proxiedApi = utils.createProxiedApi({ givenApi, client, tag: 'ct' }); + + proxiedApi.getProduct({ product: 1 }); + proxiedApi.getCategory({ category: 1 }); + + expect(givenApi.getProduct).toBeCalled(); + expect(client.post).toBeCalledWith('/ct/getCategory', [{ category: 1 }]); + }); + + it('reads cookies from incoming request', () => { + expect(utils.getCookies(null)).toEqual(''); + expect(utils.getCookies({} as any)).toEqual(''); + expect(utils.getCookies({ req: { headers: {} } } as any)).toEqual(''); + expect(utils.getCookies({ req: { headers: { cookie: { someCookie: 1 } } } } as any)).toEqual({ someCookie: 1 }); + }); + + it('it combines config with the current one', () => { + jest.spyOn(utils, 'getCookies').mockReturnValue(''); + + expect(utils.getIntegrationConfig( + { + $config: { + middlewareUrl: 'http://localhost.com' + } + } as any, + { someGivenOption: 1 } + )).toEqual({ + axios: { + baseURL: 'http://localhost.com/api', + headers: {} + }, + someGivenOption: 1 + }); + }); + + it('it combines config with the current one and adds a cookie', () => { + jest.spyOn(utils, 'getCookies').mockReturnValue('xxx'); + + expect(utils.getIntegrationConfig( + { + $config: { + middlewareUrl: 'http://localhost.com' + } + } as any, + {} + )).toEqual({ + axios: { + baseURL: 'http://localhost.com/api', + headers: { + cookie: 'xxx' + } + } + }); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 6256b9f10a..117418729b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/core", - "version": "2.5.5", + "version": "2.5.13", "sideEffects": false, "main": "lib/index.cjs.js", "module": "lib/index.es.js", @@ -15,7 +15,6 @@ "dependencies": { "axios": "0.21.1", "express": "^4.17.1", - "is-https": "^3.0.2", "lodash-es": "^4.17.15", "vue": "^2.6.11" }, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7edf3b70f2..08e4823e41 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2,6 +2,7 @@ import { Ref } from '@nuxtjs/composition-api'; import type { Request, Response } from 'express'; +import { HelmetOptions } from 'helmet'; /** * Default name of the cookie storing active localization code @@ -819,7 +820,7 @@ export interface ApiClientExtensionHooks { afterCall?: (params: AfterCallParams) => AfterCallArgs; } -export type CustomQueryFn = (query: any, variables: T) => { +export type CustomQueryFn = ({ query, variables, metadata }) => { query?: any; variables?: T; metadata: any; @@ -845,6 +846,7 @@ export type IntegrationsSection = Record export interface MiddlewareConfig { integrations: Record; + helmet?: boolean | Readonly; } export interface ApiClientFactoryParams { diff --git a/packages/core/src/utils/i18n-redirects/index.ts b/packages/core/src/utils/i18n-redirects/index.ts index 7bcbd03982..2a5f035883 100644 --- a/packages/core/src/utils/i18n-redirects/index.ts +++ b/packages/core/src/utils/i18n-redirects/index.ts @@ -3,13 +3,15 @@ const i18nRedirectsUtil = ({ defaultLocale, availableLocales, cookieLocale, - acceptedLanguages + acceptedLanguages, + autoRedirectByLocale }: { path: string; defaultLocale: string; cookieLocale: string; availableLocales: string[]; acceptedLanguages: string[]; + autoRedirectByLocale: boolean; }): { getRedirectPath: () => string; getTargetLocale: () => string; @@ -21,8 +23,8 @@ const i18nRedirectsUtil = ({ const getTargetLocale = (): string => { const languagesOrderedByPriority = [ localeFromPath, - cookieLocale, - ...acceptedLanguages, + ...(autoRedirectByLocale && [cookieLocale]), + ...(autoRedirectByLocale && acceptedLanguages), defaultLocale ]; diff --git a/packages/core/src/utils/nuxt/_proxyUtils.ts b/packages/core/src/utils/nuxt/_proxyUtils.ts index a64a17d700..ad2993b3c3 100644 --- a/packages/core/src/utils/nuxt/_proxyUtils.ts +++ b/packages/core/src/utils/nuxt/_proxyUtils.ts @@ -1,7 +1,7 @@ -import { IncomingMessage } from 'http'; import { Context as NuxtContext } from '@nuxt/types'; import merge from 'lodash-es/merge'; -import { ApiClientMethod } from '../../types'; +import { ApiClientMethod } from './../../types'; +import { Logger } from './../logger'; interface CreateProxiedApiParams { givenApi: Record; @@ -9,16 +9,6 @@ interface CreateProxiedApiParams { tag: string; } -export const getBaseUrl = (req: IncomingMessage, basePath: string | undefined = '/'): string => { - if (!req) return `${basePath}api/`; - const { headers } = req; - const isHttps = require('is-https')(req); - const scheme = isHttps ? 'https' : 'http'; - const host = headers['x-forwarded-host'] || headers.host; - - return `${scheme}://${host}${basePath}api/`; -}; - export const createProxiedApi = ({ givenApi, client, tag }: CreateProxiedApiParams) => new Proxy(givenApi, { get: (target, prop, receiver) => { @@ -36,15 +26,20 @@ export const createProxiedApi = ({ givenApi, client, tag }: CreateProxiedApiPara export const getCookies = (context: NuxtContext) => context?.req?.headers?.cookie ?? ''; export const getIntegrationConfig = (context: NuxtContext, configuration: any) => { + const baseURL = process.server ? context?.$config?.middlewareUrl : window.location.origin; const cookie = getCookies(context); - const initialConfig = merge({ + + if (process.server && context?.$config?.middlewareUrl) { + Logger.info('Applied middlewareUrl as ', context.$config.middlewareUrl); + } + + return merge({ axios: { - baseURL: getBaseUrl(context?.req, context?.base), + baseURL: new URL(/\/api\//gi.test(baseURL) ? '' : 'api', baseURL).toString(), headers: { ...(cookie ? { cookie } : {}) } } }, configuration); - - return initialConfig; }; + diff --git a/packages/core/src/utils/nuxt/index.ts b/packages/core/src/utils/nuxt/index.ts index 9ca1d86b3a..a729ffb2e2 100644 --- a/packages/core/src/utils/nuxt/index.ts +++ b/packages/core/src/utils/nuxt/index.ts @@ -25,10 +25,10 @@ export const integrationPlugin = (pluginFn: NuxtPlugin) => (nuxtCtx: NuxtContext const configure = (tag, configuration) => { const injectInContext = createAddIntegrationToCtx({ tag, nuxtCtx, inject }); const config = getIntegrationConfig(nuxtCtx, configuration); - const { middlewareUrl, ssrMiddlewareUrl } = (nuxtCtx as any).$config; + const { middlewareUrl, ssrMiddlewareUrl } = nuxtCtx.$config; if (middlewareUrl) { - config.axios.baseURL = process.server ? ssrMiddlewareUrl || middlewareUrl : middlewareUrl; + config.axios.baseURL = process.server ? middlewareUrl || ssrMiddlewareUrl : middlewareUrl; } const client = axios.create(config.axios); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 599c97b756..f010230316 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "./lib", "esModuleInterop": true, - "target": "ES2019", + "target": "es5", "module": "ES2015", "moduleResolution": "node", "importHelpers": true, @@ -12,11 +12,7 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "baseUrl": "./", - "lib": [ - "ES2019", - "ES2020", - "DOM" - ], + "lib": ["es6", "es7", "dom"], "strict": false }, "exclude": ["node_modules", "**/*.spec.ts"] diff --git a/packages/docs/.vuepress/components/IncludeContent.vue b/packages/docs/.vuepress/components/IncludeContent.vue index ee67017fc4..530e5e6d91 100644 --- a/packages/docs/.vuepress/components/IncludeContent.vue +++ b/packages/docs/.vuepress/components/IncludeContent.vue @@ -1,5 +1,5 @@ +``` + +Handling cookies in middlewares: + +```javascript +export default ({ $cookies }) => { + // `$cookies.get()` or `$cookies.set()` +}; +``` diff --git a/packages/docs/package.json b/packages/docs/package.json index e81d7aa52a..0e184c2dde 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -10,8 +10,9 @@ "core-cache-ref": "cd ../cache && api-extractor run --local", "core-core-ref": "cd ../core && api-extractor run --local", "core-middleware-ref": "cd ../middleware && api-extractor run --local", + "core-nuxt-module-ref": "cd ../nuxt-module && api-extractor run --local", "core-ref-md": "api-documenter markdown --i reference/api --o reference/api", - "build:core": "yarn core-cache-ref && yarn core-core-ref && yarn core-middleware-ref && yarn core-ref-md" + "build:core": "yarn core-cache-ref && yarn core-core-ref && yarn core-middleware-ref && yarn core-nuxt-module-ref && yarn core-ref-md" }, "devDependencies": { "@microsoft/api-documenter": "^7.13.7", @@ -21,6 +22,7 @@ "@vuepress/plugin-medium-zoom": "^1.8.2", "@vuepress/plugin-search": "^1.8.2", "handlebars": "^4.7.7", + "markdown-it-video": "^0.6.3", "node-html-to-image": "^3.2.0", "typescript": "^3.6.4", "vuepress": "^1.2.0" diff --git a/packages/docs/performance/improving-core-web-vitals.md b/packages/docs/performance/improving-core-web-vitals.md new file mode 100644 index 0000000000..cb1e2f2766 --- /dev/null +++ b/packages/docs/performance/improving-core-web-vitals.md @@ -0,0 +1,101 @@ +# Improving Core Web Vitals + +Web Vitals are unified and simplified metrics created by Google to help site owners understand the quality of experience they are delivering to their users. Core Web Vitals are the subset of Web Vitals focused on three aspects of the user experience - loading (LCP), interactivity (FID), and visual stability (CLS). + +[Read more about Web Vitals](https://web.dev/vitals/). + +## Largest Contentful Paint (LCP) :orange_book: + +The Largest Contentful Paint (LCP) represents the time needed to display the biggest element visible to the user within the initial viewport. To improve the LCP metric, you need to start loading that element as fast as possible. If it needs an asset like JavaScript, CSS, image, or font, use the "preloading". It's a technique for telling a browser to download a resource before it discovers it's needed. + +For example, you might have a font that the browser can discover late (because it first needs to download and parse the CSS file), but you know that it's critical to your website. You can preload it, so once the browser parses the CSS and finds out that the font is needed, it will already have it downloaded. + +[Read more about preloading critical assets](https://web.dev/preload-critical-assets/). + +Below we describe two ways of preloading resources in Nuxt.js, depending on your needs. + +::: warning Be careful +Preloading too many resources can have the opposite effect and impact the performance. + +Try to limit the number of resources preloaded to just those visible on the initial viewport. **If you prioritize everything, you don't prioritize anything**. +::: + +### Preloading resource on every page + +If you want to preload a resource on every page, e.g., the font used across the whole website, use the `head` property in the `nuxt.config.js` file. + +:::tip +If you are using Google Fonts, we recommend loading them using the [@nuxtjs/google-fonts](https://google-fonts.nuxtjs.org/) package. It offers reasonable defaults and performance-oriented options. +::: + +```javascript +// nuxt.config.js + +export default { + head: { + link: [ + { + rel: 'preload', + as: 'style', + href: '.../stylesheet.css' + } + ] + } +}; +``` + +### Preloading resource on a specific page + +If you want to preload a resource on a specific page, e.g., a hero image used only on the homepage, use the `head` method in the Vue.js component. + +::: tip +If you use the [@nuxt/image](https://image.nuxtjs.org/) package, you don't have to use the `head` method. Instead you can add ["preload" attribute](https://image.nuxtjs.org/components/nuxt-img#preload) to the `` component. +::: + +```vue + +``` + +### How to identify what is the Largest Contentful Paint + +The easiest way to learn which element is the Largest Contentful Paint is to use the Core Web Vital test on the [WebPageTest](https://www.webpagetest.org/webvitals) page. Enter the website URL, select one of the recommended locations and browsers, and click the `Start Test` button. + +Another option is to run Lighthouse in Chrome DevTools, described on [Google Developers](https://developers.google.com/web/tools/lighthouse#devtools) website. + +## Cumulative Layout Shift (CLS) :orange_book: + +Cumulate Layout Shift is an important user-centric metric for measuring visual stability, which shows if page had any unexpected movement. + +[Read more about CLS](https://web.dev/cls/) + +When the browser parses the HTML, it will reserve the space for images based on their `width` and `height` attributes. When they are not defined, the browser will not do that, and when the image is loaded, it will have to make space for it by moving all other content causing layout shift. + +### Always declare image width and height + +To prevent layout shifts, always use the image `width` and `height` attributes to let the browser know how much space it needs to save. Remember to do it for all images, including header logos. + +::: tip +If you are using the [@nuxt/image](https://image.nuxtjs.org/) package, you can use the same attributes in the `` component. +::: + +```html + +``` diff --git a/packages/docs/performance/intro.md b/packages/docs/performance/intro.md new file mode 100644 index 0000000000..20edaaa538 --- /dev/null +++ b/packages/docs/performance/intro.md @@ -0,0 +1,24 @@ +# Introduction to Web Performance + +Web performance is important subject in modern web. Google study over millions of page impressions found that when a site meets the recommended thresholds for the Core Web Vitals metrics, users are at least 24% less likely to abandon a page before it finishes loading. You can read more [on Chromium blog](https://blog.chromium.org/2020/05/the-science-behind-web-vitals.html). + +On the following pages you will find list of good practices that will help you optimize your website performance. + +## The indicators + +We marked some sections with the indicator below to show how impactful each problem can be: + +:orange_book: - Most important. When fixed, the usability of your website should noticeably improve. + +:ledger: - Nice to have. Fixing them could improve the performance. + +:blue_book: - Optional fixes, which might help in some cases. + +## Additional resources + +Other great resources that helped us in writing these suggestions: + +* [web.dev](https://web.dev/) +* [sitesped.io](https://www.sitespeed.io/) +* [Jakub Andrzejewski blog post](https://dev.to/theandrewsky/performance-checklist-for-vue-and-nuxt-cog) +* [wpostats.com/](https://wpostats.com/) diff --git a/packages/docs/performance/optimizing-html-and-css.md b/packages/docs/performance/optimizing-html-and-css.md new file mode 100644 index 0000000000..b9451c1481 --- /dev/null +++ b/packages/docs/performance/optimizing-html-and-css.md @@ -0,0 +1,87 @@ +# Optimizing HTML and CSS + +Large render-blocking CSS files and extensive DOM can significantly impact page performance. Below we share some tips on how to prevent that. + +## Remove unused styles :ledger: + +Removing unused styles reduces the amount of data needed to be sent through the network and the rendering time because the browser has fewer styles to process. + +The `@vue-storefront/nuxt` package present in every Vue Storefront project has a `purgeCSS` option that does this exact thing. + +```javascript{6-13} +// nuxt.config.js + +export default { + buildModules: [ + ['@vue-storefront/nuxt', { + performance: { + purgeCSS: { + enabled: true, + paths: [ + '**/*.vue' + ] + } + } + }] + ] +}; +``` + +`purgeCSS` option (_disabled by default_) uses [nuxt-purgecss](https://github.com/Developmint/nuxt-purgecss) plugin to remove unused CSS and accepts the same options, with two differences: + +* with `enabled: false`, the plugin will not be registered at all, not only be disabled + +* `**/*.vue` is added to `paths` array to detect all `.vue` files in your project, including those from `_theme` directory. Without this, the plugin would also remove some styles used on the page. + +If you decide to enable this plugin, we recommend using `enabled: process.env.NODE_ENV === 'production'`, to keep development mode as fast as possible. + +::: warning +Because PurgeCSS looks for whole class names in files, it may remove styles for dynamic classes. If you're using a dynamic class, make sure you use whole names instead of concatinating variables (eg. `isDev ? 'some-style-dev' : 'some-style-prod'` instead of `some-style-${ isDev ? 'dev' : 'prod' }`. If this can't be avoided, add them to `whitelist` array. +::: + +## Use HTTP2 Push :blue_book: + +HTTP2 Push is a performance technique to reduce latency by loading resources even before the browser knows it will need them. + +Consider a website with three resources: + +* index.html, +* styles.css, +* scripts.js. + +First, the browser will load and parse index.html. While parsing, it will find information about styles.css and script.js, sending a request to the server to get them. Because we know that the page needs those two files, we can use HTTP2 Push to send them to the client immediately without waiting for the client to request them. + +```javascript{6-8} +// nuxt.config.js + +export default { + buildModules: [ + ['@vue-storefront/nuxt', { + performance: { + httpPush: true + } + }] + ] +}; +``` + +The `httpPush` option (_enabled by default_) leverages [http2](https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-render#http2) option in Nuxt.js. It's configured to automatically push all JavaScript files needed for the current page. If you want to override this behavior, you can disable this option and use the Nuxt.js configuration instead. + +If you can't use HTTP2, you can disable this option. In this case, Nuxt.js will still `preload` these scripts, which is only slightly slower than the HTTP2 push. + +## Avoid extensive DOM size :ledger: + +Large DOM will increase memory usage, cause longer style calculations, and produce costly layout reflows. In your components, try to make as flat structure as possible and avoid nesting HTML elements. Check if the library you use doesn't create complex HTML structures. There are cases when a simple button generates 1000 lines of code. + +## Don't load print stylesheets :blue_book: + +Loading a specific stylesheet for printing slows down the page, even when not used. You can include the print styles inside your other CSS file(s) by using an `@media` query targeting type print. + + +```css +@import url("fineprint.css") print; +``` + +## Don't import SCSS files from StorefrontUI :ledger: + +`@vue-storefront/nuxt` module automatically detects if you have the `@storefront-ui/vue` package installed and, registers [@nuxtjs/style-resources](https://github.com/nuxt-community/style-resources-module) module. It automatically registers all variables, mixins, and functions from StorefrontUI, which means you don't have to import them. Importing SCSS files from StorefrontUI might duplicate some styles, significantly increasing your bundle size and impacting performance. diff --git a/packages/docs/performance/optimizing-images.md b/packages/docs/performance/optimizing-images.md new file mode 100644 index 0000000000..e4a7570ff4 --- /dev/null +++ b/packages/docs/performance/optimizing-images.md @@ -0,0 +1,74 @@ +# Optimizing images + +Images are likely the most straightforward resource to optimize. Yet if you forget to do it for at least one of them, your website might become a few megabytes heavier. + +On this page, we will share some tips on how you can prevent that. + +## Use the `@nuxt/image` package :orange_book: + +Using the [``](https://image.nuxtjs.org/components/nuxt-img) component from the [@nuxt/image](https://image.nuxtjs.org/) package is likely the single best thing you can do to stop worrying about images. It offers features for most of the things mentioned in the following sections. Using most of them only requires you to pass a single attribute. + +It offers image resizing, converting formats, preloading, and integrations with the most popular image transformation services. + +```html + +``` + +## Compress images using next-generation formats :orange_book: + +The most common performance bottlenecks are images that are not compressed and weigh multiple times more than they should. For this reason, you should always compress images, and luckily nowadays, there are plenty of lossless and lossy file types supported in modern browsers. + +If you have just a few static images on your website, you can manually compress them using a website like [Squoosh.app](https://squoosh.app/). However, if you have more images or want it to happen automatically, you can use the `@nuxt/image` package mentioned above. + +## Don't declare images in CSS :ledger: + +Images declared in the CSS files are often downloaded much later than those in HTML because the browser has to download and parse the CSS file before knowing that it has to load and display an image. + +If the image is also the biggest element visible to the user within the initial viewport, it will also negatively impact the [Largest Contentful Paint](/performance/improving-core-web-vitals.html#largest-contentful-paint-lcp) time. + +```diff +- + ++
++ ++
+``` + +## Lazy load offscreen images :orange_book: + +Lazy loading is a technique used to prevent or delay the loading of non-critical resources until they are needed. You can use this mechanism for different types of resources, but in the case of images, our goal is to lazily load everything that is not visible to the user within the initial viewport. All other images can be loaded when the user scrolls down the page. + +Use the `loading="lazy"` attribute to load an image lazily. It also works for the `` component. + +```html + + +``` + +If you want to load resources other than images lazily, check out the [vue-lazyload](https://www.npmjs.com/package/vue-lazyload) package. + +## Scale images on the server, not browser :orange_book: + +Don't download big images to scale them down in the browser because it results in downloading data and processing that you could have avoided. Instead, use a tool or service that creates multiple versions of the same image server-side and serves the appropriate one depending on the size. + +Here's the example using the `` component that will handle this automatically. + +```html + +``` diff --git a/packages/docs/performance/optimizing-javascript.md b/packages/docs/performance/optimizing-javascript.md new file mode 100644 index 0000000000..c98d587758 --- /dev/null +++ b/packages/docs/performance/optimizing-javascript.md @@ -0,0 +1,131 @@ +# Optimizing JavaScript + +Loading too much JavaScript increases the time the browser parses, compiles, and executes it. It's even worse if a given page doesn't use most of it. + +On this page, we will share some tips on how you can prevent that and serve only the scripts needed. + +## Remove unused scripts :orange_book: + +Removing unused scripts reduces the amount of data sent through the network and time required to make the page interactive because the browser has fewer scripts to process. + +### Analyze JavaScript bundles + +You can check the JavaScript bundles with tools like [Webpack bundle analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer). In Nuxt, this library is available out of the box by [changing the configuration](https://nuxtjs.org/docs/configuration-glossary/configuration-build/#analyze) or running a build with the `--analyze` flag. + +```bash +yarn nuxt build --analyze +``` + +:::warning +Remember to never use these options in production. +::: + +### Tree-shaking + +Tree shaking is a technique for eliminating dead code from the final bundle. "Dead code" is the code that never gets used or called. Smaller bundles mean that the browser has less JavaScript to download and parse. + +To make tree-shaking work properly, you should avoid importing the whole package, but instead just what you need. + +```diff +- import * as arrayUtils from 'array-utils'; ++ import { unique, reverse, sortBy } from 'array-utils'; +``` + +### Code splitting + +Bundlers often output big JavaScript files, which contain all application's code. Code splitting allows for creating smaller files with only the code needed for a specific page or component. This technique helps speed up a page by skipping unused JavaScript code. + +There is also a vendor bundle with common parts shared between multiple bundles. For example, if Home and Product pages use the same Navigation component, it will go to the vendor bundle and be downloaded only once. + +Nuxt.js does code-splitting out of the box by creating separate bundles for every page/route. You can control this behavior using the [build.splitChunks](https://nuxtjs.org/docs/configuration-glossary/configuration-build/#splitchunks) property in `nuxt.config.js` file. + +## Avoid serving polyfills to modern browsers :ledger: + +Polyfills and transforms enable you to use new JavaScript features in a legacy browser. However, they are unnecessary in modern browsers, making the bundle bigger and impacting the performance. + +### Modern mode + +The Nuxt.js has a [--modern](https://nuxtjs.org/docs/configuration-glossary/configuration-modern/) parameter that you can use with the `nuxt build` command to create two bundles: + +* "legacy" bundle for older browsers, +* "modern" bundle for evergreen browsers. + +Browsers will load only one of them, depending on whether it supports ES modules or not. + +### Configure Babel + +Babel is a toolchain used to converting modern JavaScript code into a backwards compatible version for current and older browsers or environments. + +Nuxt.js includes it out of the box. You can control its behavior using the [build.babel](https://nuxtjs.org/docs/configuration-glossary/configuration-build/#babel) property in `nuxt.config.js` file. + +Default configuration: + +```javascript +// nuxt.config.js + +export default { + build: { + babel: { + babelrc: false, + cacheDirectory: undefined, + presets: ['@nuxt/babel-preset-app'] + } + } +}; +``` + +With this configuration, the default targets are: + +* `ie: '9'` for the legacy bundle. +* `esmodules:true` for the modern bundle. +* `node: 'current'` for the server bundle. + +## Avoid adding third-party scripts :ledger: + +Third-party code can significantly impact the performance, and the best thing you can do is not to add them to your page at all. However, if you have to, there are some tricks to reduce the performance impact on your application. + +* Load scripts with the `async` or `defer` attribute to avoid blocking document parsing. + +```javascript +// nuxt.config.js + +export default { + head: { + script: [ + { + src: ``, + defer: true + }, + { + src: '', + async: true + }, + ] + } +}; +``` + +* Self-host the script if the third-party server is slow. +* Remove the script if it doesn't add clear value to your site. +* Use the `rel=preconnect` or `rel=dns-prefetch` attributes in `` to do a DNS lookup for domains hosting third-party scripts. + +```javascript +// nuxt.config.js + +export default { + head: { + link: [ + { rel: 'dns-prefetch', href: 'https://fonts.googleapis.com' }, + { rel: 'preconnect', href: 'https://fonts.gstatic.com' }, + ], +}; +``` + +* Lazy load third-party resources [with facades](https://web.dev/third-party-facades/?utm_source=lighthouse&utm_medium=devtools). +* Move third-party scripts to Web worker using, for example, the [@nuxtjs/partytown](https://github.com/nuxt-community/partytown-module) module. + +### Educate people using Google Tag Manager about web perf + +Non-technical users often use Google Tag Manager to add scripts, styling, and other elements or toggle content visibility on the page. This can lead to Cumulate Layout Shifts, extra Total Blocking Time, an increased number of requests and their weight, or even rerendering the whole page. + +Developers should educate them that adding scripts via Google Tag Manager can significantly impact performance. diff --git a/packages/docs/performance/other-optimizations.md b/packages/docs/performance/other-optimizations.md new file mode 100644 index 0000000000..e1633e9bac --- /dev/null +++ b/packages/docs/performance/other-optimizations.md @@ -0,0 +1,140 @@ +# Other optimizations + +There are plenty of general optimizations that didn't fit the previous categories or improve multiple areas of the application. + +## Avoid render-blocking resources :orange_book: + +Render-blocking resources are scripts, stylesheets, and other imports in the `` that delay the browser from rendering page content to the screen until they are downloaded and parsed. Such resources delay the First Paint - time needed for the browser to render something (i.e., background colors, borders, text, or images) for the first time. To prevent that: + +* add the `defer` or `async` attribute to the `