Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ipx production support #257

Merged
merged 13 commits into from May 27, 2021
19 changes: 17 additions & 2 deletions docs/pages/en/1.getting-started/1.installation.md
Expand Up @@ -23,14 +23,29 @@ Add `@nuxt/image` devDependency to your project:
::::
:::::

Add the module to the `buildModules` of your `nuxt.config`:
Add the module to `buildModules` in your `nuxt.config`:

```ts [nuxt.config.js]
export default {
buildModules: ['@nuxt/image']
target: 'static',
buildModules: [
'@nuxt/image',
]
}
```

:::alert{type="info"}
If you have a `server` target and are using the default provider ([`ipx`](/providers/ipx)), make sure you add `@nuxt/image` to your `modules` instead to self-host your `/_ipx` endpoint.

```ts [nuxt.config.js]
export default {
modules: [
'@nuxt/image',
]
}
```
:::

:::alert{type="success"}
You can now start using [`<nuxt-img>`](/components/nuxt-img) and [`<nuxt-picture>`](/components/nuxt-picture) components in your Nuxt app ✨
:::
Expand Down
25 changes: 12 additions & 13 deletions docs/pages/en/1.getting-started/2.providers.md
Expand Up @@ -3,27 +3,26 @@ title: Providers
description: Nuxt Image supports multiple providers for high performances.
---

## Default provider
Providers are integrations between Nuxt Image and third-party image transformation services. Each provider is responsible for generating correct URLs for that image transformation service.

Nuxt Image comes with a preconfigured instance of [IPX](/providers/ipx) to provide images transformations based on the [Sharp](https://www.npmjs.com/package/sharp) package. This default provider can be used without any configuration. The module will automatically optimize `<nuxt-img>` or `<nuxt-picture>` sources and accepts all [options](/api/options/), except for any modifiers that are specific to other providers.

With a `server` target, IPX will generate images at runtime. With a `static` target, IPX will generate all optimized assets on running `nuxt generate` and store them in the `dist` folder.

[Learn more about Nuxt deployment targets](https://nuxtjs.org/docs/2.x/features/deployment-targets)

## Image Providers

Providers are integrations between the Nuxt Image module and third-party image transformation services (or, as in the case of `ipx`, a self-hosted/generate-time image transformation service). Each provider is responsible for generating correct URLs for that image transformation service.

Nuxt Image can be configured to work with any external image transformation service. Here is a complete list of providers that are supported out-of-the-box and can be easily configured to use:
Nuxt Image can be configured to work with any external image transformation service. Here is a list of providers that are supported out-of-the-box and can be easily configured to use:

- [`IPX`](/providers/ipx) (default)
- [`Cloudinary`](/providers/cloudinary)
- [`Fastly`](/providers/fastly)
- [`Imgix`](/providers/imgix)
- [`IPX`](/providers/ipx) (selfhosted)
- [`Prismic`](/providers/prismic)
- [`Sanity`](/providers/sanity)
- [`Twicpics`](/providers/twicpics)
- [`Storyblok`](/providers/storyblok)

If you are looking for a specific provider outside of this list, you can [create your own provider](/advanced/custom-provider).

Nuxt Image will automatically optimize `<nuxt-img>` or `<nuxt-picture>` sources and accepts all [options](/api/options/) for specified target, except for modifiers that are specific to other providers.

## Default Provider

The default provider for Nuxt Image is [ipx provider](/providers/ipx) or [static images](/getting-started/static) (for `target: static`). Either option can be used without any configuration.

[Learn more about Nuxt deployment targets](https://nuxtjs.org/docs/2.x/features/deployment-targets)

10 changes: 10 additions & 0 deletions docs/pages/en/1.getting-started/3.static.md
@@ -0,0 +1,10 @@
---
title: Static images
description: 'Optimizing images for static websites'
---

If you are building a static site, Nuxt Image will optimize and save your images locally when your site is generated - and deploy them alongside your generated pages.

:::alert{type="info"}
Even if you are using another provider, you can opt-in to this generate behaviour for a particular image by setting `provider="static"` directly. (See [component documentation](/components/nuxt-img) for more information.)
:::
69 changes: 57 additions & 12 deletions docs/pages/en/4.providers/ipx.md
@@ -1,23 +1,68 @@
---
title: IPX Provider (static)
description: 'Nuxt Image internally use IPX as static provider.'
title: IPX Provider
description: 'Self hosted image provider'
navigation:
title: IPX (static)
title: IPX
---

When no provider is specified globally, the default provider is `static` which uses [IPX](https://github.com/nuxt-contrib/ipx).

IPX is an opensource and self-hosted image optimizer based on [Sharp](https://github.com/lovell/sharp) developed by the Nuxt team.
Nuxt Image comes with a preconfigured instance of [ipx](/providers/ipx) to provide image transformations based on [sharp](https://github.com/lovell/sharp).
IPX is an open source, self-hosted image optimizer based on [sharp](https://github.com/lovell/sharp).

The image module internally uses IPX for static image optimization but you can also self-host it as a runtime optimizer by enabling the provider.
## Self-hosting `ipx` in production

### Static assets
### Using CDN

It's common if you are using a third-party provider that you may want to also include some images
that are stored locally within your repo.
This approach is recommended if you are planning to use images in a high load production and using other providers is not suitable.

```vue
<NuxtImg provider="static" src="/logo.png" width="300" height="200" />
### Add `ipx` dependency

You'll need to ensure that `ipx` is in your production dependencies.

<d-code-group>
<d-code-block label="Yarn" active>

```bash
yarn add ipx
```

</d-code-block>
<d-code-block label="NPM">

```bash
npm install ipx
```

This will load the image in as `/static/logo.png` and apply the IPX optimizations if applicable.
</d-code-block>
</d-code-group>

### Add `serverMiddleware` handler

Finally, just add `@nuxt/image` to your `modules` (instead of `buildModules`) in `nuxt.config`. This will ensure that the `/_ipx` endpoint continues to work at runtime.


### Programmatic middidleware

If you have an advanced use case, you may instead add the following code to your `nuxt.config` (or create a custom server middleware file directly that handles the `/_ipx` endpoint):

```js [nuxt.config.js]
import path from 'path'
import { createIPX, createIPXMiddleware } from 'ipx'

const ipx = createIPX({
dir: path.join(__dirname, 'static'),
// https://image.nuxtjs.org/api/options#domains
domains: [],
// Any options you need to pass to sharp
sharp: {}
})

export default {
serverMiddleware: [
{
path: '/_ipx',
handler: createIPXMiddleware(ipx)
}
]
}
```
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -24,7 +24,6 @@
"test:unit": "jest test/unit --forceExit"
},
"dependencies": {
"consola": "^2.15.3",
"defu": "^5.0.0",
"fs-extra": "^10.0.0",
"hasha": "^5.2.2",
Expand All @@ -34,6 +33,7 @@
"lru-cache": "^6.0.0",
"node-fetch": "^2.6.1",
"p-limit": "^3.1.0",
"rc9": "^1.2.0",
"requrl": "^3.0.2",
"ufo": "^0.7.5",
"upath": "^2.0.1"
Expand All @@ -51,6 +51,7 @@
"@types/jest": "latest",
"@types/lru-cache": "latest",
"@types/node-fetch": "latest",
"@types/semver": "^7.3.5",
"@vue/test-utils": "latest",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "latest",
Expand Down
4 changes: 1 addition & 3 deletions playground/nuxt.config.ts
Expand Up @@ -9,10 +9,8 @@ export default <NuxtConfig> {
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
},
modules: [
'../src/module.ts'
],
buildModules: [
'../src/module.ts',
'@nuxt/typescript-build'
],
image: {
Expand Down
60 changes: 55 additions & 5 deletions src/ipx.ts
@@ -1,7 +1,57 @@
import type { IPXOptions } from 'ipx'
import { relative, resolve } from 'upath'
import { update as updaterc } from 'rc9'
import { mkdirp, readFile, writeFile } from 'fs-extra'
import { lt } from 'semver'

export function createIPXMiddleware (ipxOptions: IPXOptions) {
const { createIPX, createIPXMiddleware } = require('ipx') as typeof import('ipx')
const ipx = createIPX(ipxOptions)
return createIPXMiddleware(ipx)
import type { ProviderSetup } from './types'

export const ipxSetup: ProviderSetup = async (_providerOptions, moduleOptions, nuxt) => {
const isStatic = nuxt.options.target === 'static'
const runtimeDir = resolve(__dirname, 'runtime')
const ipxOptions = {
dir: resolve(nuxt.options.rootDir, moduleOptions.dir),
domains: moduleOptions.domains,
sharp: moduleOptions.sharp
}

// Add IPX middleware unless nuxtrc or user added a custom middleware
const hasUserProvidedIPX = !!nuxt.options.serverMiddleware
.find((mw: { path: string }) => mw.path && mw.path.startsWith('/_ipx'))

if (!hasUserProvidedIPX) {
const { createIPX, createIPXMiddleware } = await import('ipx')
const ipx = createIPX(ipxOptions)
nuxt.options.serverMiddleware.push({
path: '/_ipx',
handle: createIPXMiddleware(ipx)
})
}

// Warn if unhandled /_ipx endpoint only if not using `modules`
const installedInModules = nuxt.options.modules.some(
(mod: string | (() => any)) => typeof mod === 'string' && mod.includes('@nuxt/image')
)

if (!isStatic && !hasUserProvidedIPX && !installedInModules && lt(nuxt.constructor.version, '2.16.0')) {
// eslint-disable-next-line no-console
console.warn('[@nuxt/image] If you would like to use the `ipx` provider at runtime.\nMake sure to follow the instructions at https://image.nuxtjs.org/providers/ipx .')
}

if (nuxt.options.dev || hasUserProvidedIPX) {
return
}

// In production, add IPX module to nuxtrc (used in Nuxt 2.16+)
nuxt.hook('build:done', async () => {
const handler = await readFile(resolve(runtimeDir, 'ipx.js'), 'utf-8')
const distDir = resolve(nuxt.options.buildDir, 'dist')
const apiDir = resolve(distDir, 'api')
const apiFile = resolve(apiDir, 'ipx.js')
const relativeApiFile = '~~/' + relative(nuxt.options.rootDir, apiFile)

await mkdirp(apiDir)
await writeFile(apiFile, handler.replace(/.__IPX_OPTIONS__./, JSON.stringify(ipxOptions)))

updaterc({ serverMiddleware: [{ path: '/_ipx', handler: relativeApiFile }] }, { dir: distDir, name: 'nuxtrc' })
})
}
22 changes: 5 additions & 17 deletions src/module.ts
@@ -1,16 +1,14 @@
import { resolve } from 'upath'

import defu from 'defu'

import type { Module } from '@nuxt/types'
import { setupStaticGeneration } from './generate'
import { createIPXMiddleware } from './ipx'
import { resolveProviders, detectProvider } from './provider'
import { pick, pkg } from './utils'
import type { ModuleOptions, CreateImageOptions } from './types'
import type { Module } from '@nuxt/types'

const imageModule: Module<ModuleOptions> = async function imageModule (moduleOptions) {
const { nuxt, addPlugin, addServerMiddleware } = this
const { nuxt, addPlugin } = this

const defaults: ModuleOptions = {
staticFilename: '[publicPath]/image/[hash][ext]',
Expand All @@ -36,8 +34,7 @@ const imageModule: Module<ModuleOptions> = async function imageModule (moduleOpt

const options: ModuleOptions = defu(moduleOptions, nuxt.options.image, defaults)

options.provider = detectProvider(options.provider)

options.provider = detectProvider(options.provider, nuxt.options.target === 'static')
options[options.provider] = options[options.provider] || {}

const imageOptions: Omit<CreateImageOptions, 'providers'> = pick(options, [
Expand Down Expand Up @@ -73,16 +70,7 @@ const imageModule: Module<ModuleOptions> = async function imageModule (moduleOpt
}
})

addServerMiddleware({
path: '/_ipx',
handle: createIPXMiddleware({
dir: options.dir,
domains: options.domains,
sharp: options.sharp
})
})

// transform asset urls that pass to `src` attribute on image components
// Transform asset urls that pass to `src` attribute on image components
nuxt.options.build.loaders = defu({
vue: { transformAssetUrls: { 'nuxt-img': 'src', 'nuxt-picture': 'src', NuxtPicture: 'src', NuxtImg: 'src' } }
}, nuxt.options.build.loaders || {})
Expand All @@ -102,6 +90,6 @@ const imageModule: Module<ModuleOptions> = async function imageModule (moduleOpt
})
}

; (imageModule as any).meta = pkg
; (imageModule as any).meta = pkg

export default imageModule
9 changes: 7 additions & 2 deletions src/provider.ts
Expand Up @@ -3,6 +3,7 @@ import { writeJson, mkdirp } from 'fs-extra'
import { parseURL } from 'ufo'
import { hash } from './utils'
import type { ModuleOptions, InputProvider, ImageModuleProvider, ProviderSetup } from './types'
import { ipxSetup } from './ipx'

const BuiltInProviders = [
'cloudinary',
Expand All @@ -19,6 +20,10 @@ const BuiltInProviders = [
]

export const providerSetup: Record<string, ProviderSetup> = {
// IPX
ipx: ipxSetup,
static: ipxSetup,

// https://vercel.com/docs/more/adding-your-framework#images
async vercel (_providerOptions, moduleOptions, nuxt) {
const imagesConfig = resolve(nuxt.options.rootDir, '.vercel_build_output/config/images.json')
Expand Down Expand Up @@ -74,7 +79,7 @@ export function resolveProvider (nuxt: any, key: string, input: InputProvider):
}
}

export function detectProvider (userInput?: string) {
export function detectProvider (userInput?: string, isStatic: boolean = false) {
if (process.env.NUXT_IMAGE_PROVIDER) {
return process.env.NUXT_IMAGE_PROVIDER
}
Expand All @@ -87,5 +92,5 @@ export function detectProvider (userInput?: string) {
return 'vercel'
}

return 'static'
return isStatic ? 'static' : 'ipx'
}
2 changes: 1 addition & 1 deletion src/runtime/components/nuxt-img.vue
Expand Up @@ -7,9 +7,9 @@
</template>

<script lang="ts">
import { imageMixin } from './image.mixin'
import type { DefineComponentWithMixin } from '../../types/vue'
import type { ImageSizes } from '../../types'
import { imageMixin } from './image.mixin'

import { parseSize } from '~image'

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/components/nuxt-picture.vue
Expand Up @@ -16,8 +16,8 @@
</template>

<script lang="ts">
import { imageMixin } from './image.mixin'
import type { DefineComponentWithMixin } from '../../types/vue'
import { imageMixin } from './image.mixin'

import { getFileExtension } from '~image'

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/image.ts
@@ -1,8 +1,8 @@
import defu from 'defu'
import type { ImageOptions, ImageSizesOptions, CreateImageOptions, ResolvedImage, MapToStatic, ImageCTX, $Img } from '../types/image'
import { imageMeta } from './utils/meta'
import { parseSize } from './utils'
import { useStaticImageMap } from './utils/static-map'
import type { ImageOptions, ImageSizesOptions, CreateImageOptions, ResolvedImage, MapToStatic, ImageCTX, $Img } from '../types/image'

export function createImage (globalOptions: CreateImageOptions, nuxtContext: any) {
const staticImageManifest: Record<string, string> = (process.client && process.static) ? useStaticImageMap(nuxtContext) : {}
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/ipx.ts
@@ -0,0 +1,5 @@
import { createIPX, createIPXMiddleware } from 'ipx'

const ipx = createIPX('__IPX_OPTIONS__')

export default createIPXMiddleware(ipx)
2 changes: 1 addition & 1 deletion src/runtime/providers/prismic.ts
@@ -1,6 +1,6 @@
import { joinURL, parseQuery, parseURL, stringifyQuery } from 'ufo'
import { operationsGenerator } from './imgix'
import type { ProviderGetImage } from 'src'
import { operationsGenerator } from './imgix'

const PRISMIC_IMGIX_BUCKET = 'https://images.prismic.io'

Expand Down