Squashed commit of the following:

commit fbab73c96e
Author: matthewp <matthewp@users.noreply.github.com>
Date:   Tue Mar 7 16:38:11 2023 +0000

    [ci] format

commit a206106098
Author: Matthew Phillips <matthew@skypack.dev>
Date:   Tue Mar 7 11:35:54 2023 -0500

    Expose the ssr manifest (#6435)

    * Expose the ssr manifest

    * Add changeset

    * Add types for virtual mod

commit 2751584387
Author: Princesseuh <Princesseuh@users.noreply.github.com>
Date:   Tue Mar 7 15:14:15 2023 +0000

    [ci] format

commit 694918a56b
Author: Erika <3019731+Princesseuh@users.noreply.github.com>
Date:   Tue Mar 7 16:12:21 2023 +0100

    Implement RFC "A core story for images" (#6344)

    * feat(assets): Add Vite plugin

    * feat(images): Set up Image component

    * fix(types): Attempt to fix type generation

    * Revert "fix(types): Attempt to fix type generation"

    This reverts commit 063aa276e2.

    * fix(image): Fix image types causing build to fail

    * feat(image): Implement client side part

    * feat(services): Allow arbitrary transforms parameters

    * fix(image): Fix paths and types

    * config(types): Update config types to provide completions for available services

    * feat(image): Add serving in dev

    * feat(image): Improve type error messages

    * refactor(image): Move sharp's parseParams to baseService

    * refactor(image): Skip work in dev for remote servies

    * feat(image): Add support for remote images

    * feat(image): Add squoosh service

    * chore: update export map

    * refactor(image): Abstract attributes handling by services

    * config(vercel): Remove test image service

    * feat(image): Support for relative images in Markdown (WIP)

    * feat(images): Add support for relative images in Markdown

    * feat(image): Update with RFC feedback

    * fix(image): Fix alt error on getImage

    * feat(image): Add support for assets validation through content collections

    * feat(image): Remove validateTransform

    * feat(image): Move to assets folder

    * fix(image): Fix package exports

    * feat(image): Add static imports references to virtual moduel

    * fix(image): Fix images from content collections not working when embedded

    * chore: lockfile

    * fix(markdown): Fix type

    * fix(images): Flag enhanced images behing an experimental flag

    * config(example): Update images example conifg

    * fix(image): Fix types

    * fix(image): Fix asset type for strict, allow arbritary input and output formats

    * chore: fix example check

    * feat(image): Emit assets for ESM imported images

    * Add initial core image tests (#6381)

    * feat(images): Make frontmatter extraction more generic than images for future

    * feat(image): Add support for building

    * fix(image): Fix types

    * fix(images): Fix compatibility with image integration

    * feat(images): Cuter generation stats

    * fix(images): Globals are unsafe, it turns out

    * fix(images): Only generate images if flag is enabled

    * fix(images): Only create `addStaticImage` in build

    * feat(images): Add SSR endpoint

    * fix(images): Only inject route in SSR

    * Add tests for SSR

    * Remove console.log

    * Updated lockfile

    * rename to satisfy the link gods

    * skip build tests for now

    * fix(images): Fix WASM files not being copied in dev

    * feat(images): Add quality presets

    * fix build tests running

    * Remove console.log

    * Add tests for getImage

    * Test local services

    * Test the content collections API

    * Add tests for quality

    * Skipping content collections test

    * feat(image): Add support for `~/assets` alias

    * test(image): Add tests for aliases in dev

    * Fix windows + content collections

    * test(image): Add tests for aliased images and images in Markdown

    * Fix markdown images being built

    * Should be posix join

    * Use the optimized image

    * fix test

    * Fixes windows smoke

    * fix(image): Nits

    * feat(images): Add automatic update for `env.d.ts` when experimental images are enabled

    * fix(images): Revert env.d.ts change if the user opted-out of the experimental image support

    * chore: remove bad image example project

    * feat(image): Rename `experimental.images` to `experimental.assets`

    * fix(images): Remove unused code in MDX integration

    * chore: Remove unrelated change

    * fix(images): Remove export from astro/components

    * Fix, esm import on Win

    * test(images): Add test for format

    * fix(images): Add `client-image.d.ts` to export map

    * chore: changeset

    * fix(images): Adjust with feedback, no more automatic refine, asset() -> image()

    * fix(images): Fix types

    * fix(images): Remove unnecessary spread

    * fix(images): Better types for parseUrl and transform

    * fix(images): Fix types

    * fix(images): Adjust from feedback

    * fix(images): Pass width and height through getHTMLAttributes even if they're not added by the uesr

    * fix(images): Recusirsively extract frontmatter assets

    * fix(images): Use a reduce instead

    * feat(images): Add support for data: URIs

    * chore: changeset

    * docs(images): Misc docs fixes

    * Update .changeset/gold-rocks-cry.md

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    * Update .changeset/gold-rocks-cry.md

    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

    * Update packages/astro/src/@types/astro.ts

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    * Update packages/astro/src/assets/services/service.ts

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    * Update packages/astro/src/assets/services/service.ts

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    * Update packages/astro/src/assets/services/service.ts

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    * Update packages/astro/src/assets/types.ts

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    * Update packages/astro/src/assets/types.ts

    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>

    ---------

    Co-authored-by: Matthew Phillips <matthew@skypack.dev>
    Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
    Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
    Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

commit 377530a810
Author: ematipico <ematipico@users.noreply.github.com>
Date:   Tue Mar 7 13:43:21 2023 +0000

    [ci] format

commit fe88f89512
Author: Emanuele Stoppa <my.burning@gmail.com>
Date:   Tue Mar 7 13:41:24 2023 +0000

    chore: use directive `@ts-expect-error` instead of `@ts-ignore` (#6429)

commit e1858e6334
Author: ematipico <ematipico@users.noreply.github.com>
Date:   Tue Mar 7 06:57:52 2023 +0000

    [ci] format

commit 75921b3cd9
Author: Emanuele Stoppa <my.burning@gmail.com>
Date:   Tue Mar 7 06:55:41 2023 +0000

    feat(cli): add `--watch` to `astro check` command (#6356)

    * feat(cli): add `--watch` to `astro check` command

    * chore: refactor in a leaner way, logic not changed

    * chore: lint

    * chore: revert changes in sync command

    * chore: tweak server settings

    * test: add one test case

    * chore: increase timeout

    * test: predictable testing

    * chore: add changeset

    * chore: code suggestions

    * code suggestions

    * chore: use directly `chokidar`

    * chore: tweak code

    * fix: open documents first

    * chore: disable test

    * chore: code suggestions

    * chore: code suggestions

    * Apply suggestions from code review

    Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>

    * code suggestions

    * chore: rebase

    ---------

    Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>

commit 1291afc09d
Author: Bjorn Lu <bjornlu.dev@gmail.com>
Date:   Tue Mar 7 14:50:34 2023 +0800

    Fix changeset run (#6442)

commit 00a0af7ed4
Author: Bjorn Lu <bjornlu.dev@gmail.com>
Date:   Tue Mar 7 10:52:47 2023 +0800

    Move benchmark package and update changeset config (#6433)

commit af05a4fa46
Author: Nate Moore <natemoo-re@users.noreply.github.com>
Date:   Mon Mar 6 14:06:33 2023 -0600

    Update README.md (#6437)

    * Update README.md

    * Update README.md

commit 8ebf4b7481
Author: Nate Moore <natemoo-re@users.noreply.github.com>
Date:   Mon Mar 6 14:03:33 2023 -0600

    chore: update branding assets (#6436)

commit afbbc4d5bf
Author: Erika <3019731+Princesseuh@users.noreply.github.com>
Date:   Mon Mar 6 19:57:16 2023 +0100

    Update compilation target for Node 16 (#6213)

    * config(esbuild): Update esbuild target to node16

    * config(package): Update root package.json node engine

    * config(tsconfig): Update all the tsconfigs module and targets

    * chore: changeset

    * chore: remove unneeded file

commit 18acae3edc
Author: Sarah Rainsberger <sarah@rainsberger.ca>
Date:   Mon Mar 6 14:36:40 2023 -0400

    [error docs] update link for client-side scripts (#6423)

commit 8b49d1781d
Author: ematipico <ematipico@users.noreply.github.com>
Date:   Mon Mar 6 17:03:18 2023 +0000

    [ci] format

commit a4a74ab70c
Author: Emanuele Stoppa <my.burning@gmail.com>
Date:   Mon Mar 6 16:58:56 2023 +0000

    feat(cli): add help flags to various commands (#6394)

    Co-authored-by: Happydev <81974850+MoustaphaDev@users.noreply.github.com>

commit b087b83fe2
Author: Dennis Morello <dennismorello@gmail.com>
Date:   Mon Mar 6 17:38:42 2023 +0100

    Add getStaticPaths type helpers to infer params and props (#6150)

    * feat(astro): add InferGetStaticParamsType and InferGetStaticPropsType type helpers

    * chore(astro): added changeset

commit 19fe4cb629
Author: Houston (Bot) <108291165+astrobot-houston@users.noreply.github.com>
Date:   Mon Mar 6 08:30:38 2023 -0800

    [ci] release (#6414)

    Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
bholmesdev 2023-03-07 12:59:10 -05:00
parent eac40e3494
commit fbe129bf95
249 changed files with 15974 additions and 598 deletions

View file

@ -1,5 +0,0 @@
---
'@astrojs/webapi': patch
---
Use undici's FormData instead of a polyfill

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Add getStaticPaths type helpers to infer params and props

View file

@ -0,0 +1,15 @@
---
'astro': minor
'@astrojs/mdx': minor
'@astrojs/markdown-remark': minor
---
Add a new experimental flag (`experimental.assets`) to enable our new core Assets story.
This unlocks a few features:
- A new built-in image component and JavaScript API to transform and optimize images.
- Relative images with automatic optimization in Markdown.
- Support for validating assets using content collections.
- and more!
See [Assets (Experimental)](https://docs.astro.build/en/guides/assets/) on our docs site for more information on how to use this feature!

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Expose the manifest to plugins via the astro:ssr-manifest virtual module

View file

@ -1,5 +0,0 @@
---
'astro': patch
---
Remove redundant comments when `astro add` update `astro.config.mjs`

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Add `--help` to various commands: `check`, `sync`, `dev`, `preview`, and `build`

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Added a new `--watch` flag to the command `astro check`

View file

@ -0,0 +1,30 @@
---
'astro': minor
'@astrojs/prism': minor
'@astrojs/rss': minor
'create-astro': minor
'@astrojs/alpinejs': minor
'@astrojs/cloudflare': minor
'@astrojs/deno': minor
'@astrojs/image': minor
'@astrojs/lit': minor
'@astrojs/mdx': minor
'@astrojs/netlify': minor
'@astrojs/node': minor
'@astrojs/partytown': minor
'@astrojs/preact': minor
'@astrojs/prefetch': minor
'@astrojs/react': minor
'@astrojs/sitemap': minor
'@astrojs/solid-js': minor
'@astrojs/svelte': minor
'@astrojs/tailwind': minor
'@astrojs/turbolinks': minor
'@astrojs/vercel': minor
'@astrojs/vue': minor
'@astrojs/markdown-remark': minor
'@astrojs/telemetry': minor
'@astrojs/webapi': minor
---
Updated compilation settings to disable downlevelling for Node 14

View file

@ -1,5 +0,0 @@
---
'astro': patch
---
Prevent `?inline` and `?raw` css query suffixes from injecting style tags in development

BIN
.github/assets/banner.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View file

@ -1,4 +1,4 @@
![The Astro logo.](assets/social/banner-minimal.png 'The Astro logo.') ![Build the web you want](.github/assets/banner.png 'Build the web you want')
<p align="center"> <p align="center">
<br/> <br/>

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<path fill="#FF5D01" fill-rule="evenodd" d="M4.428 14.135c-.312.997-.467 1.495-.429 1.968.036.417.178.818.413 1.165.266.392.7.682 1.57 1.261l3.839 2.56c.775.516 1.163.775 1.582.876.37.09.757.09 1.128.003.42-.1.81-.357 1.588-.87L18 18.533c.878-.581 1.318-.87 1.588-1.266A2.43 2.43 0 0020 16.095c.039-.477-.122-.978-.44-1.982l-3.4-10.7c-.165-.52-.247-.779-.405-.972a1.296 1.296 0 00-.523-.382c-.23-.092-.502-.092-1.047-.092H9.754c-.547 0-.822 0-1.053.094a1.29 1.29 0 00-.524.384c-.158.194-.239.455-.401.977L4.428 14.136zm9.93-9.897c.13.162.198.383.331.82l2.919 9.588a12.146 12.146 0 00-3.49-1.18l-1.9-6.423a.248.248 0 00-.474 0l-1.878 6.418a12.128 12.128 0 00-3.505 1.184l2.934-9.588c.133-.439.2-.658.331-.82.116-.143.267-.254.437-.323.194-.078.423-.078.881-.078h2.095c.458 0 .688 0 .882.078.17.069.32.18.437.324zm-2.263 11.557c1.106 0 2.067-.28 2.548-.692a2.1 2.1 0 01-.155 1.636c-.308.57-.755.853-1.158 1.108-.506.321-.941.598-.941 1.34 0 .245.056.479.157.686a1.868 1.868 0 01-1.151-1.725v-.046c0-.512.001-1.143-.722-1.143a.765.765 0 00-.766.764c-.812-.813-.741-1.983-.741-1.983 0-.239.024-.611.131-.936.302.568 1.44.99 2.798.99z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 32 32">
<path fill="url(#a)" fill-rule="evenodd" d="m20.4 2.4.6 1.5 5.4 17.8c-2-1-4.2-1.8-6.5-2.2l-3.5-12a.5.5 0 0 0-.9 0l-3.5 12c-2.3.4-4.4 1.1-6.5 2.2L11 3.9c.2-.8.4-1.2.6-1.5.2-.3.5-.5.8-.6.4-.2.8-.2 1.6-.2h4c.8 0 1.2 0 1.6.2.3 0 .6.3.8.6Z" clip-rule="evenodd"/>
<path fill="#FF5D01" fill-rule="evenodd" d="M21 22.5c-.9.8-2.6 1.3-4.7 1.3-2.5 0-4.6-.8-5.2-1.8-.2.6-.2 1.3-.2 1.7 0 0-.2 2.2 1.3 3.7 0-.8.7-1.4 1.5-1.4 1.3 0 1.3 1 1.3 2v.2c0 1.4.9 2.6 2.1 3.2a3 3 0 0 1-.3-1.3c0-1.4.9-1.9 1.8-2.5.7-.5 1.6-1 2.1-2a3.9 3.9 0 0 0 .3-3Z" clip-rule="evenodd"/>
<path fill="url(#b)" fill-rule="evenodd" d="M21 22.5c-.9.8-2.6 1.3-4.7 1.3-2.5 0-4.6-.8-5.2-1.8-.2.6-.2 1.3-.2 1.7 0 0-.2 2.2 1.3 3.7 0-.8.7-1.4 1.5-1.4 1.3 0 1.3 1 1.3 2v.2c0 1.4.9 2.6 2.1 3.2a3 3 0 0 1-.3-1.3c0-1.4.9-1.9 1.8-2.5.7-.5 1.6-1 2.1-2a3.9 3.9 0 0 0 .3-3Z" clip-rule="evenodd"/>
<defs>
<linearGradient id="a" x1="22.1" x2="16" y1=".7" y2="21.7" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff"/>
<stop offset="1" stop-color="#F9FAFB"/>
</linearGradient>
<linearGradient id="b" x1="25" x2="19.8" y1="16.3" y2="27.4" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF1639"/>
<stop offset="1" stop-color="#FF1639" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

View file

@ -1,22 +0,0 @@
<svg width="192" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M131.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.258 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z"
fill="url(#paint0_linear)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
fill="#FF5D01" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
fill="url(#paint1_linear)" />
<defs>
<linearGradient id="paint0_linear" x1="144.599" y1="5.423" x2="95.791" y2="173.38" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff" />
<stop offset="1" stop-color="#F9FAFB" />
</linearGradient>
<linearGradient id="paint1_linear" x1="168.336" y1="130.49" x2="126.065" y2="218.982"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FF1639" />
<stop offset="1" stop-color="#FF1639" stop-opacity="0" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,22 +0,0 @@
<svg width="192" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M131.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.258 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z"
fill="url(#paint0_linear)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
fill="#FF5D01" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
fill="url(#paint1_linear)" />
<defs>
<linearGradient id="paint0_linear" x1="144.599" y1="5.423" x2="95.791" y2="173.38" gradientUnits="userSpaceOnUse">
<stop stop-color="#000014" />
<stop offset="1" stop-color="#150426" />
</linearGradient>
<linearGradient id="paint1_linear" x1="168.336" y1="130.49" x2="126.065" y2="218.982"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FF1639" />
<stop offset="1" stop-color="#FF1639" stop-opacity="0" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -103,7 +103,7 @@ function printResult(result) {
} }
/** /**
* Simple fetch utility to get the render time sent by `@astrojs/timer` in plain text * Simple fetch utility to get the render time sent by `@benchmark/timer` in plain text
* @param {string} url * @param {string} url
* @returns {Promise<number>} * @returns {Promise<number>}
*/ */

View file

@ -74,7 +74,7 @@ export async function run(projectDir) {
new URL('./astro.config.js', projectDir), new URL('./astro.config.js', projectDir),
`\ `\
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import timer from '@astrojs/timer'; import timer from '@benchmark/timer';
import mdx from '@astrojs/mdx'; import mdx from '@astrojs/mdx';
export default defineConfig({ export default defineConfig({

View file

@ -9,7 +9,7 @@
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*", "@astrojs/mdx": "workspace:*",
"@astrojs/node": "workspace:*", "@astrojs/node": "workspace:*",
"@astrojs/timer": "workspace:*", "@benchmark/timer": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"autocannon": "^7.10.0", "autocannon": "^7.10.0",
"execa": "^6.1.0", "execa": "^6.1.0",

View file

@ -1,3 +1,3 @@
# @astrojs/timer # @benchmark/timer
Like `@astrojs/node`, but returns the rendered time in milliseconds for the page instead of the page content itself. This is used for internal benchmarks only. Like `@astrojs/node`, but returns the rendered time in milliseconds for the page instead of the page content itself. This is used for internal benchmarks only.

View file

@ -1,5 +1,5 @@
{ {
"name": "@astrojs/timer", "name": "@benchmark/timer",
"description": "Preview server for benchmark", "description": "Preview server for benchmark",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
@ -7,16 +7,10 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"author": "withastro", "author": "withastro",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/integrations/timer"
},
"keywords": [ "keywords": [
"withastro", "withastro",
"astro-adapter" "astro-adapter"
], ],
"bugs": "https://github.com/withastro/astro/issues",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./server.js": "./dist/server.js", "./server.js": "./dist/server.js",
@ -33,7 +27,7 @@
"server-destroy": "^1.0.1" "server-destroy": "^1.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^2.0.17" "astro": "workspace:^2.0.18"
}, },
"devDependencies": { "devDependencies": {
"@types/server-destroy": "^1.0.1", "@types/server-destroy": "^1.0.1",

View file

@ -2,22 +2,22 @@ import type { AstroAdapter, AstroIntegration } from 'astro';
export function getAdapter(): AstroAdapter { export function getAdapter(): AstroAdapter {
return { return {
name: '@astrojs/timer', name: '@benchmark/timer',
serverEntrypoint: '@astrojs/timer/server.js', serverEntrypoint: '@benchmark/timer/server.js',
previewEntrypoint: '@astrojs/timer/preview.js', previewEntrypoint: '@benchmark/timer/preview.js',
exports: ['handler'], exports: ['handler'],
}; };
} }
export default function createIntegration(): AstroIntegration { export default function createIntegration(): AstroIntegration {
return { return {
name: '@astrojs/timer', name: '@benchmark/timer',
hooks: { hooks: {
'astro:config:setup': ({ updateConfig }) => { 'astro:config:setup': ({ updateConfig }) => {
updateConfig({ updateConfig({
vite: { vite: {
ssr: { ssr: {
noExternal: ['@astrojs/timer'], noExternal: ['@benchmark/timer'],
}, },
}, },
}); });
@ -26,7 +26,8 @@ export default function createIntegration(): AstroIntegration {
setAdapter(getAdapter()); setAdapter(getAdapter());
if (config.output === 'static') { if (config.output === 'static') {
console.warn(`[@astrojs/timer] \`output: "server"\` is required to use this adapter.`); // eslint-disable-next-line no-console
console.warn(`[@benchmark/timer] \`output: "server"\` is required to use this adapter.`);
} }
}, },
}, },

View file

@ -11,6 +11,6 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"@astrojs/mdx": "^0.17.2", "@astrojs/mdx": "^0.17.2",
"@astrojs/rss": "^2.1.1", "@astrojs/rss": "^2.1.1",
"@astrojs/sitemap": "^1.1.0" "@astrojs/sitemap": "^1.1.0"

View file

@ -15,7 +15,7 @@
], ],
"scripts": {}, "scripts": {},
"devDependencies": { "devDependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "^2.0.0-beta.0" "astro": "^2.0.0-beta.0"

View file

@ -10,7 +10,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/deno": "^4.0.2" "@astrojs/deno": "^4.0.2"

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"preact": "^10.7.3", "preact": "^10.7.3",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",

View file

@ -6,7 +6,7 @@ export const SITE = {
export const OPEN_GRAPH = { export const OPEN_GRAPH = {
image: { image: {
src: 'https://github.com/withastro/astro/blob/main/assets/social/banner-minimal.png?raw=true', src: 'https://github.com/withastro/astro/blob/main/.github/assets/banner.png?raw=true',
alt: alt:
'astro logo on a starry expanse of space,' + 'astro logo on a starry expanse of space,' +
' with a purple saturn-like planet floating in the right foreground', ' with a purple saturn-like planet floating in the right foreground',

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"alpinejs": "^3.10.2", "alpinejs": "^3.10.2",
"@astrojs/alpinejs": "^0.1.3", "@astrojs/alpinejs": "^0.1.3",
"@types/alpinejs": "^3.7.0" "@types/alpinejs": "^3.7.0"

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"lit": "^2.2.5", "lit": "^2.2.5",
"@astrojs/lit": "^1.2.1", "@astrojs/lit": "^1.2.1",
"@webcomponents/template-shadowroot": "^0.1.0" "@webcomponents/template-shadowroot": "^0.1.0"

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"preact": "^10.7.3", "preact": "^10.7.3",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"preact": "^10.7.3", "preact": "^10.7.3",
"@astrojs/preact": "^2.0.3", "@astrojs/preact": "^2.0.3",
"@preact/signals": "^1.1.0" "@preact/signals": "^1.1.0"

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"@astrojs/react": "^2.0.2", "@astrojs/react": "^2.0.2",

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"solid-js": "^1.4.3", "solid-js": "^1.4.3",
"@astrojs/solid-js": "^2.0.2" "@astrojs/solid-js": "^2.0.2"
} }

View file

@ -13,6 +13,6 @@
"dependencies": { "dependencies": {
"svelte": "^3.48.0", "svelte": "^3.48.0",
"@astrojs/svelte": "^2.0.2", "@astrojs/svelte": "^2.0.2",
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"vue": "^3.2.37", "vue": "^3.2.37",
"@astrojs/vue": "^2.0.1" "@astrojs/vue": "^2.0.1"
} }

View file

@ -12,6 +12,6 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^5.0.4", "@astrojs/node": "^5.0.4",
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -15,7 +15,7 @@
], ],
"scripts": {}, "scripts": {},
"devDependencies": { "devDependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
}, },
"peerDependencies": { "peerDependencies": {
"astro": "^2.0.0-beta.0" "astro": "^2.0.0-beta.0"

View file

@ -11,6 +11,6 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -11,6 +11,6 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -11,6 +11,6 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -12,7 +12,7 @@
"server": "node dist/server/entry.mjs" "server": "node dist/server/entry.mjs"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"svelte": "^3.48.0", "svelte": "^3.48.0",
"@astrojs/svelte": "^2.0.2", "@astrojs/svelte": "^2.0.2",
"@astrojs/node": "^5.0.4", "@astrojs/node": "^5.0.4",

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"@astrojs/markdown-remark": "^2.0.1", "@astrojs/markdown-remark": "^2.0.1",
"hast-util-select": "5.0.1", "hast-util-select": "5.0.1",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",

View file

@ -11,6 +11,6 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17" "astro": "^2.0.18"
} }
} }

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"preact": "^10.6.5", "preact": "^10.6.5",
"@astrojs/preact": "^2.0.3", "@astrojs/preact": "^2.0.3",
"@astrojs/mdx": "^0.17.2" "@astrojs/mdx": "^0.17.2"

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"preact": "^10.7.3", "preact": "^10.7.3",
"@astrojs/preact": "^2.0.3", "@astrojs/preact": "^2.0.3",
"nanostores": "^0.5.12", "nanostores": "^0.5.12",

View file

@ -14,7 +14,7 @@
"@astrojs/mdx": "^0.17.2", "@astrojs/mdx": "^0.17.2",
"@astrojs/tailwind": "^3.0.1", "@astrojs/tailwind": "^3.0.1",
"@types/canvas-confetti": "^1.4.3", "@types/canvas-confetti": "^1.4.3",
"astro": "^2.0.17", "astro": "^2.0.18",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"canvas-confetti": "^1.5.1", "canvas-confetti": "^1.5.1",
"postcss": "^8.4.14", "postcss": "^8.4.14",

View file

@ -11,7 +11,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"vite-plugin-pwa": "0.11.11", "vite-plugin-pwa": "0.11.11",
"workbox-window": "^6.5.3" "workbox-window": "^6.5.3"
} }

View file

@ -12,7 +12,7 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"astro": "^2.0.17", "astro": "^2.0.18",
"vitest": "^0.20.3" "vitest": "^0.20.3"
} }
} }

View file

@ -8,10 +8,10 @@
}, },
"scripts": { "scripts": {
"release": "pnpm run build && changeset publish", "release": "pnpm run build && changeset publish",
"build": "turbo run build --output-logs=new-only --no-deps --filter=astro --filter=create-astro --filter=\"@astrojs/*\"", "build": "turbo run build --output-logs=new-only --no-deps --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --filter=\"@benchmark/*\"",
"build:ci": "turbo run build:ci --output-logs=new-only --no-deps --filter=astro --filter=create-astro --filter=\"@astrojs/*\"", "build:ci": "turbo run build:ci --output-logs=new-only --no-deps --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --filter=\"@benchmark/*\"",
"build:examples": "turbo run build --filter=\"@example/*\"", "build:examples": "turbo run build --filter=\"@example/*\"",
"dev": "turbo run dev --no-deps --no-cache --parallel --filter=astro --filter=create-astro --filter=\"@astrojs/*\"", "dev": "turbo run dev --no-deps --no-cache --parallel --filter=astro --filter=create-astro --filter=\"@astrojs/*\" --filter=\"@benchmark/*\"",
"format": "pnpm run format:code", "format": "pnpm run format:code",
"format:ci": "pnpm run format:imports && pnpm run format:code", "format:ci": "pnpm run format:imports && pnpm run format:code",
"format:code": "prettier -w . --cache --plugin-search-dir=.", "format:code": "prettier -w . --cache --plugin-search-dir=.",
@ -40,7 +40,7 @@
"packages/astro/test/fixtures/static build/pkg" "packages/astro/test/fixtures/static build/pkg"
], ],
"engines": { "engines": {
"node": "^14.18.0 || >=16.12.0", "node": ">=16.12.0",
"pnpm": ">=7.9.5" "pnpm": ">=7.9.5"
}, },
"packageManager": "pnpm@7.12.2", "packageManager": "pnpm@7.12.2",

View file

@ -3,8 +3,8 @@
"include": ["src"], "include": ["src"],
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"target": "ES2020", "target": "ES2021",
"module": "ES2020", "module": "ES2022",
"outDir": "./dist" "outDir": "./dist"
} }
} }

View file

@ -3,9 +3,9 @@
"include": ["src"], "include": ["src"],
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"module": "ES2020", "module": "ES2022",
"outDir": "./dist", "outDir": "./dist",
"target": "ES2020", "target": "ES2021",
"strictNullChecks": true "strictNullChecks": true
} }
} }

View file

@ -1,5 +1,16 @@
# astro # astro
## 2.0.18
### Patch Changes
- [#6412](https://github.com/withastro/astro/pull/6412) [`cd8469947`](https://github.com/withastro/astro/commit/cd8469947bb63b4233f3459614c5210feac1da96) Thanks [@liruifengv](https://github.com/liruifengv)! - Remove redundant comments when `astro add` update `astro.config.mjs`
- [#6426](https://github.com/withastro/astro/pull/6426) [`e0844852d`](https://github.com/withastro/astro/commit/e0844852d31d0f5680f2710aaa84e3e808aeb88d) Thanks [@MoustaphaDev](https://github.com/MoustaphaDev)! - Prevent `?inline` and `?raw` css query suffixes from injecting style tags in development
- Updated dependencies [[`0abd1d3e4`](https://github.com/withastro/astro/commit/0abd1d3e42cf7bf5efb8c41f37e011b933fb0629)]:
- @astrojs/webapi@2.0.3
## 2.0.17 ## 2.0.17
### Patch Changes ### Patch Changes

View file

@ -1,10 +1,10 @@
<br/> <br/>
<p align="center"> <p align="center">
<img src="../../assets/social/banner-minimal.png" alt="Astro logo"> <img src="../../.github/assets/banner.png" alt="Build the web you want">
<br/><br/> <br/><br/>
<a href="https://astro.build">Astro</a> is a website build tool for the modern web &mdash; <a href="https://astro.build">Astro</a> is the all-in-one web framework designed for speed.
<br/> <br/>
powerful developer experience meets lightweight output. Pull your content from anywhere and deploy everywhere, all powered by your favorite UI components and libraries.
<br/><br/> <br/><br/>
</p> </p>

View file

@ -1,5 +1,28 @@
/// <reference path="./import-meta.d.ts" /> /// <reference path="./import-meta.d.ts" />
declare module 'astro:assets' {
// Exporting things one by one is a bit cumbersome, not sure if there's a better way - erika, 2023-02-03
type AstroAssets = {
getImage: typeof import('./dist/assets/index.js').getImage;
Image: typeof import('./components/Image.astro').default;
};
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] };
type ImgAttributes = WithRequired<
Omit<import('./types').HTMLAttributes<'img'>, 'src' | 'width' | 'height'>,
'alt'
>;
export type LocalImageProps = Simplify<
import('./dist/assets/types.js').LocalImageProps<ImgAttributes>
>;
export type RemoteImageProps = Simplify<
import('./dist/assets/types.js').RemoteImageProps<ImgAttributes>
>;
export const { getImage, Image }: AstroAssets;
}
type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>; type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
interface ExportedMarkdownModuleEntities { interface ExportedMarkdownModuleEntities {
frontmatter: MD['frontmatter']; frontmatter: MD['frontmatter'];
@ -117,6 +140,10 @@ declare module '*.mdx' {
export default load; export default load;
} }
declare module 'astro:ssr-manifest' {
export const manifest: import('./dist/@types/astro').SSRManifest;
}
// Everything below are Vite's types (apart from image types, which are in `client.d.ts`) // Everything below are Vite's types (apart from image types, which are in `client.d.ts`)
// CSS modules // CSS modules

48
packages/astro/client-image.d.ts vendored Normal file
View file

@ -0,0 +1,48 @@
/// <reference path="./client-base.d.ts" />
type InputFormat = 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp';
interface ImageMetadata {
src: string;
width: number;
height: number;
format: InputFormat;
}
// images
declare module '*.avif' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.gif' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.heic' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.heif' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.jpeg' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.jpg' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.png' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.tiff' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.webp' {
const metadata: ImageMetadata;
export default metadata;
}

View file

@ -0,0 +1,28 @@
---
import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
// The TypeScript diagnostic for JSX props uses the last member of the union to suggest props, so it would be better for
// LocalImageProps to be last. Unfortunately, when we do this the error messages that remote images get are complete nonsense
// Not 100% sure how to fix this, seems to be a TypeScript issue. Unfortunate.
type Props = LocalImageProps | RemoteImageProps;
const props = Astro.props;
if (props.alt === undefined || props.alt === null) {
throw new AstroError(AstroErrorData.ImageMissingAlt);
}
// As a convenience, allow width and height to be string with a number in them, to match HTML's native `img`.
if (typeof props.width === 'string') {
props.width = parseInt(props.width);
}
if (typeof props.height === 'string') {
props.height = parseInt(props.height);
}
const image = await getImage(props);
---
<img src={image.src} {...image.attributes} />

View file

@ -1,6 +1,6 @@
{ {
"name": "astro", "name": "astro",
"version": "2.0.17", "version": "2.0.18",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module", "type": "module",
"author": "withastro", "author": "withastro",
@ -32,6 +32,7 @@
"./types": "./types.d.ts", "./types": "./types.d.ts",
"./client": "./client.d.ts", "./client": "./client.d.ts",
"./client-base": "./client-base.d.ts", "./client-base": "./client-base.d.ts",
"./client-image": "./client-image.d.ts",
"./import-meta": "./import-meta.d.ts", "./import-meta": "./import-meta.d.ts",
"./astro-jsx": "./astro-jsx.d.ts", "./astro-jsx": "./astro-jsx.d.ts",
"./tsconfigs/*.json": "./tsconfigs/*", "./tsconfigs/*.json": "./tsconfigs/*",
@ -47,6 +48,10 @@
"./client/*": "./dist/runtime/client/*", "./client/*": "./dist/runtime/client/*",
"./components": "./components/index.ts", "./components": "./components/index.ts",
"./components/*": "./components/*", "./components/*": "./components/*",
"./assets": "./dist/assets/index.js",
"./assets/image-endpoint": "./dist/assets/image-endpoint.js",
"./assets/services/sharp": "./dist/assets/services/sharp.js",
"./assets/services/squoosh": "./dist/assets/services/squoosh.js",
"./content/internal": "./dist/content/internal.js", "./content/internal": "./dist/content/internal.js",
"./debug": "./components/Debug.astro", "./debug": "./components/Debug.astro",
"./internal/*": "./dist/runtime/server/*", "./internal/*": "./dist/runtime/server/*",
@ -77,6 +82,7 @@
"env.d.ts", "env.d.ts",
"client.d.ts", "client.d.ts",
"client-base.d.ts", "client-base.d.ts",
"client-image.d.ts",
"import-meta.d.ts", "import-meta.d.ts",
"astro-jsx.d.ts", "astro-jsx.d.ts",
"types.d.ts", "types.d.ts",
@ -86,10 +92,10 @@
], ],
"scripts": { "scripts": {
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"", "prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"",
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc", "build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc && pnpm run postbuild",
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\"", "build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"", "dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"", "postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
"test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js", "test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js",
"test:unit:match": "mocha --exit --timeout 30000 ./test/units/**/*.test.js -g", "test:unit:match": "mocha --exit --timeout 30000 ./test/units/**/*.test.js -g",
"test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js", "test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
@ -102,7 +108,7 @@
"@astrojs/language-server": "^0.28.3", "@astrojs/language-server": "^0.28.3",
"@astrojs/markdown-remark": "^2.0.1", "@astrojs/markdown-remark": "^2.0.1",
"@astrojs/telemetry": "^2.0.1", "@astrojs/telemetry": "^2.0.1",
"@astrojs/webapi": "^2.0.2", "@astrojs/webapi": "^2.0.3",
"@babel/core": "^7.18.2", "@babel/core": "^7.18.2",
"@babel/generator": "^7.18.2", "@babel/generator": "^7.18.2",
"@babel/parser": "^7.18.4", "@babel/parser": "^7.18.4",
@ -113,6 +119,7 @@
"@types/yargs-parser": "^21.0.0", "@types/yargs-parser": "^21.0.0",
"acorn": "^8.8.1", "acorn": "^8.8.1",
"boxen": "^6.2.1", "boxen": "^6.2.1",
"chokidar": "^3.5.3",
"ci-info": "^3.3.1", "ci-info": "^3.3.1",
"common-ancestor-path": "^1.0.1", "common-ancestor-path": "^1.0.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
@ -127,6 +134,7 @@
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"html-escaper": "^3.0.3", "html-escaper": "^3.0.3",
"image-size": "^1.0.2",
"kleur": "^4.1.4", "kleur": "^4.1.4",
"magic-string": "^0.27.0", "magic-string": "^0.27.0",
"mime": "^3.0.0", "mime": "^3.0.0",
@ -172,6 +180,7 @@
"@types/rimraf": "^3.0.2", "@types/rimraf": "^3.0.2",
"@types/send": "^0.17.1", "@types/send": "^0.17.1",
"@types/server-destroy": "^1.0.1", "@types/server-destroy": "^1.0.1",
"@types/sharp": "^0.31.1",
"@types/unist": "^2.0.6", "@types/unist": "^2.0.6",
"astro-scripts": "workspace:*", "astro-scripts": "workspace:*",
"chai": "^4.3.6", "chai": "^4.3.6",
@ -186,10 +195,19 @@
"remark-code-titles": "^0.1.2", "remark-code-titles": "^0.1.2",
"rollup": "^3.9.0", "rollup": "^3.9.0",
"sass": "^1.52.2", "sass": "^1.52.2",
"sharp": "^0.31.3",
"srcset-parse": "^1.1.0", "srcset-parse": "^1.1.0",
"undici": "^5.20.0", "undici": "^5.20.0",
"unified": "^10.1.2" "unified": "^10.1.2"
}, },
"peerDependencies": {
"sharp": "^0.31.3"
},
"peerDependenciesMeta": {
"sharp": {
"optional": true
}
},
"engines": { "engines": {
"node": ">=16.12.0", "node": ">=16.12.0",
"npm": ">=6.14.0" "npm": ">=6.14.0"

View file

@ -18,6 +18,7 @@ import type { PageBuildData } from '../core/build/types';
import type { AstroConfigSchema } from '../core/config'; import type { AstroConfigSchema } from '../core/config';
import type { AstroTimer } from '../core/config/timer'; import type { AstroTimer } from '../core/config/timer';
import type { AstroCookies } from '../core/cookies'; import type { AstroCookies } from '../core/cookies';
import type { LogOptions } from '../core/logger/core';
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server'; import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
export type { export type {
@ -28,6 +29,8 @@ export type {
RemarkPlugins, RemarkPlugins,
ShikiConfig, ShikiConfig,
} from '@astrojs/markdown-remark'; } from '@astrojs/markdown-remark';
export type { ExternalImageService, LocalImageService } from '../assets/services/service';
export type { ImageTransform } from '../assets/types';
export type { SSRManifest } from '../core/app/types'; export type { SSRManifest } from '../core/app/types';
export type { AstroCookies } from '../core/cookies'; export type { AstroCookies } from '../core/cookies';
@ -85,6 +88,7 @@ export interface CLIFlags {
port?: number; port?: number;
config?: string; config?: string;
drafts?: boolean; drafts?: boolean;
experimentalAssets?: boolean;
} }
export interface BuildConfig { export interface BuildConfig {
@ -696,6 +700,16 @@ export interface AstroUserConfig {
server?: ServerConfig | ((options: { command: 'dev' | 'preview' }) => ServerConfig); server?: ServerConfig | ((options: { command: 'dev' | 'preview' }) => ServerConfig);
/**
* @docs
* @kind heading
* @name Image options
*/
image?: {
// eslint-disable-next-line @typescript-eslint/ban-types
service: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
};
/** /**
* @docs * @docs
* @kind heading * @kind heading
@ -918,7 +932,27 @@ export interface AstroUserConfig {
* Astro offers experimental flags to give users early access to new features. * Astro offers experimental flags to give users early access to new features.
* These flags are not guaranteed to be stable. * These flags are not guaranteed to be stable.
*/ */
experimental?: object; experimental?: {
/**
* @docs
* @name experimental.assets
* @type {boolean}
* @default `false`
* @version 2.1.0
* @description
* Enable experimental support for optimizing and resizing images. With this enabled, a new `astro:assets` module will be exposed.
*
* To enable this feature, set `experimental.assets` to `true` in your Astro config:
*
* ```js
* {
* experimental: {
* assets: true,
* },
* }
*/
assets?: boolean;
};
// Legacy options to be removed // Legacy options to be removed
@ -1115,6 +1149,60 @@ export type GetStaticPaths = (
| GetStaticPathsResult | GetStaticPathsResult
| GetStaticPathsResult[]; | GetStaticPathsResult[];
/**
* Infers the shape of the `params` property returned by `getStaticPaths()`.
*
* @example
* ```ts
* export async function getStaticPaths() {
* return results.map((entry) => ({
* params: { slug: entry.slug },
* }));
* }
*
* type Params = InferGetStaticParamsType<typeof getStaticPaths>;
* // ^? { slug: string; }
*
* const { slug } = Astro.params as Params;
* ```
*/
export type InferGetStaticParamsType<T> = T extends () => Promise<infer R>
? R extends Array<infer U>
? U extends { params: infer P }
? P
: never
: never
: never;
/**
* Infers the shape of the `props` property returned by `getStaticPaths()`.
*
* @example
* ```ts
* export async function getStaticPaths() {
* return results.map((entry) => ({
* params: { slug: entry.slug },
* props: {
* propA: true,
* propB: 42
* },
* }));
* }
*
* type Props = InferGetStaticPropsType<typeof getStaticPaths>;
* // ^? { propA: boolean; propB: number; }
*
* const { propA, propB } = Astro.props as Props;
* ```
*/
export type InferGetStaticPropsType<T> = T extends () => Promise<infer R>
? R extends Array<infer U>
? U extends { props: infer P }
? P
: never
: never
: never;
export interface HydrateOptions { export interface HydrateOptions {
name: string; name: string;
value?: string; value?: string;
@ -1399,6 +1487,11 @@ export interface AstroIntegration {
}; };
} }
export interface AstroPluginOptions {
settings: AstroSettings;
logging: LogOptions;
}
export type RouteType = 'page' | 'endpoint'; export type RouteType = 'page' | 'endpoint';
export interface RoutePart { export interface RoutePart {

View file

@ -0,0 +1,3 @@
# assets
This directory powers the Assets story in Astro. Notably, it contains all the code related to optimizing images and serving them in the different modes Astro can run in (SSG, SSR, dev, build etc).

View file

@ -0,0 +1,14 @@
export const VIRTUAL_MODULE_ID = 'astro:assets';
export const VIRTUAL_SERVICE_ID = 'virtual:image-service';
export const VALID_INPUT_FORMATS = [
'heic',
'heif',
'avif',
'jpeg',
'jpg',
'png',
'tiff',
'webp',
'gif',
] as const;
export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg'] as const;

View file

@ -0,0 +1,66 @@
import mime from 'mime';
import type { APIRoute } from '../@types/astro.js';
import { isRemotePath } from '../core/path.js';
import { getConfiguredImageService } from './internal.js';
import { isLocalService } from './services/service.js';
import { etag } from './utils/etag.js';
async function loadRemoteImage(src: URL) {
try {
const res = await fetch(src);
if (!res.ok) {
return undefined;
}
return Buffer.from(await res.arrayBuffer());
} catch (err: unknown) {
return undefined;
}
}
/**
* Endpoint used in SSR to serve optimized images
*/
export const get: APIRoute = async ({ request }) => {
try {
const imageService = await getConfiguredImageService();
if (!isLocalService(imageService)) {
throw new Error('Configured image service is not a local service');
}
const url = new URL(request.url);
const transform = await imageService.parseURL(url);
if (!transform || !transform.src) {
throw new Error('Incorrect transform returned by `parseURL`');
}
let inputBuffer: Buffer | undefined = undefined;
// TODO: handle config subpaths?
const sourceUrl = isRemotePath(transform.src)
? new URL(transform.src)
: new URL(transform.src, url.origin);
inputBuffer = await loadRemoteImage(sourceUrl);
if (!inputBuffer) {
return new Response('Not Found', { status: 404 });
}
const { data, format } = await imageService.transform(inputBuffer, transform);
return new Response(data, {
status: 200,
headers: {
'Content-Type': mime.getType(format) || '',
'Cache-Control': 'public, max-age=31536000',
ETag: etag(data.toString()),
Date: new Date().toUTCString(),
},
});
} catch (err: unknown) {
return new Response(`Server Error: ${err}`, { status: 500 });
}
};

View file

@ -0,0 +1,4 @@
export { getConfiguredImageService, getImage } from './internal.js';
export { baseService } from './services/service.js';
export { type LocalImageProps, type RemoteImageProps } from './types.js';
export { imageMetadata } from './utils/metadata.js';

View file

@ -0,0 +1,117 @@
import fs from 'node:fs';
import { StaticBuildOptions } from '../core/build/types.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { ImageService, isLocalService, LocalImageService } from './services/service.js';
import type { ImageMetadata, ImageTransform } from './types.js';
export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
return typeof src === 'object';
}
export async function getConfiguredImageService(): Promise<ImageService> {
if (!globalThis.astroAsset.imageService) {
const { default: service }: { default: ImageService } = await import(
// @ts-expect-error
'virtual:image-service'
).catch((e) => {
const error = new AstroError(AstroErrorData.InvalidImageService);
(error as any).cause = e;
throw error;
});
globalThis.astroAsset.imageService = service;
return service;
}
return globalThis.astroAsset.imageService;
}
interface GetImageResult {
options: ImageTransform;
src: string;
attributes: Record<string, any>;
}
/**
* Get an optimized image and the necessary attributes to render it.
*
* **Example**
* ```astro
* ---
* import { getImage } from 'astro:assets';
* import originalImage from '../assets/image.png';
*
* const optimizedImage = await getImage({src: originalImage, width: 1280 })
* ---
* <img src={optimizedImage.src} {...optimizedImage.attributes} />
* ```
*
* This is functionally equivalent to using the `<Image />` component, as the component calls this function internally.
*/
export async function getImage(options: ImageTransform): Promise<GetImageResult> {
const service = await getConfiguredImageService();
let imageURL = service.getURL(options);
// In build and for local services, we need to collect the requested parameters so we can generate the final images
if (isLocalService(service) && globalThis.astroAsset.addStaticImage) {
imageURL = globalThis.astroAsset.addStaticImage(options);
}
return {
options,
src: imageURL,
attributes: service.getHTMLAttributes !== undefined ? service.getHTMLAttributes(options) : {},
};
}
export function getStaticImageList(): Iterable<[ImageTransform, string]> {
if (!globalThis?.astroAsset?.staticImages) {
return [];
}
return globalThis.astroAsset.staticImages?.entries();
}
interface GenerationData {
weight: {
before: number;
after: number;
};
}
export async function generateImage(
buildOpts: StaticBuildOptions,
options: ImageTransform,
filepath: string
): Promise<GenerationData | undefined> {
if (!isESMImportedImage(options.src)) {
return undefined;
}
const imageService = (await getConfiguredImageService()) as LocalImageService;
let serverRoot: URL, clientRoot: URL;
if (buildOpts.settings.config.output === 'server') {
serverRoot = buildOpts.settings.config.build.server;
clientRoot = buildOpts.settings.config.build.client;
} else {
serverRoot = buildOpts.settings.config.outDir;
clientRoot = buildOpts.settings.config.outDir;
}
const fileData = await fs.promises.readFile(new URL('.' + options.src.src, serverRoot));
const resultData = await imageService.transform(fileData, { ...options, src: options.src.src });
const finalFileURL = new URL('.' + filepath, clientRoot);
const finalFolderURL = new URL('./', finalFileURL);
await fs.promises.mkdir(finalFolderURL, { recursive: true });
await fs.promises.writeFile(finalFileURL, resultData.data);
return {
weight: {
before: Math.trunc(fileData.byteLength / 1024),
after: Math.trunc(resultData.data.byteLength / 1024),
},
};
}

View file

@ -0,0 +1,174 @@
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import { isRemotePath } from '../../core/path.js';
import { isESMImportedImage } from '../internal.js';
import { ImageTransform, OutputFormat } from '../types.js';
export type ImageService = LocalImageService | ExternalImageService;
export function isLocalService(service: ImageService | undefined): service is LocalImageService {
if (!service) {
return false;
}
return 'transform' in service;
}
export function parseQuality(quality: string): string | number {
let result = parseInt(quality);
if (Number.isNaN(result)) {
return quality;
}
return result;
}
interface SharedServiceProps {
/**
* Return the URL to the endpoint or URL your images are generated from.
*
* For a local service, your service should expose an endpoint handling the image requests, or use Astro's at `/_image`.
*
* For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image`
*
*/
getURL: (options: ImageTransform) => string;
/**
* Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
*
* For example, you might want to return the `width` and `height` to avoid CLS, or a particular `class` or `style`.
* In most cases, you'll want to return directly what your user supplied you, minus the attributes that were used to generate the image.
*/
getHTMLAttributes?: (options: ImageTransform) => Record<string, any>;
}
export type ExternalImageService = SharedServiceProps;
type LocalImageTransform = {
src: string;
[key: string]: any;
};
export interface LocalImageService extends SharedServiceProps {
/**
* Parse the requested parameters passed in the URL from `getURL` back into an object to be used later by `transform`
*
* In most cases, this will get query parameters using, for example, `params.get('width')` and return those.
*/
parseURL: (url: URL) => LocalImageTransform | undefined;
/**
* Performs the image transformations on the input image and returns both the binary data and
* final image format of the optimized image.
*/
transform: (
inputBuffer: Buffer,
transform: LocalImageTransform
) => Promise<{ data: Buffer; format: OutputFormat }>;
}
export type BaseServiceTransform = {
src: string;
width?: number;
height?: number;
format?: string | null;
quality?: string | null;
};
/**
* Basic local service using the included `_image` endpoint.
* This service intentionally does not implement `transform`.
*
* Example usage:
* ```ts
* const service = {
* getURL: baseService.getURL,
* parseURL: baseService.parseURL,
* getHTMLAttributes: baseService.getHTMLAttributes,
* async transform(inputBuffer, transformOptions) {...}
* }
* ```
*
* This service only supports the following properties: `width`, `height`, `format` and `quality`.
* Additionally, remote URLs are passed as-is.
*
*/
export const baseService: Omit<LocalImageService, 'transform'> = {
getHTMLAttributes(options) {
let targetWidth = options.width;
let targetHeight = options.height;
if (isESMImportedImage(options.src)) {
const aspectRatio = options.src.width / options.src.height;
// If we have a desired height and no width, calculate the target width automatically
if (targetHeight && !targetWidth) {
targetWidth = Math.round(targetHeight * aspectRatio);
} else if (targetWidth && !targetHeight) {
targetHeight = Math.round(targetWidth / aspectRatio);
} else {
targetWidth = options.src.width;
targetHeight = options.src.height;
}
}
const { src, width, height, format, quality, ...attributes } = options;
return {
...attributes,
width: targetWidth,
height: targetHeight,
loading: attributes.loading ?? 'lazy',
decoding: attributes.decoding ?? 'async',
};
},
getURL(options: ImageTransform) {
if (!isESMImportedImage(options.src)) {
// For non-ESM imported images, width and height are required to avoid CLS, as we can't infer them from the file
let missingDimension: 'width' | 'height' | 'both' | undefined;
if (!options.width && !options.height) {
missingDimension = 'both';
} else if (!options.width && options.height) {
missingDimension = 'width';
} else if (options.width && !options.height) {
missingDimension = 'height';
}
if (missingDimension) {
throw new AstroError({
...AstroErrorData.MissingImageDimension,
message: AstroErrorData.MissingImageDimension.message(missingDimension),
});
}
}
// Both our currently available local services don't handle remote images, so for them we can just return as is
if (!isESMImportedImage(options.src) && isRemotePath(options.src)) {
return options.src;
}
const searchParams = new URLSearchParams();
searchParams.append('href', isESMImportedImage(options.src) ? options.src.src : options.src);
options.width && searchParams.append('w', options.width.toString());
options.height && searchParams.append('h', options.height.toString());
options.quality && searchParams.append('q', options.quality.toString());
options.format && searchParams.append('f', options.format);
return '/_image?' + searchParams;
},
parseURL(url) {
const params = url.searchParams;
if (!params.has('href')) {
return undefined;
}
const transform: BaseServiceTransform = {
src: params.get('href')!,
width: params.has('w') ? parseInt(params.get('w')!) : undefined,
height: params.has('h') ? parseInt(params.get('h')!) : undefined,
format: params.get('f') as OutputFormat | null,
quality: params.get('q'),
};
return transform;
},
};

View file

@ -0,0 +1,72 @@
import type { FormatEnum } from 'sharp';
import type { ImageQualityPreset, OutputFormat } from '../types.js';
import { baseService, BaseServiceTransform, LocalImageService, parseQuality } from './service.js';
let sharp: typeof import('sharp');
const qualityTable: Record<ImageQualityPreset, number> = {
low: 25,
mid: 50,
high: 80,
max: 100,
};
async function loadSharp() {
let sharpImport: typeof import('sharp');
try {
sharpImport = (await import('sharp')).default;
} catch (e) {
throw new Error('Could not find Sharp. Please install Sharp manually into your project.');
}
return sharpImport;
}
const sharpService: LocalImageService = {
getURL: baseService.getURL,
parseURL: baseService.parseURL,
getHTMLAttributes: baseService.getHTMLAttributes,
async transform(inputBuffer, transformOptions) {
if (!sharp) sharp = await loadSharp();
const transform: BaseServiceTransform = transformOptions;
// If the user didn't specify a format, we'll default to `webp`. It offers the best ratio of compatibility / quality
// In the future, hopefully we can replace this with `avif`, alas, Edge. See https://caniuse.com/avif
if (!transform.format) {
transform.format = 'webp';
}
let result = sharp(inputBuffer, { failOnError: false, pages: -1 });
// Never resize using both width and height at the same time, prioritizing width.
if (transform.height && !transform.width) {
result.resize({ height: transform.height });
} else if (transform.width) {
result.resize({ width: transform.width });
}
if (transform.format) {
let quality: number | string | undefined = undefined;
if (transform.quality) {
const parsedQuality = parseQuality(transform.quality);
if (typeof parsedQuality === 'number') {
quality = parsedQuality;
} else {
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : undefined;
}
}
result.toFormat(transform.format as keyof FormatEnum, { quality: quality });
}
const { data, info } = await result.toBuffer({ resolveWithObject: true });
return {
data: data,
format: info.format as OutputFormat,
};
},
};
export default sharpService;

View file

@ -0,0 +1,72 @@
// TODO: Investigate removing this service once sharp lands WASM support, as libsquoosh is deprecated
import type { ImageQualityPreset, OutputFormat } from '../types.js';
import { baseService, BaseServiceTransform, LocalImageService, parseQuality } from './service.js';
import { processBuffer } from './vendor/squoosh/image-pool.js';
import type { Operation } from './vendor/squoosh/image.js';
const baseQuality = { low: 25, mid: 50, high: 80, max: 100 };
const qualityTable: Record<Exclude<OutputFormat, 'png'>, Record<ImageQualityPreset, number>> = {
avif: {
// Squoosh's AVIF encoder has a bit of a weird behavior where `62` is technically the maximum, and anything over is overkill
max: 62,
high: 45,
mid: 35,
low: 20,
},
jpeg: baseQuality,
jpg: baseQuality,
webp: baseQuality,
// Squoosh's PNG encoder does not support a quality setting, so we can skip that here
};
const service: LocalImageService = {
getURL: baseService.getURL,
parseURL: baseService.parseURL,
getHTMLAttributes: baseService.getHTMLAttributes,
async transform(inputBuffer, transformOptions) {
const transform: BaseServiceTransform = transformOptions as BaseServiceTransform;
let format = transform.format;
if (!format) {
format = 'webp';
}
const operations: Operation[] = [];
// Never resize using both width and height at the same time, prioritizing width.
if (transform.height && !transform.width) {
operations.push({
type: 'resize',
height: transform.height,
});
} else if (transform.width) {
operations.push({
type: 'resize',
width: transform.width,
});
}
let quality: number | string | undefined = undefined;
if (transform.quality) {
const parsedQuality = parseQuality(transform.quality);
if (typeof parsedQuality === 'number') {
quality = parsedQuality;
} else {
quality =
transform.quality in qualityTable[format]
? qualityTable[format][transform.quality]
: undefined;
}
}
const data = await processBuffer(inputBuffer, operations, format, quality);
return {
data: Buffer.from(data),
format: format,
};
},
};
export default service;

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,32 @@
// eslint-disable-next-line no-shadow
export const enum AVIFTune {
auto,
psnr,
ssim,
}
export interface EncodeOptions {
cqLevel: number
denoiseLevel: number
cqAlphaLevel: number
tileRowsLog2: number
tileColsLog2: number
speed: number
subsample: number
chromaDeltaQ: boolean
sharpness: number
tune: AVIFTune
}
export interface AVIFModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions
): Uint8Array
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<AVIFModule>
export default moduleFactory

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,373 @@
import { promises as fsp } from 'node:fs'
import { getModuleURL, instantiateEmscriptenWasm, pathify } from './emscripten-utils.js'
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData
}
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory
rotate(width: number, height: number, rotate: number): void
}
}
interface ResizeWithAspectParams {
input_width: number
input_height: number
target_width?: number
target_height?: number
}
export interface ResizeOptions {
width?: number
height?: number
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
premultiply: boolean
linearRGB: boolean
}
export interface RotateOptions {
numRotations: number
}
// MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from './mozjpeg/mozjpeg_enc'
// @ts-ignore
import mozEnc from './mozjpeg/mozjpeg_node_enc.js'
const mozEncWasm = new URL('./mozjpeg/mozjpeg_node_enc.wasm', getModuleURL(import.meta.url))
// @ts-ignore
import mozDec from './mozjpeg/mozjpeg_node_dec.js'
const mozDecWasm = new URL('./mozjpeg/mozjpeg_node_dec.wasm', getModuleURL(import.meta.url))
// WebP
import type { WebPModule as WebPEncodeModule } from './webp/webp_enc'
// @ts-ignore
import webpEnc from './webp/webp_node_enc.js'
const webpEncWasm = new URL('./webp/webp_node_enc.wasm', getModuleURL(import.meta.url))
// @ts-ignore
import webpDec from './webp/webp_node_dec.js'
const webpDecWasm = new URL('./webp/webp_node_dec.wasm', getModuleURL(import.meta.url))
// AVIF
import type { AVIFModule as AVIFEncodeModule } from './avif/avif_enc'
// @ts-ignore
import avifEnc from './avif/avif_node_enc.js'
const avifEncWasm = new URL('./avif/avif_node_enc.wasm', getModuleURL(import.meta.url))
// @ts-ignore
import avifDec from './avif/avif_node_dec.js'
const avifDecWasm = new URL('./avif/avif_node_dec.wasm', getModuleURL(import.meta.url))
// PNG
// @ts-ignore
import * as pngEncDec from './png/squoosh_png.js'
const pngEncDecWasm = new URL('./png/squoosh_png_bg.wasm', getModuleURL(import.meta.url))
const pngEncDecInit = () =>
pngEncDec.default(fsp.readFile(pathify(pngEncDecWasm.toString())))
// OxiPNG
// @ts-ignore
import * as oxipng from './png/squoosh_oxipng.js'
const oxipngWasm = new URL('./png/squoosh_oxipng_bg.wasm', getModuleURL(import.meta.url))
const oxipngInit = () => oxipng.default(fsp.readFile(pathify(oxipngWasm.toString())))
// Resize
// @ts-ignore
import * as resize from './resize/squoosh_resize.js'
const resizeWasm = new URL('./resize/squoosh_resize_bg.wasm', getModuleURL(import.meta.url))
const resizeInit = () => resize.default(fsp.readFile(pathify(resizeWasm.toString())))
// rotate
const rotateWasm = new URL('./rotate/rotate.wasm', getModuleURL(import.meta.url))
// Our decoders currently rely on a `ImageData` global.
import ImageData from './image_data.js'
(global as any).ImageData = ImageData
function resizeNameToIndex(
name: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
) {
switch (name) {
case 'triangle':
return 0
case 'catrom':
return 1
case 'mitchell':
return 2
case 'lanczos3':
return 3
default:
throw Error(`Unknown resize algorithm "${name}"`)
}
}
function resizeWithAspect({
input_width,
input_height,
target_width,
target_height,
}: ResizeWithAspectParams): { width: number; height: number } {
if (!target_width && !target_height) {
throw Error('Need to specify at least width or height when resizing')
}
if (target_width && target_height) {
return { width: target_width, height: target_height }
}
if (!target_width) {
return {
width: Math.round((input_width / input_height) * target_height!),
height: target_height!,
}
}
return {
width: target_width,
height: Math.round((input_height / input_width) * target_width),
}
}
export const preprocessors = {
resize: {
name: 'Resize',
description: 'Resize the image before compressing',
instantiate: async () => {
await resizeInit()
return (
buffer: Uint8Array,
input_width: number,
input_height: number,
{ width, height, method, premultiply, linearRGB }: ResizeOptions
) => {
;({ width, height } = resizeWithAspect({
input_width,
input_height,
target_width: width,
target_height: height,
}))
const imageData = new ImageData(
resize.resize(
buffer,
input_width,
input_height,
width,
height,
resizeNameToIndex(method),
premultiply,
linearRGB
),
width,
height
)
resize.cleanup()
return imageData
}
},
defaultOptions: {
method: 'lanczos3',
fitMethod: 'stretch',
premultiply: true,
linearRGB: true,
},
},
rotate: {
name: 'Rotate',
description: 'Rotate image',
instantiate: async () => {
return async (
buffer: Uint8Array,
width: number,
height: number,
{ numRotations }: RotateOptions
) => {
const degrees = (numRotations * 90) % 360
const sameDimensions = degrees === 0 || degrees === 180
const size = width * height * 4
const instance = (
await WebAssembly.instantiate(await fsp.readFile(pathify(rotateWasm.toString())))
).instance as RotateModuleInstance
const { memory } = instance.exports
const additionalPagesNeeded = Math.ceil(
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024)
)
if (additionalPagesNeeded > 0) {
memory.grow(additionalPagesNeeded)
}
const view = new Uint8ClampedArray(memory.buffer)
view.set(buffer, 8)
instance.exports.rotate(width, height, degrees)
return new ImageData(
view.slice(size + 8, size * 2 + 8),
sameDimensions ? width : height,
sameDimensions ? height : width
)
}
},
defaultOptions: {
numRotations: 0,
},
},
} as const
export const codecs = {
mozjpeg: {
name: 'MozJPEG',
extension: 'jpg',
detectors: [/^\xFF\xD8\xFF/],
dec: () =>
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm.toString()),
enc: () =>
instantiateEmscriptenWasm(
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
mozEncWasm.toString()
),
defaultEncoderOptions: {
quality: 75,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: 3 /*YCbCr*/,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
},
autoOptimize: {
option: 'quality',
min: 0,
max: 100,
},
},
webp: {
name: 'WebP',
extension: 'webp',
detectors: [/^RIFF....WEBPVP8[LX ]/s],
dec: () =>
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm.toString()),
enc: () =>
instantiateEmscriptenWasm(
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
webpEncWasm.toString()
),
defaultEncoderOptions: {
quality: 75,
target_size: 0,
target_PSNR: 0,
method: 4,
sns_strength: 50,
filter_strength: 60,
filter_sharpness: 0,
filter_type: 1,
partitions: 0,
segments: 4,
pass: 1,
show_compressed: 0,
preprocessing: 0,
autofilter: 0,
partition_limit: 0,
alpha_compression: 1,
alpha_filtering: 1,
alpha_quality: 100,
lossless: 0,
exact: 0,
image_hint: 0,
emulate_jpeg_size: 0,
thread_level: 0,
low_memory: 0,
near_lossless: 100,
use_delta_palette: 0,
use_sharp_yuv: 0,
},
autoOptimize: {
option: 'quality',
min: 0,
max: 100,
},
},
avif: {
name: 'AVIF',
extension: 'avif',
// eslint-disable-next-line no-control-regex
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
dec: () =>
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm.toString()),
enc: async () => {
return instantiateEmscriptenWasm(
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncWasm.toString()
)
},
defaultEncoderOptions: {
cqLevel: 33,
cqAlphaLevel: -1,
denoiseLevel: 0,
tileColsLog2: 0,
tileRowsLog2: 0,
speed: 6,
subsample: 1,
chromaDeltaQ: false,
sharpness: 0,
tune: 0 /* AVIFTune.auto */,
},
autoOptimize: {
option: 'cqLevel',
min: 62,
max: 0,
},
},
oxipng: {
name: 'OxiPNG',
extension: 'png',
// eslint-disable-next-line no-control-regex
detectors: [/^\x89PNG\x0D\x0A\x1A\x0A/],
dec: async () => {
await pngEncDecInit()
return {
decode: (buffer: Buffer | Uint8Array) => {
const imageData = pngEncDec.decode(buffer)
pngEncDec.cleanup()
return imageData
},
}
},
enc: async () => {
await pngEncDecInit()
await oxipngInit()
return {
encode: (
buffer: Uint8ClampedArray | ArrayBuffer,
width: number,
height: number,
opts: { level: number }
) => {
const simplePng = pngEncDec.encode(
new Uint8Array(buffer),
width,
height
)
const imageData = oxipng.optimise(simplePng, opts.level, false)
oxipng.cleanup()
return imageData
},
}
},
defaultEncoderOptions: {
level: 2,
},
autoOptimize: {
option: 'level',
min: 6,
max: 1,
},
},
} as const

View file

@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export async function copyWasmFiles(dir: URL) {
const src = new URL('./', import.meta.url);
await copyDir(fileURLToPath(src), fileURLToPath(dir));
}
async function copyDir(src: string, dest: string) {
const itemNames = await fs.readdir(src);
await Promise.all(itemNames.map(async (srcName) => {
const srcPath = path.join(src, srcName);
const destPath = path.join(dest, srcName);
const s = await fs.stat(srcPath);
if (s.isFile() && /.wasm$/.test(srcPath)) {
await fs.mkdir(path.dirname(destPath), { recursive: true });
await fs.copyFile(srcPath, destPath);
}
else if (s.isDirectory()) {
await copyDir(srcPath, destPath);
}
}));
}

View file

@ -0,0 +1,121 @@
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
declare namespace EmscriptenWasm {
type ModuleFactory<T extends Module = Module> = (
moduleOverrides?: ModuleOpts
) => Promise<T>
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'
// Options object for modularized Emscripten files. Shoe-horned by @surma.
// FIXME: This an incomplete definition!
interface ModuleOpts {
mainScriptUrlOrBlob?: string
noInitialRun?: boolean
locateFile?: (url: string) => string
onRuntimeInitialized?: () => void
}
interface Module {
print(str: string): void
printErr(str: string): void
arguments: string[]
environment: EnvironmentType
preInit: { (): void }[]
preRun: { (): void }[]
postRun: { (): void }[]
preinitializedWebGLContext: WebGLRenderingContext
noInitialRun: boolean
noExitRuntime: boolean
logReadFiles: boolean
filePackagePrefixURL: string
wasmBinary: ArrayBuffer
destroy(object: object): void
getPreloadedPackage(
remotePackageName: string,
remotePackageSize: number
): ArrayBuffer
instantiateWasm(
imports: WebAssembly.Imports,
successCallback: (module: WebAssembly.Module) => void
): WebAssembly.Exports
locateFile(url: string): string
onCustomMessage(event: MessageEvent): void
Runtime: any
ccall(
ident: string,
returnType: string | null,
argTypes: string[],
args: any[]
): any
cwrap(ident: string, returnType: string | null, argTypes: string[]): any
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void
getValue(ptr: number, type: string, noSafe?: boolean): number
ALLOC_NORMAL: number
ALLOC_STACK: number
ALLOC_STATIC: number
ALLOC_DYNAMIC: number
ALLOC_NONE: number
allocate(slab: any, types: string, allocator: number, ptr: number): number
allocate(slab: any, types: string[], allocator: number, ptr: number): number
Pointer_stringify(ptr: number, length?: number): string
UTF16ToString(ptr: number): string
stringToUTF16(str: string, outPtr: number): void
UTF32ToString(ptr: number): string
stringToUTF32(str: string, outPtr: number): void
// USE_TYPED_ARRAYS == 1
HEAP: Int32Array
IHEAP: Int32Array
FHEAP: Float64Array
// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array
HEAP16: Int16Array
HEAP32: Int32Array
HEAPU8: Uint8Array
HEAPU16: Uint16Array
HEAPU32: Uint32Array
HEAPF32: Float32Array
HEAPF64: Float64Array
TOTAL_STACK: number
TOTAL_MEMORY: number
FAST_MEMORY: number
addOnPreRun(cb: () => any): void
addOnInit(cb: () => any): void
addOnPreMain(cb: () => any): void
addOnExit(cb: () => any): void
addOnPostRun(cb: () => any): void
// Tools
intArrayFromString(
stringy: string,
dontAddNull?: boolean,
length?: number
): number[]
intArrayToString(array: number[]): string
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void
writeArrayToMemory(array: number[], buffer: number): void
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void
addRunDependency(id: any): void
removeRunDependency(id: any): void
preloadedImages: any
preloadedAudios: any
_malloc(size: number): number
_free(ptr: number): void
// Augmentations below by @surma.
onRuntimeInitialized: () => void | null
}
}

View file

@ -0,0 +1,44 @@
//
import { fileURLToPath, pathToFileURL } from 'node:url'
export function pathify(path: string): string {
if (path.startsWith('file://')) {
path = fileURLToPath(path)
}
return path
}
export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
factory: EmscriptenWasm.ModuleFactory<T>,
path: string,
workerJS = ''
): Promise<T> {
return factory({
locateFile(requestPath) {
// The glue code generated by emscripten uses the original
// file names of the worker file and the wasm binary.
// These will have changed in the bundling process and
// we need to inject them here.
if (requestPath.endsWith('.wasm')) return pathify(path)
if (requestPath.endsWith('.worker.js')) return pathify(workerJS)
return requestPath
},
})
}
export function dirname(url: string) {
return url.substring(0, url.lastIndexOf('/'))
}
/**
* On certain serverless hosts, our ESM bundle is transpiled to CJS before being run, which means
* import.meta.url is undefined, so we'll fall back to __dirname in those cases
* We should be able to remove this once https://github.com/netlify/zip-it-and-ship-it/issues/750 is fixed
*/
export function getModuleURL(url: string | undefined): string {
if (!url) {
return pathToFileURL(__dirname).toString();
}
return url
}

View file

@ -0,0 +1,150 @@
import { isMainThread } from 'node:worker_threads';
import { cpus } from 'os';
import { fileURLToPath } from 'url';
import type { OutputFormat } from '../../../types.js';
import { getModuleURL } from './emscripten-utils.js';
import type { Operation } from './image.js';
import * as impl from './impl.js';
import execOnce from './utils/execOnce.js';
import WorkerPool from './utils/workerPool.js';
const getWorker = execOnce(() => {
return new WorkerPool(
// There will be at most 7 workers needed since each worker will take
// at least 1 operation type.
Math.max(1, Math.min(cpus().length - 1, 7)),
fileURLToPath(getModuleURL(import.meta.url))
);
});
type DecodeParams = {
operation: 'decode';
buffer: Buffer;
};
type ResizeParams = {
operation: 'resize';
imageData: ImageData;
height?: number;
width?: number;
};
type RotateParams = {
operation: 'rotate';
imageData: ImageData;
numRotations: number;
};
type EncodeAvifParams = {
operation: 'encodeavif';
imageData: ImageData;
quality: number;
};
type EncodeJpegParams = {
operation: 'encodejpeg';
imageData: ImageData;
quality: number;
};
type EncodePngParams = {
operation: 'encodepng';
imageData: ImageData;
};
type EncodeWebpParams = {
operation: 'encodewebp';
imageData: ImageData;
quality: number;
};
type JobMessage =
| DecodeParams
| ResizeParams
| RotateParams
| EncodeAvifParams
| EncodeJpegParams
| EncodePngParams
| EncodeWebpParams;
function handleJob(params: JobMessage) {
switch (params.operation) {
case 'decode':
return impl.decodeBuffer(params.buffer);
case 'resize':
return impl.resize({
image: params.imageData as any,
width: params.width,
height: params.height,
});
case 'rotate':
return impl.rotate(params.imageData as any, params.numRotations);
case 'encodeavif':
return impl.encodeAvif(params.imageData as any, { quality: params.quality });
case 'encodejpeg':
return impl.encodeJpeg(params.imageData as any, { quality: params.quality });
case 'encodepng':
return impl.encodePng(params.imageData as any);
case 'encodewebp':
return impl.encodeWebp(params.imageData as any, { quality: params.quality });
default:
throw Error(`Invalid job "${(params as any).operation}"`);
}
}
export async function processBuffer(
buffer: Buffer,
operations: Operation[],
encoding: OutputFormat,
quality?: number
): Promise<Uint8Array> {
// @ts-ignore
const worker = await getWorker();
let imageData = await worker.dispatchJob({
operation: 'decode',
buffer,
});
for (const operation of operations) {
if (operation.type === 'rotate') {
imageData = await worker.dispatchJob({
operation: 'rotate',
imageData,
numRotations: operation.numRotations,
});
} else if (operation.type === 'resize') {
imageData = await worker.dispatchJob({
operation: 'resize',
imageData,
height: operation.height,
width: operation.width,
});
}
}
switch (encoding) {
case 'avif':
return (await worker.dispatchJob({
operation: 'encodeavif',
imageData,
quality,
})) as Uint8Array;
case 'jpeg':
case 'jpg':
return (await worker.dispatchJob({
operation: 'encodejpeg',
imageData,
quality,
})) as Uint8Array;
case 'png':
return (await worker.dispatchJob({
operation: 'encodepng',
imageData,
})) as Uint8Array;
case 'webp':
return (await worker.dispatchJob({
operation: 'encodewebp',
imageData,
quality,
})) as Uint8Array;
default:
throw Error(`Unsupported encoding format`);
}
}
if (!isMainThread) {
WorkerPool.useThisThreadAsWorker(handleJob);
}

View file

@ -0,0 +1,43 @@
import type { OutputFormat } from '../../../types.js';
import * as impl from './impl.js';
type RotateOperation = {
type: 'rotate'
numRotations: number
}
type ResizeOperation = {
type: 'resize'
width?: number
height?: number
}
export type Operation = RotateOperation | ResizeOperation
export async function processBuffer(
buffer: Buffer,
operations: Operation[],
encoding: OutputFormat,
quality?: number
): Promise<Uint8Array> {
let imageData = await impl.decodeBuffer(buffer)
for (const operation of operations) {
if (operation.type === 'rotate') {
imageData = await impl.rotate(imageData, operation.numRotations);
} else if (operation.type === 'resize') {
imageData = await impl.resize({ image: imageData, width: operation.width, height: operation.height })
}
}
switch (encoding) {
case 'avif':
return await impl.encodeAvif(imageData, { quality }) as Uint8Array;
case 'jpeg':
case 'jpg':
return await impl.encodeJpeg(imageData, { quality }) as Uint8Array;
case 'png':
return await impl.encodePng(imageData) as Uint8Array;
case 'webp':
return await impl.encodeWebp(imageData, { quality }) as Uint8Array;
default:
throw Error(`Unsupported encoding format`)
}
}

View file

@ -0,0 +1,33 @@
export default class ImageData {
static from(input: ImageData): ImageData {
return new ImageData(input.data || input._data, input.width, input.height)
}
private _data: Buffer | Uint8Array | Uint8ClampedArray
width: number
height: number
get data(): Buffer {
if (Object.prototype.toString.call(this._data) === '[object Object]') {
return Buffer.from(Object.values(this._data))
}
if (
this._data instanceof Buffer ||
this._data instanceof Uint8Array ||
this._data instanceof Uint8ClampedArray
) {
return Buffer.from(this._data)
}
throw new Error('invariant')
}
constructor(
data: Buffer | Uint8Array | Uint8ClampedArray,
width: number,
height: number
) {
this._data = data
this.width = width
this.height = height
}
}

View file

@ -0,0 +1,143 @@
import { codecs as supportedFormats, preprocessors } from './codecs.js'
import ImageData from './image_data.js'
type EncoderKey = keyof typeof supportedFormats
const DELAY_MS = 1000
let _promise: Promise<void> | undefined
function delayOnce(ms: number): Promise<void> {
if (!_promise) {
_promise = new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
return _promise
}
function maybeDelay(): Promise<void> {
const isAppleM1 = process.arch === 'arm64' && process.platform === 'darwin'
if (isAppleM1) {
return delayOnce(DELAY_MS)
}
return Promise.resolve()
}
export async function decodeBuffer(
_buffer: Buffer | Uint8Array
): Promise<ImageData> {
const buffer = Buffer.from(_buffer)
const firstChunk = buffer.slice(0, 16)
const firstChunkString = Array.from(firstChunk)
.map((v) => String.fromCodePoint(v))
.join('')
// TODO (future PR): support more formats
if (firstChunkString.includes('GIF')) {
throw Error(`GIF images are not supported, please install the @astrojs/image/sharp plugin`)
}
const key = Object.entries(supportedFormats).find(([, { detectors }]) =>
detectors.some((detector) => detector.exec(firstChunkString))
)?.[0] as EncoderKey | undefined
if (!key) {
throw Error(`Buffer has an unsupported format`)
}
const encoder = supportedFormats[key]
const mod = await encoder.dec()
const rgba = mod.decode(new Uint8Array(buffer))
// @ts-ignore
return rgba
}
export async function rotate(
image: ImageData,
numRotations: number
): Promise<ImageData> {
image = ImageData.from(image)
const m = await preprocessors['rotate'].instantiate()
return await m(image.data, image.width, image.height, { numRotations })
}
type ResizeOpts = { image: ImageData } & { width?: number; height?: number }
export async function resize({ image, width, height }: ResizeOpts) {
image = ImageData.from(image)
const p = preprocessors['resize']
const m = await p.instantiate()
await maybeDelay()
return await m(image.data, image.width, image.height, {
...p.defaultOptions,
width,
height,
})
}
export async function encodeJpeg(
image: ImageData,
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['mozjpeg']
const m = await e.enc()
await maybeDelay()
const quality = opts.quality || e.defaultEncoderOptions.quality
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
quality,
})
return r
}
export async function encodeWebp(
image: ImageData,
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['webp']
const m = await e.enc()
await maybeDelay()
const quality = opts.quality || e.defaultEncoderOptions.quality
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
quality,
})
return r
}
export async function encodeAvif(
image: ImageData,
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['avif']
const m = await e.enc()
await maybeDelay()
const val = e.autoOptimize.min
// AVIF doesn't use a 0-100 quality, default to 75 and convert to cqLevel below
const quality = opts.quality || 75
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
// Think of cqLevel as the "amount" of quantization (0 to 62),
// so a lower value yields higher quality (0 to 100).
cqLevel: quality === 0 ? val : Math.round(val - (quality / 100) * val),
})
return r
}
export async function encodePng(
image: ImageData
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['oxipng']
const m = await e.enc()
await maybeDelay()
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
})
return r
}

View file

@ -0,0 +1,38 @@
// eslint-disable-next-line no-shadow
export const enum MozJpegColorSpace {
GRAYSCALE = 1,
RGB,
YCbCr,
}
export interface EncodeOptions {
quality: number
baseline: boolean
arithmetic: boolean
progressive: boolean
optimize_coding: boolean
smoothing: number
color_space: MozJpegColorSpace
quant_table: number
trellis_multipass: boolean
trellis_opt_zero: boolean
trellis_opt_table: boolean
trellis_loops: number
auto_subsample: boolean
chroma_subsample: number
separate_chroma_quality: boolean
chroma_quality: number
}
export interface MozJPEGModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions
): Uint8Array
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<MozJPEGModule>
export default moduleFactory

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,120 @@
// @ts-nocheck
let wasm
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
})
cachedTextDecoder.decode()
let cachegetUint8Memory0 = null
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
}
return cachegetUint8Memory0
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
}
let WASM_VECTOR_LEN = 0
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1)
getUint8Memory0().set(arg, ptr / 1)
WASM_VECTOR_LEN = arg.length
return ptr
}
let cachegetInt32Memory0 = null
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
}
return cachegetInt32Memory0
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len)
}
/**
* @param {Uint8Array} data
* @param {number} level
* @param {boolean} interlace
* @returns {Uint8Array}
*/
export function optimise(data, level, interlace) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
wasm.optimise(retptr, ptr0, len0, level, interlace)
const r0 = getInt32Memory0()[retptr / 4 + 0]
const r1 = getInt32Memory0()[retptr / 4 + 1]
const v1 = getArrayU8FromWasm0(r0, r1).slice()
wasm.__wbindgen_free(r0, r1 * 1)
return v1
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
return await WebAssembly.instantiateStreaming(module, imports)
}
const bytes = await module.arrayBuffer()
return await WebAssembly.instantiate(bytes, imports)
} else {
const instance = await WebAssembly.instantiate(module, imports)
if (instance instanceof WebAssembly.Instance) {
return { instance, module }
} else {
return instance
}
}
}
async function init(input) {
const imports = {}
imports.wbg = {}
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1))
}
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input)
}
const { instance, module } = await load(await input, imports)
wasm = instance.exports
init.__wbindgen_wasm_module = module
return wasm
}
export default init
// Manually remove the wasm and memory references to trigger GC
export function cleanup() {
wasm = null
cachegetUint8Memory0 = null
cachegetInt32Memory0 = null
}

View file

@ -0,0 +1,184 @@
// @ts-nocheck
let wasm
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
})
cachedTextDecoder.decode()
let cachegetUint8Memory0 = null
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
}
return cachegetUint8Memory0
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
}
let cachegetUint8ClampedMemory0 = null
function getUint8ClampedMemory0() {
if (
cachegetUint8ClampedMemory0 === null ||
cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer
) {
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer)
}
return cachegetUint8ClampedMemory0
}
function getClampedArrayU8FromWasm0(ptr, len) {
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len)
}
const heap = new Array(32).fill(undefined)
heap.push(undefined, null, true, false)
let heap_next = heap.length
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1)
const idx = heap_next
heap_next = heap[idx]
heap[idx] = obj
return idx
}
let WASM_VECTOR_LEN = 0
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1)
getUint8Memory0().set(arg, ptr / 1)
WASM_VECTOR_LEN = arg.length
return ptr
}
let cachegetInt32Memory0 = null
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
}
return cachegetInt32Memory0
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len)
}
/**
* @param {Uint8Array} data
* @param {number} width
* @param {number} height
* @returns {Uint8Array}
*/
export function encode(data, width, height) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
wasm.encode(retptr, ptr0, len0, width, height)
const r0 = getInt32Memory0()[retptr / 4 + 0]
const r1 = getInt32Memory0()[retptr / 4 + 1]
const v1 = getArrayU8FromWasm0(r0, r1).slice()
wasm.__wbindgen_free(r0, r1 * 1)
return v1
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
}
}
function getObject(idx) {
return heap[idx]
}
function dropObject(idx) {
if (idx < 36) return
heap[idx] = heap_next
heap_next = idx
}
function takeObject(idx) {
const ret = getObject(idx)
dropObject(idx)
return ret
}
/**
* @param {Uint8Array} data
* @returns {ImageData}
*/
export function decode(data) {
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
const ret = wasm.decode(ptr0, len0)
return takeObject(ret)
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
return await WebAssembly.instantiateStreaming(module, imports)
}
const bytes = await module.arrayBuffer()
return await WebAssembly.instantiate(bytes, imports)
} else {
const instance = await WebAssembly.instantiate(module, imports)
if (instance instanceof WebAssembly.Instance) {
return { instance, module }
} else {
return instance
}
}
}
async function init(input) {
const imports = {}
imports.wbg = {}
imports.wbg.__wbg_newwithownedu8clampedarrayandsh_787b2db8ea6bfd62 =
function (arg0, arg1, arg2, arg3) {
const v0 = getClampedArrayU8FromWasm0(arg0, arg1).slice()
wasm.__wbindgen_free(arg0, arg1 * 1)
const ret = new ImageData(v0, arg2 >>> 0, arg3 >>> 0)
return addHeapObject(ret)
}
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1))
}
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input)
}
const { instance, module } = await load(await input, imports)
wasm = instance.exports
init.__wbindgen_wasm_module = module
return wasm
}
export default init
// Manually remove the wasm and memory references to trigger GC
export function cleanup() {
wasm = null
cachegetUint8ClampedMemory0 = null
cachegetUint8Memory0 = null
cachegetInt32Memory0 = null
}

View file

@ -0,0 +1,141 @@
// @ts-nocheck
let wasm
let cachegetUint8Memory0 = null
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
}
return cachegetUint8Memory0
}
let WASM_VECTOR_LEN = 0
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1)
getUint8Memory0().set(arg, ptr / 1)
WASM_VECTOR_LEN = arg.length
return ptr
}
let cachegetInt32Memory0 = null
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
}
return cachegetInt32Memory0
}
let cachegetUint8ClampedMemory0 = null
function getUint8ClampedMemory0() {
if (
cachegetUint8ClampedMemory0 === null ||
cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer
) {
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer)
}
return cachegetUint8ClampedMemory0
}
function getClampedArrayU8FromWasm0(ptr, len) {
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len)
}
/**
* @param {Uint8Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} output_width
* @param {number} output_height
* @param {number} typ_idx
* @param {boolean} premultiply
* @param {boolean} color_space_conversion
* @returns {Uint8ClampedArray}
*/
export function resize(
input_image,
input_width,
input_height,
output_width,
output_height,
typ_idx,
premultiply,
color_space_conversion
) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passArray8ToWasm0(input_image, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
wasm.resize(
retptr,
ptr0,
len0,
input_width,
input_height,
output_width,
output_height,
typ_idx,
premultiply,
color_space_conversion
)
const r0 = getInt32Memory0()[retptr / 4 + 0]
const r1 = getInt32Memory0()[retptr / 4 + 1]
const v1 = getClampedArrayU8FromWasm0(r0, r1).slice()
wasm.__wbindgen_free(r0, r1 * 1)
return v1
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
return await WebAssembly.instantiateStreaming(module, imports)
}
const bytes = await module.arrayBuffer()
return await WebAssembly.instantiate(bytes, imports)
} else {
const instance = await WebAssembly.instantiate(module, imports)
if (instance instanceof WebAssembly.Instance) {
return { instance, module }
} else {
return instance
}
}
}
async function init(input) {
const imports = {}
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input)
}
const { instance, module } = await load(await input, imports)
wasm = instance.exports
init.__wbindgen_wasm_module = module
return wasm
}
export default init
// Manually remove the wasm and memory references to trigger GC
export function cleanup() {
wasm = null
cachegetUint8Memory0 = null
cachegetInt32Memory0 = null
}

Some files were not shown because too many files have changed in this diff Show more