diff --git a/.changeset/fifty-avocados-lay.md b/.changeset/fifty-avocados-lay.md deleted file mode 100644 index eaf9f2e32..000000000 --- a/.changeset/fifty-avocados-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"astro": patch ---- - -Improve third-party Astro package support diff --git a/.changeset/little-boats-happen.md b/.changeset/little-boats-happen.md deleted file mode 100644 index ba970324b..000000000 --- a/.changeset/little-boats-happen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/vue': patch ---- - -Mark vueperslides as a default noExternal diff --git a/.changeset/rich-dolphins-teach.md b/.changeset/rich-dolphins-teach.md deleted file mode 100644 index a9bbf7de9..000000000 --- a/.changeset/rich-dolphins-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/image': patch ---- - -Parallelize image transforms diff --git a/.changeset/seven-zoos-lie.md b/.changeset/seven-zoos-lie.md deleted file mode 100644 index b19de1b05..000000000 --- a/.changeset/seven-zoos-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Remove regression when there is duplicate client/server CSS diff --git a/.changeset/slimy-fireants-carry.md b/.changeset/slimy-fireants-carry.md deleted file mode 100644 index 7f972759a..000000000 --- a/.changeset/slimy-fireants-carry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Correctly escape paths in file names diff --git a/.changeset/smooth-hats-smell.md b/.changeset/smooth-hats-smell.md deleted file mode 100644 index cac2f8bef..000000000 --- a/.changeset/smooth-hats-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Ensure SSR module is loaded before testing if it's CSS in dev diff --git a/.changeset/tasty-owls-watch.md b/.changeset/tasty-owls-watch.md new file mode 100644 index 000000000..8d0eb53f4 --- /dev/null +++ b/.changeset/tasty-owls-watch.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/react': patch +--- + +Fix framework components on Vercel Edge diff --git a/.changeset/three-owls-help.md b/.changeset/three-owls-help.md deleted file mode 100644 index 9fe2ded0f..000000000 --- a/.changeset/three-owls-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/alpinejs': patch ---- - -Update homepage link diff --git a/examples/basics/package.json b/examples/basics/package.json index d4d44b91f..36b26f934 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5" + "astro": "^1.1.7" } } diff --git a/examples/blog/package.json b/examples/blog/package.json index 0ccc1d178..920624ff8 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "@astrojs/mdx": "^0.11.1", "@astrojs/rss": "^1.0.0", "@astrojs/sitemap": "^1.0.0" diff --git a/examples/component/.gitignore b/examples/component/.gitignore deleted file mode 100644 index 02f6e50b4..000000000 --- a/examples/component/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# build output -dist/ - -# dependencies -node_modules/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - - -# environment variables -.env -.env.production - -# macOS-specific files -.DS_Store diff --git a/examples/component/.npmrc b/examples/component/.npmrc deleted file mode 100644 index ef83021af..000000000 --- a/examples/component/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -# Expose Astro dependencies for `pnpm` users -shamefully-hoist=true diff --git a/examples/component/.stackblitzrc b/examples/component/.stackblitzrc deleted file mode 100644 index 43798ecff..000000000 --- a/examples/component/.stackblitzrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "startCommand": "npm start", - "env": { - "ENABLE_CJS_IMPORTS": true - } -} \ No newline at end of file diff --git a/examples/component/README.md b/examples/component/README.md index 8cd73e343..2023a749e 100644 --- a/examples/component/README.md +++ b/examples/component/README.md @@ -1,12 +1,13 @@ -# Astro Starter Kit: Component +# Astro Starter Kit: Component Package + +This is a template for an Astro component library. Use this template for writing components to use in multiple projects or publish to NPM. ``` npm init astro -- --template component ``` -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/component) +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/non-html-pages) -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! ## πŸš€ Project Structure @@ -14,34 +15,19 @@ Inside of your Astro project, you'll see the following folders and files: ``` / -β”œβ”€β”€ demo/ -β”‚ β”œβ”€β”€ public/ -β”‚ └── src/ -β”‚ └── pages/ -β”‚ └── index.astro -└── packages/ - └── my-component/ - β”œβ”€β”€ index.js - └── package.json +β”œβ”€β”€ index.ts +β”œβ”€β”€ src +β”‚ └── MyComponent.astro +β”œβ”€β”€ tsconfig.json +β”œβ”€β”€ package.json ``` -This project uses **workspaces** to develop a single package, `@example/my-component`, from `packages/my-component`. It also includes a `demo` Astro site for testing and demonstrating the component. - - +The `index.ts` file is the "entry point" for your package. Export your components in `index.ts` to make them importable from your package. ## 🧞 Commands - All commands are run from the root of the project, from a terminal: | Command | Action | | :--------------------- | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:3000` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). +| `npm link` | Registers this package locally. Run `npm link my-component-library` in an Astro project to install your components +| `npm publish` | [Publishes](https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages#publishing-unscoped-public-packages) this package to NPM. Requires you to be [logged in](https://docs.npmjs.com/cli/v8/commands/npm-adduser) diff --git a/examples/component/demo/package.json b/examples/component/demo/package.json deleted file mode 100644 index 118b218b7..000000000 --- a/examples/component/demo/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@example/my-component-demo", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "astro preview", - "astro": "astro" - }, - "devDependencies": { - "@example/my-component": "workspace:*", - "astro": "^1.1.5" - } -} diff --git a/examples/component/demo/public/favicon.svg b/examples/component/demo/public/favicon.svg deleted file mode 100644 index 0f3906297..000000000 --- a/examples/component/demo/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/examples/component/demo/src/pages/index.astro b/examples/component/demo/src/pages/index.astro deleted file mode 100644 index f0704e56b..000000000 --- a/examples/component/demo/src/pages/index.astro +++ /dev/null @@ -1,25 +0,0 @@ ---- -import * as Component from '@example/my-component'; ---- - - - - - - - Welcome to Astro - - - - - Welcome to Astro - Plain Button - - diff --git a/examples/component/index.ts b/examples/component/index.ts new file mode 100644 index 000000000..0e2e94fb2 --- /dev/null +++ b/examples/component/index.ts @@ -0,0 +1,6 @@ +// Do not write code directly here, instead use the `src` folder! +// Then, use this file to export everything you want your user to access. + +import MyComponent from './src/MyComponent.astro'; + +export default MyComponent; diff --git a/examples/component/package.json b/examples/component/package.json index f8fb37630..80f78ef8d 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -1,13 +1,23 @@ { "name": "@example/component", - "version": "0.0.1", "private": true, - "scripts": { - "start": "astro --root demo dev", - "build": "astro --root demo build", - "serve": "astro --root demo preview" + "version": "0.0.1", + "type": "module", + "exports": { + ".": "./index.ts" }, - "dependencies": { - "astro": "^1.1.5" + "files": [ + "src", + "index.ts" + ], + "keywords": [ + "astro-component" + ], + "scripts": {}, + "devDependencies": { + "astro": "^1.1.7" + }, + "peerDependencies": { + "astro": "^1.1.7" } } diff --git a/examples/component/packages/my-component/Button.astro b/examples/component/packages/my-component/Button.astro deleted file mode 100644 index 87943fa28..000000000 --- a/examples/component/packages/my-component/Button.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -export interface Props extends Record { - type?: string; -} - -const { type, ...props } = { - ...Astro.props, -}; - -props.type = type || 'button'; ---- - - diff --git a/examples/component/packages/my-component/Heading.astro b/examples/component/packages/my-component/Heading.astro deleted file mode 100644 index 75e4aa4e0..000000000 --- a/examples/component/packages/my-component/Heading.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -export interface Props extends Record { - level?: number | string; - role?: string; -} - -const { level, role, ...props } = { - ...Astro.props, -}; - -props.role = role || 'heading'; -props['aria-level'] = level || '1'; ---- - - diff --git a/examples/component/packages/my-component/README.md b/examples/component/packages/my-component/README.md deleted file mode 100644 index 41a53a6f0..000000000 --- a/examples/component/packages/my-component/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Example `@example/my-component` - -This is an example package, exported as `@example/my-component`. It consists of two Astro components, **Button** and **Heading**. - -### Button - -The **Button** component generates a ` -``` - -### Heading - -The **Heading** component generates an `` tag with a default **role** of **heading** and a **level** attribute that gets written to **aria-level**. - -```astro ---- -import * as Component from '@example/my-component' ---- -Heading -Subheading -``` - -```html - -Plain Button -Subheading -``` diff --git a/examples/component/packages/my-component/index.js b/examples/component/packages/my-component/index.js deleted file mode 100644 index 603a81a96..000000000 --- a/examples/component/packages/my-component/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Button } from './Button.astro'; -export { default as Heading } from './Heading.astro'; diff --git a/examples/component/packages/my-component/package.json b/examples/component/packages/my-component/package.json deleted file mode 100644 index 47212b558..000000000 --- a/examples/component/packages/my-component/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@example/my-component", - "version": "0.0.1", - "private": true, - "type": "module", - "exports": { - ".": "./index.js", - "./Button": "./Button.astro", - "./Heading": "./Heading.astro" - }, - "files": [ - "index.js", - "Button.astro", - "Heading.jsx" - ], - "keywords": [ - "astro-component", - "button", - "heading", - "example" - ] -} diff --git a/examples/component/pnpm-workspace.yaml b/examples/component/pnpm-workspace.yaml deleted file mode 100644 index 22fd6807e..000000000 --- a/examples/component/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - 'packages/**/*' - - 'demo' diff --git a/examples/component/sandbox.config.json b/examples/component/sandbox.config.json deleted file mode 100644 index 9178af77d..000000000 --- a/examples/component/sandbox.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "infiniteLoopProtection": true, - "hardReloadOnChange": false, - "view": "browser", - "template": "node", - "container": { - "port": 3000, - "startScript": "start", - "node": "14" - } -} diff --git a/examples/component/src/MyComponent.astro b/examples/component/src/MyComponent.astro new file mode 100644 index 000000000..96f9ecdda --- /dev/null +++ b/examples/component/src/MyComponent.astro @@ -0,0 +1,8 @@ +--- +// Write your component code in this file! +export interface Props { + prefix?: string +} +--- + +
{Astro.props.prefix} My special component
diff --git a/examples/component/tsconfig.json b/examples/component/tsconfig.json new file mode 100644 index 000000000..d78f81ec4 --- /dev/null +++ b/examples/component/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/base" +} diff --git a/examples/docs/package.json b/examples/docs/package.json index 0c0add7dc..96a7aa29c 100644 --- a/examples/docs/package.json +++ b/examples/docs/package.json @@ -11,12 +11,12 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "preact": "^10.7.3", "react": "^18.1.0", "react-dom": "^18.1.0", "@astrojs/react": "^1.1.0", - "@astrojs/preact": "^1.0.2", + "@astrojs/preact": "^1.1.0", "@algolia/client-search": "^4.13.1", "@docsearch/css": "^3.1.0", "@docsearch/react": "^3.1.0", diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index f5f2f3071..ecead3157 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "alpinejs": "^3.10.2", - "@astrojs/alpinejs": "^0.1.1", + "@astrojs/alpinejs": "^0.1.2", "@types/alpinejs": "^3.7.0" } } diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json index aa9cdae26..d9ea200a7 100644 --- a/examples/framework-lit/package.json +++ b/examples/framework-lit/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "lit": "^2.2.5", "@astrojs/lit": "^1.0.0", "@webcomponents/template-shadowroot": "^0.1.0" diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index edab8d742..63bff5342 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -10,17 +10,17 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "preact": "^10.7.3", "react": "^18.1.0", "react-dom": "^18.1.0", "solid-js": "^1.4.3", "svelte": "^3.48.0", "vue": "^3.2.37", - "@astrojs/preact": "^1.0.2", + "@astrojs/preact": "^1.1.0", "@astrojs/react": "^1.1.0", "@astrojs/solid-js": "^1.1.0", "@astrojs/svelte": "^1.0.0", - "@astrojs/vue": "^1.0.0" + "@astrojs/vue": "^1.0.1" } } diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index 9fe5859b4..86fa870bd 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -10,8 +10,8 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "preact": "^10.7.3", - "@astrojs/preact": "^1.0.2" + "@astrojs/preact": "^1.1.0" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index cccfbf0ce..a68fbd845 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "react": "^18.1.0", "react-dom": "^18.1.0", "@astrojs/react": "^1.1.0", diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 40484e92f..55392d4df 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "solid-js": "^1.4.3", "@astrojs/solid-js": "^1.1.0" } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 9b64aba73..25c5252d7 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -12,6 +12,6 @@ "dependencies": { "svelte": "^3.48.0", "@astrojs/svelte": "^1.0.0", - "astro": "^1.1.5" + "astro": "^1.1.7" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index ca8b3b075..b14317693 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -10,8 +10,8 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "vue": "^3.2.37", - "@astrojs/vue": "^1.0.0" + "@astrojs/vue": "^1.0.1" } } diff --git a/examples/minimal/package.json b/examples/minimal/package.json index 115162510..05284048d 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5" + "astro": "^1.1.7" } } diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json index 1c02146f9..83d238c44 100644 --- a/examples/non-html-pages/package.json +++ b/examples/non-html-pages/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5" + "astro": "^1.1.7" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index a1c002dc7..69f7a343e 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5" + "astro": "^1.1.7" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index b9d96f4c7..ef957afc8 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -12,7 +12,7 @@ }, "devDependencies": {}, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "svelte": "^3.48.0", "@astrojs/svelte": "^1.0.0", "@astrojs/node": "^1.0.1", diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json index eca0da00b..7262a8b2e 100644 --- a/examples/with-markdown-plugins/package.json +++ b/examples/with-markdown-plugins/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "@astrojs/markdown-remark": "^1.1.0", "hast-util-select": "5.0.1", "rehype-autolink-headings": "^6.1.1", diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json index 860e310c6..1a7936dc7 100644 --- a/examples/with-markdown-shiki/package.json +++ b/examples/with-markdown-shiki/package.json @@ -10,6 +10,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5" + "astro": "^1.1.7" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 94b99ee43..22446d4d4 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "preact": "^10.6.5", - "@astrojs/preact": "^1.0.2", + "@astrojs/preact": "^1.1.0", "@astrojs/mdx": "^0.11.1" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 6391fca67..81f93325a 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -10,9 +10,9 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "preact": "^10.7.3", - "@astrojs/preact": "^1.0.2", + "@astrojs/preact": "^1.1.0", "nanostores": "^0.5.12", "@nanostores/preact": "^0.1.3" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index f44677545..7688bf6eb 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "@astrojs/tailwind": "^1.0.0", "autoprefixer": "^10.4.7", "canvas-confetti": "^1.5.1", diff --git a/examples/with-vite-plugin-pwa/package.json b/examples/with-vite-plugin-pwa/package.json index c2a3d1745..8c72a34e8 100644 --- a/examples/with-vite-plugin-pwa/package.json +++ b/examples/with-vite-plugin-pwa/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "vite-plugin-pwa": "0.11.11", "workbox-window": "^6.5.3" } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index ba93c3bc9..c7edbeac6 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -12,7 +12,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^1.1.5", + "astro": "^1.1.7", "vitest": "^0.20.3" } } diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index fdaea4381..27b36d8d3 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,27 @@ # astro +## 1.1.7 + +### Patch Changes + +- [#4646](https://github.com/withastro/astro/pull/4646) [`98f242cdc`](https://github.com/withastro/astro/commit/98f242cdcd860679ad787ffb387558cb1dc93b87) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Add cyclic ref detection when serializing props + +- [#4656](https://github.com/withastro/astro/pull/4656) [`6d845c353`](https://github.com/withastro/astro/commit/6d845c353d5688f30787c4361f86c605fb638dd9) Thanks [@matthewp](https://github.com/matthewp)! - Fix bug with using `assert` as import identifier + +- [#4403](https://github.com/withastro/astro/pull/4403) [`d31e72c3b`](https://github.com/withastro/astro/commit/d31e72c3ba8270d1e8d33c533502b3c4c6390a15) Thanks [@JohnDaly](https://github.com/JohnDaly)! - Fix for components, declared with JSXMemberExpression nodes, that failed to hydrate due to incomplete 'component-export' metadata + +## 1.1.6 + +### Patch Changes + +- [#4623](https://github.com/withastro/astro/pull/4623) [`eb1862b4e`](https://github.com/withastro/astro/commit/eb1862b4e68b399eecc7267ea9e0bee36983b0cb) Thanks [@delucis](https://github.com/delucis)! - Improve third-party Astro package support + +- [#4643](https://github.com/withastro/astro/pull/4643) [`307b7b97c`](https://github.com/withastro/astro/commit/307b7b97ce79d076ceb4fdc25fd28a27077deb34) Thanks [@matthewp](https://github.com/matthewp)! - Remove regression when there is duplicate client/server CSS + +- [#4584](https://github.com/withastro/astro/pull/4584) [`29a5fdc15`](https://github.com/withastro/astro/commit/29a5fdc1535fc389035d8107025f7490bfa976ed) Thanks [@bluwy](https://github.com/bluwy)! - Correctly escape paths in file names + +- [#4621](https://github.com/withastro/astro/pull/4621) [`0068afb87`](https://github.com/withastro/astro/commit/0068afb876342ae76154e552dfc5bb6832b665ed) Thanks [@AllanChain](https://github.com/AllanChain)! - Ensure SSR module is loaded before testing if it's CSS in dev + ## 1.1.5 ### Patch Changes diff --git a/packages/astro/e2e/error-cyclic.test.js b/packages/astro/e2e/error-cyclic.test.js new file mode 100644 index 000000000..78c3bd1ea --- /dev/null +++ b/packages/astro/e2e/error-cyclic.test.js @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; +import { testFactory, getErrorOverlayMessage } from './test-utils.js'; + +const test = testFactory({ root: './fixtures/error-cyclic/' }); + +let devServer; + +test.beforeEach(async ({ astro }) => { + devServer = await astro.startDevServer(); +}); + +test.afterEach(async ({ astro }) => { + await devServer.stop(); + astro.resetAllFiles(); +}); + +test.describe('Error: Cyclic Reference', () => { + test('overlay', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const message = await getErrorOverlayMessage(page); + expect(message).toMatch('Cyclic reference'); + }); +}); diff --git a/examples/component/demo/astro.config.mjs b/packages/astro/e2e/fixtures/error-cyclic/astro.config.mjs similarity index 61% rename from examples/component/demo/astro.config.mjs rename to packages/astro/e2e/fixtures/error-cyclic/astro.config.mjs index 2d73a2807..08916b1fe 100644 --- a/examples/component/demo/astro.config.mjs +++ b/packages/astro/e2e/fixtures/error-cyclic/astro.config.mjs @@ -1,10 +1,7 @@ import { defineConfig } from 'astro/config'; +import preact from '@astrojs/preact'; // https://astro.build/config export default defineConfig({ - vite: { - ssr: { - noExternal: ['@example/my-component'], - }, - }, + integrations: [preact()], }); diff --git a/packages/astro/e2e/fixtures/error-cyclic/package.json b/packages/astro/e2e/fixtures/error-cyclic/package.json new file mode 100644 index 000000000..46cce7a02 --- /dev/null +++ b/packages/astro/e2e/fixtures/error-cyclic/package.json @@ -0,0 +1,9 @@ +{ + "name": "@e2e/error-cyclic", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/preact": "workspace:*" + } +} diff --git a/packages/astro/e2e/fixtures/error-cyclic/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/error-cyclic/src/components/PreactCounter.tsx new file mode 100644 index 000000000..b0570046c --- /dev/null +++ b/packages/astro/e2e/fixtures/error-cyclic/src/components/PreactCounter.tsx @@ -0,0 +1,17 @@ +import { useState } from 'preact/hooks'; + +/** a counter written in Preact */ +export function PreactCounter({ children, id }) { + const [count, setCount] = useState(0); + const add = () => setCount((i) => i + 1); + const subtract = () => setCount((i) => i - 1); + + return ( +
+ +
{count}
+ +
{children}
+
+ ); +} diff --git a/packages/astro/e2e/fixtures/error-cyclic/src/pages/index.astro b/packages/astro/e2e/fixtures/error-cyclic/src/pages/index.astro new file mode 100644 index 000000000..32d68994b --- /dev/null +++ b/packages/astro/e2e/fixtures/error-cyclic/src/pages/index.astro @@ -0,0 +1,8 @@ +--- +import { PreactCounter } from '../components/PreactCounter' + +const cycle: any = { foo: ['bar'] } +cycle.foo.push(cycle) +--- + + diff --git a/packages/astro/e2e/fixtures/namespaced-component/astro.config.mjs b/packages/astro/e2e/fixtures/namespaced-component/astro.config.mjs index 08916b1fe..f7c8e034b 100644 --- a/packages/astro/e2e/fixtures/namespaced-component/astro.config.mjs +++ b/packages/astro/e2e/fixtures/namespaced-component/astro.config.mjs @@ -1,7 +1,8 @@ import { defineConfig } from 'astro/config'; import preact from '@astrojs/preact'; +import mdx from "@astrojs/mdx"; // https://astro.build/config export default defineConfig({ - integrations: [preact()], + integrations: [preact(), mdx()] }); diff --git a/packages/astro/e2e/fixtures/namespaced-component/package.json b/packages/astro/e2e/fixtures/namespaced-component/package.json index 6968717cf..96de360e2 100644 --- a/packages/astro/e2e/fixtures/namespaced-component/package.json +++ b/packages/astro/e2e/fixtures/namespaced-component/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "devDependencies": { + "@astrojs/mdx": "workspace:*", "@astrojs/preact": "workspace:*", "astro": "workspace:*" }, diff --git a/packages/astro/e2e/fixtures/namespaced-component/src/pages/index.astro b/packages/astro/e2e/fixtures/namespaced-component/src/pages/index.astro index 608b48458..7a65b02ae 100644 --- a/packages/astro/e2e/fixtures/namespaced-component/src/pages/index.astro +++ b/packages/astro/e2e/fixtures/namespaced-component/src/pages/index.astro @@ -1,5 +1,6 @@ --- import * as ns from '../components/PreactCounter.tsx'; +import { components } from '../components/PreactCounter.tsx'; --- @@ -10,9 +11,13 @@ import * as ns from '../components/PreactCounter.tsx';
- -

preact

+ +

preact (namespace import)

+ + +

preact (named import)

+
diff --git a/packages/astro/e2e/fixtures/namespaced-component/src/pages/mdx.mdx b/packages/astro/e2e/fixtures/namespaced-component/src/pages/mdx.mdx new file mode 100644 index 000000000..33ae8cff3 --- /dev/null +++ b/packages/astro/e2e/fixtures/namespaced-component/src/pages/mdx.mdx @@ -0,0 +1,10 @@ +import * as ns from '../components/PreactCounter.tsx'; +import { components } from '../components/PreactCounter.tsx'; + + + preact (namespace import) + + + + preact (named import) + \ No newline at end of file diff --git a/packages/astro/e2e/namespaced-component.test.js b/packages/astro/e2e/namespaced-component.test.js index a23f9ee78..729f67f4d 100644 --- a/packages/astro/e2e/namespaced-component.test.js +++ b/packages/astro/e2e/namespaced-component.test.js @@ -19,18 +19,68 @@ test.describe('Hydrating namespaced components', () => { test('Preact Component', async ({ page }) => { await page.goto('/'); - const counter = await page.locator('#preact-counter'); - await expect(counter, 'component is visible').toBeVisible(); + // Counter declared with: + const namespacedCounter = await page.locator('#preact-counter-namespace'); + await expect(namespacedCounter, 'component is visible').toBeVisible(); - const count = await counter.locator('pre'); - await expect(count, 'initial count is 0').toHaveText('0'); + const namespacedCount = await namespacedCounter.locator('pre'); + await expect(namespacedCount, 'initial count is 0').toHaveText('0'); - const children = await counter.locator('.children'); - await expect(children, 'children exist').toHaveText('preact'); + const namespacedChildren = await namespacedCounter.locator('.children'); + await expect(namespacedChildren, 'children exist').toHaveText('preact (namespace import)'); - const increment = await counter.locator('.increment'); - await increment.click(); + const namespacedIncrement = await namespacedCounter.locator('.increment'); + await namespacedIncrement.click(); - await expect(count, 'count incremented by 1').toHaveText('1'); + await expect(namespacedCount, 'count incremented by 1').toHaveText('1'); + + // Counter declared with: + const namedCounter = await page.locator('#preact-counter-named'); + await expect(namedCounter, 'component is visible').toBeVisible(); + + const namedCount = await namedCounter.locator('pre'); + await expect(namedCount, 'initial count is 0').toHaveText('0'); + + const namedChildren = await namedCounter.locator('.children'); + await expect(namedChildren, 'children exist').toHaveText('preact (named import)'); + + const namedIncrement = await namedCounter.locator('.increment'); + await namedIncrement.click(); + + await expect(namedCount, 'count incremented by 1').toHaveText('1'); + }); + + test('MDX', async ({ page }) => { + await page.goto('/mdx'); + + // Counter declared with: + const namespacedCounter = await page.locator('#preact-counter-namespace'); + await expect(namespacedCounter, 'component is visible').toBeVisible(); + + const namespacedCount = await namespacedCounter.locator('pre'); + await expect(namespacedCount, 'initial count is 0').toHaveText('0'); + + const namespacedChildren = await namespacedCounter.locator('.children'); + await expect(namespacedChildren, 'children exist').toHaveText('preact (namespace import)'); + + const namespacedIncrement = await namespacedCounter.locator('.increment'); + await namespacedIncrement.click(); + + await expect(namespacedCount, 'count incremented by 1').toHaveText('1'); + + // Counter declared with: + const namedCounter = await page.locator('#preact-counter-named'); + await expect(namedCounter, 'component is visible').toBeVisible(); + + const namedCount = await namedCounter.locator('pre'); + await expect(namedCount, 'initial count is 0').toHaveText('0'); + + const namedChildren = await namedCounter.locator('.children'); + await expect(namedChildren, 'children exist').toHaveText('preact (named import)'); + + const namedIncrement = await namedCounter.locator('.increment'); + await namedIncrement.click(); + + await expect(namedCount, 'count incremented by 1').toHaveText('1'); }); }); diff --git a/packages/astro/package.json b/packages/astro/package.json index 6ec238651..82c848ad3 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "astro", - "version": "1.1.5", + "version": "1.1.7", "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.", "type": "module", "author": "withastro", @@ -94,7 +94,7 @@ "test:e2e:match": "playwright test -g" }, "dependencies": { - "@astrojs/compiler": "^0.23.4", + "@astrojs/compiler": "^0.23.5", "@astrojs/language-server": "^0.23.0", "@astrojs/markdown-remark": "^1.1.1", "@astrojs/telemetry": "^1.0.0", diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index e93fd7fb9..8ee301344 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -243,7 +243,7 @@ class DependencyWalker { dir = parentDir; } - } catch (e) { + } catch { // Give up! Who knows where the `package.json` is… } } diff --git a/packages/astro/src/jsx/babel.ts b/packages/astro/src/jsx/babel.ts index 1a5a7664e..a8c4ef3c6 100644 --- a/packages/astro/src/jsx/babel.ts +++ b/packages/astro/src/jsx/babel.ts @@ -207,7 +207,8 @@ export default function astroJSX(): PluginObj { break; } if (namespace.at(0) === local) { - path.setData('import', { name: imported, path: source }); + const name = imported === '*' ? imported : tagName; + path.setData('import', { name, path: source }); break; } } diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index fea251b79..30688237a 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -146,7 +146,7 @@ export async function generateHydrateScript( if (renderer.clientEntrypoint) { island.props['component-export'] = componentExport.value; island.props['renderer-url'] = await result.resolve(decodeURI(renderer.clientEntrypoint)); - island.props['props'] = escapeHTML(serializeProps(props)); + island.props['props'] = escapeHTML(serializeProps(props, metadata)); } island.props['ssr'] = ''; diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index 066a86353..75398d2b9 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -303,7 +303,8 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr // Include componentExport name, componentUrl, and props in hash to dedupe identical islands const astroId = shorthash( `\n${html}\n${serializeProps( - props + props, + metadata )}` ); diff --git a/packages/astro/src/runtime/server/render/page.ts b/packages/astro/src/runtime/server/render/page.ts index 166eb01cd..cacaf7dd0 100644 --- a/packages/astro/src/runtime/server/render/page.ts +++ b/packages/astro/src/runtime/server/render/page.ts @@ -79,7 +79,10 @@ export async function renderPage( controller.enqueue(encoder.encode('\n')); } } - controller.enqueue(encoder.encode(html)); + // Convert HTML object to string + // for environments that won't "toString" automatically + // (ex. Cloudflare and Vercel Edge) + controller.enqueue(encoder.encode(String(html))); i++; } controller.close(); diff --git a/packages/astro/src/runtime/server/serialize.ts b/packages/astro/src/runtime/server/serialize.ts index 21996a932..10812ab75 100644 --- a/packages/astro/src/runtime/server/serialize.ts +++ b/packages/astro/src/runtime/server/serialize.ts @@ -1,3 +1,5 @@ +import type { AstroComponentMetadata } from '../../@types/astro'; + type ValueOf = T[keyof T]; const PROP_TYPE = { @@ -11,19 +13,31 @@ const PROP_TYPE = { URL: 7, }; -function serializeArray(value: any[]): any[] { - return value.map((v) => convertToSerializedForm(v)); +function serializeArray(value: any[], metadata: AstroComponentMetadata): any[] { + return value.map((v) => convertToSerializedForm(v, metadata)); } -function serializeObject(value: Record): Record { +function serializeObject( + value: Record, + metadata: AstroComponentMetadata +): Record { + if (cyclicRefs.has(value)) { + throw new Error(`Cyclic reference detected while serializing props for <${metadata.displayName} client:${metadata.hydrate}>! + +Cyclic references cannot be safely serialized for client-side usage. Please remove the cyclic reference.`); + } + cyclicRefs.add(value); return Object.fromEntries( Object.entries(value).map(([k, v]) => { - return [k, convertToSerializedForm(v)]; + return [k, convertToSerializedForm(v, metadata)]; }) ); } -function convertToSerializedForm(value: any): [ValueOf, any] { +function convertToSerializedForm( + value: any, + metadata: AstroComponentMetadata +): [ValueOf, any] { const tag = Object.prototype.toString.call(value); switch (tag) { case '[object Date]': { @@ -33,10 +47,16 @@ function convertToSerializedForm(value: any): [ValueOf, any] { return [PROP_TYPE.RegExp, (value as RegExp).source]; } case '[object Map]': { - return [PROP_TYPE.Map, JSON.stringify(serializeArray(Array.from(value as Map)))]; + return [ + PROP_TYPE.Map, + JSON.stringify(serializeArray(Array.from(value as Map), metadata)), + ]; } case '[object Set]': { - return [PROP_TYPE.Set, JSON.stringify(serializeArray(Array.from(value as Set)))]; + return [ + PROP_TYPE.Set, + JSON.stringify(serializeArray(Array.from(value as Set), metadata)), + ]; } case '[object BigInt]': { return [PROP_TYPE.BigInt, (value as bigint).toString()]; @@ -45,11 +65,11 @@ function convertToSerializedForm(value: any): [ValueOf, any] { return [PROP_TYPE.URL, (value as URL).toString()]; } case '[object Array]': { - return [PROP_TYPE.JSON, JSON.stringify(serializeArray(value))]; + return [PROP_TYPE.JSON, JSON.stringify(serializeArray(value, metadata))]; } default: { if (value !== null && typeof value === 'object') { - return [PROP_TYPE.Value, serializeObject(value)]; + return [PROP_TYPE.Value, serializeObject(value, metadata)]; } else { return [PROP_TYPE.Value, value]; } @@ -57,6 +77,9 @@ function convertToSerializedForm(value: any): [ValueOf, any] { } } -export function serializeProps(props: any) { - return JSON.stringify(serializeObject(props)); +let cyclicRefs = new WeakSet(); +export function serializeProps(props: any, metadata: AstroComponentMetadata) { + const serialized = JSON.stringify(serializeObject(props, metadata)); + cyclicRefs = new WeakSet(); + return serialized; } diff --git a/packages/astro/src/vite-plugin-astro-server/index.ts b/packages/astro/src/vite-plugin-astro-server/index.ts index 525ec0a86..5a550ec18 100644 --- a/packages/astro/src/vite-plugin-astro-server/index.ts +++ b/packages/astro/src/vite-plugin-astro-server/index.ts @@ -120,6 +120,7 @@ async function handle500Response( ) { res.on('close', () => setTimeout(() => viteServer.ws.send(getViteErrorPayload(err)), 200)); if (res.headersSent) { + res.write(``); res.end(); } else { writeHtmlResponse( diff --git a/packages/integrations/alpinejs/CHANGELOG.md b/packages/integrations/alpinejs/CHANGELOG.md index dc46c6a1b..b28097914 100644 --- a/packages/integrations/alpinejs/CHANGELOG.md +++ b/packages/integrations/alpinejs/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/alpinejs +## 0.1.2 + +### Patch Changes + +- [#4622](https://github.com/withastro/astro/pull/4622) [`63cd9d89e`](https://github.com/withastro/astro/commit/63cd9d89e8b83ce5e39cdae84a8342e28d1940cc) Thanks [@mohammed-elhaouari](https://github.com/mohammed-elhaouari)! - Update homepage link + ## 0.1.1 ### Patch Changes diff --git a/packages/integrations/alpinejs/README.md b/packages/integrations/alpinejs/README.md index 3d43b3e25..9eaf45382 100644 --- a/packages/integrations/alpinejs/README.md +++ b/packages/integrations/alpinejs/README.md @@ -18,14 +18,16 @@ The `astro add` command-line tool automates the installation for you. Run one of ```sh # Using NPM -npm run astro add alpinejs +npx astro add alpinejs # Using Yarn yarn astro add alpinejs # Using PNPM pnpm astro add alpinejs ``` -Finally, in the terminal window running Astro, press `CTRL+C` and then type `npm run astro dev` to restart the dev server. +Finally, in the terminal window running Astro, press `CTRL+C` and then restart the dev server. + +If you run into any issues, [feel free to report them to us on GitHub](https://github.com/withastro/astro/issues) and try the manual installation steps below. ### Manual Install diff --git a/packages/integrations/alpinejs/package.json b/packages/integrations/alpinejs/package.json index b19ca0359..44f924e6a 100644 --- a/packages/integrations/alpinejs/package.json +++ b/packages/integrations/alpinejs/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/alpinejs", "description": "The official Alpine.js integration for Astro.", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/image/CHANGELOG.md b/packages/integrations/image/CHANGELOG.md index e88f9b4a2..8e79766bc 100644 --- a/packages/integrations/image/CHANGELOG.md +++ b/packages/integrations/image/CHANGELOG.md @@ -1,5 +1,21 @@ # @astrojs/image +## 0.6.0 + +### Minor Changes + +- [#4642](https://github.com/withastro/astro/pull/4642) [`e4348a4eb`](https://github.com/withastro/astro/commit/e4348a4eb49466579204eb5f7fb8823736f467c0) Thanks [@beeb](https://github.com/beeb)! - Added a `background` option to specify a background color to replace transparent pixels (alpha layer). + +### Patch Changes + +- [#4649](https://github.com/withastro/astro/pull/4649) [`db70afdcd`](https://github.com/withastro/astro/commit/db70afdcd5b7d6b39c9953e88dbdadc5e3a93175) Thanks [@tony-sull](https://github.com/tony-sull)! - Fixes a bug related to filenames for remote images in SSG builds + +## 0.5.1 + +### Patch Changes + +- [#4626](https://github.com/withastro/astro/pull/4626) [`494c2b835`](https://github.com/withastro/astro/commit/494c2b8353d1975d840c5acaf70cb513b99c58e5) Thanks [@altano](https://github.com/altano)! - Parallelize image transforms + ## 0.5.0 ### Minor Changes diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index 394db59de..fda9727e6 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -24,24 +24,24 @@ This integration provides `` and `` components as well as a ba ### Quick Install - + The `astro add` command-line tool automates the installation for you. Run one of the following commands in a new terminal window. (If you aren't sure which package manager you're using, run the first command.) Then, follow the prompts, and type "y" in the terminal (meaning "yes") for each one. - + ```sh # Using NPM -npm run astro add image +npx astro add image # Using Yarn yarn astro add image # Using PNPM pnpm astro add image ``` - -Then, restart the dev server by typing `CTRL-C` and then `npm run astro dev` in the terminal window that was running Astro. - -Because this command is new, it might not properly set things up. If that happens, [feel free to log an issue on our GitHub](https://github.com/withastro/astro/issues) and try the manual installation steps below. + +Finally, in the terminal window running Astro, press `CTRL+C` and then restart the dev server. + +If you run into any issues, [feel free to report them to us on GitHub](https://github.com/withastro/astro/issues) and try the manual installation steps below. ### Manual Install - + First, install the `@astrojs/image` package using your package manager. If you're using npm or aren't sure, run this in the terminal: ```sh npm install @astrojs/image @@ -57,7 +57,7 @@ export default { // ... integrations: [image()], } -``` +``` Then, restart the dev server. ### Update `env.d.ts` @@ -190,6 +190,24 @@ A `string` can be provided in the form of `{width}:{height}`, ex: `16:9` or `3:4 A `number` can also be provided, useful when the aspect ratio is calculated at build time. This can be an inline number such as `1.777` or inlined as a JSX expression like `aspectRatio={16/9}`. +#### background + +

+ +**Type:** `ColorDefinition`
+**Default:** `undefined` +

+ +The background color to use for replacing the alpha channel with `sharp`'s `flatten` method. In case the output format +doesn't support transparency (i.e. `jpeg`), it's advisable to include a background color, otherwise black will be used +as default replacement for transparent pixels. + +The parameter accepts a `string` as value. + +The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_colornames.asp), a hexadecimal +color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form +`rgb(100,100,100)`. + ### ` #### src @@ -271,6 +289,24 @@ A `number` can also be provided, useful when the aspect ratio is calculated at b The output formats to be used in the optimized image. If not provided, `webp` and `avif` will be used in addition to the original image format. +#### background + +

+ +**Type:** `ColorDefinition`
+**Default:** `undefined` +

+ +The background color to use for replacing the alpha channel with `sharp`'s `flatten` method. In case the output format +doesn't support transparency (i.e. `jpeg`), it's advisable to include a background color, otherwise black will be used +as default replacement for transparent pixels. + +The parameter accepts a `string` as value. + +The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_colornames.asp), a hexadecimal +color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form +`rgb(100,100,100)`. + ### `getImage` This is the helper function used by the `` component to build `` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `` component. @@ -307,7 +343,7 @@ The integration can be configured to run with a different image service, either ### config.serviceEntryPoint - + The `serviceEntryPoint` should resolve to the image service installed from NPM. The default entry point is `@astrojs/image/sharp`, which resolves to the entry point exported from this integration's `package.json`. ```js @@ -342,7 +378,7 @@ export default { ## Examples ### Local images - + Image files in your project's `src` directory can be imported in frontmatter and passed directly to the `` component. All other properties are optional and will default to the original image file's properties if not provided. ```astro @@ -371,7 +407,7 @@ import heroImage from '../assets/hero.png'; Files in the `/public` directory are always served or copied as-is, with no processing. We recommend that local images are always kept in `src/` so that Astro can transform, optimize and bundle them. But if you absolutely must keep an image in `public/`, use its relative URL path as the image's `src=` attribute. It will be treated as a remote image, which requires an `aspectRatio` attribute. -Alternatively, you can import an image from your `public/` directory in your frontmatter and use a variable in your `src=` attribute. You cannot, however, import this directly inside the component as its `src` value. +Alternatively, you can import an image from your `public/` directory in your frontmatter and use a variable in your `src=` attribute. You cannot, however, import this directly inside the component as its `src` value. For example, use an image located at `public/social.png` in either static or SSR builds like so: @@ -386,7 +422,7 @@ import socialImage from '/social.png'; ``` ### Remote images - + Remote images can be transformed with the `` component. The `` component needs to know the final dimensions for the `` element to avoid content layout shifts. For remote images, this means you must either provide `width` and `height`, or one of the dimensions plus the required `aspectRatio`. ```astro diff --git a/packages/integrations/image/components/Image.astro b/packages/integrations/image/components/Image.astro index 1777fffab..578db702d 100644 --- a/packages/integrations/image/components/Image.astro +++ b/packages/integrations/image/components/Image.astro @@ -17,7 +17,7 @@ interface RemoteImageProps extends TransformOptions, astroHTML.JSX.ImgHTMLAttrib src: string; /** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */ alt: string; - format: OutputFormat; + format?: OutputFormat; width: number; height: number; } diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index 7fe43d9db..e28f5bf40 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -27,6 +27,7 @@ interface RemoteImageProps widths: number[]; aspectRatio: TransformOptions['aspectRatio']; formats?: OutputFormat[]; + background: TransformOptions['background']; } export type Props = LocalImageProps | RemoteImageProps; @@ -37,6 +38,7 @@ const { sizes, widths, aspectRatio, + background, formats = ['avif', 'webp'], loading = 'lazy', decoding = 'async', @@ -47,7 +49,7 @@ if (alt === undefined || alt === null) { warnForMissingAlt(); } -const { image, sources } = await getPicture({ src, widths, formats, aspectRatio }); +const { image, sources } = await getPicture({ src, widths, formats, aspectRatio, background }); --- diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index 4d342f75a..250c5cc5e 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/image", "description": "Load and transform images in your Astro site.", - "version": "0.5.0", + "version": "0.6.0", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/image/src/lib/get-image.ts b/packages/integrations/image/src/lib/get-image.ts index a28db2861..c6f5ab941 100644 --- a/packages/integrations/image/src/lib/get-image.ts +++ b/packages/integrations/image/src/lib/get-image.ts @@ -1,5 +1,10 @@ /// -import type { ImageService, OutputFormat, TransformOptions } from '../loaders/index.js'; +import type { + ColorDefinition, + ImageService, + OutputFormat, + TransformOptions, +} from '../loaders/index.js'; import { isSSRService, parseAspectRatio } from '../loaders/index.js'; import sharp from '../loaders/squoosh.js'; import { isRemoteImage } from '../utils/paths.js'; @@ -63,7 +68,7 @@ async function resolveTransform(input: GetImageTransform): Promise { @@ -64,6 +65,7 @@ export async function getPicture(params: GetPictureParams): Promise getSource(format))); diff --git a/packages/integrations/image/src/loaders/colornames.ts b/packages/integrations/image/src/loaders/colornames.ts new file mode 100644 index 000000000..806e55f31 --- /dev/null +++ b/packages/integrations/image/src/loaders/colornames.ts @@ -0,0 +1,290 @@ +export type NamedColor = + | 'aliceblue' + | 'antiquewhite' + | 'aqua' + | 'aquamarine' + | 'azure' + | 'beige' + | 'bisque' + | 'black' + | 'blanchedalmond' + | 'blue' + | 'blueviolet' + | 'brown' + | 'burlywood' + | 'cadetblue' + | 'chartreuse' + | 'chocolate' + | 'coral' + | 'cornflowerblue' + | 'cornsilk' + | 'crimson' + | 'cyan' + | 'darkblue' + | 'darkcyan' + | 'darkgoldenrod' + | 'darkgray' + | 'darkgreen' + | 'darkkhaki' + | 'darkmagenta' + | 'darkolivegreen' + | 'darkorange' + | 'darkorchid' + | 'darkred' + | 'darksalmon' + | 'darkseagreen' + | 'darkslateblue' + | 'darkslategray' + | 'darkturquoise' + | 'darkviolet' + | 'deeppink' + | 'deepskyblue' + | 'dimgray' + | 'dodgerblue' + | 'firebrick' + | 'floralwhite' + | 'forestgreen' + | 'fuchsia' + | 'gainsboro' + | 'ghostwhite' + | 'gold' + | 'goldenrod' + | 'gray' + | 'green' + | 'greenyellow' + | 'honeydew' + | 'hotpink' + | 'indianred' + | 'indigo' + | 'ivory' + | 'khaki' + | 'lavender' + | 'lavenderblush' + | 'lawngreen' + | 'lemonchiffon' + | 'lightblue' + | 'lightcoral' + | 'lightcyan' + | 'lightgoldenrodyellow' + | 'lightgray' + | 'lightgreen' + | 'lightpink' + | 'lightsalmon' + | 'lightsalmon' + | 'lightseagreen' + | 'lightskyblue' + | 'lightslategray' + | 'lightsteelblue' + | 'lightyellow' + | 'lime' + | 'limegreen' + | 'linen' + | 'magenta' + | 'maroon' + | 'mediumaquamarine' + | 'mediumblue' + | 'mediumorchid' + | 'mediumpurple' + | 'mediumseagreen' + | 'mediumslateblue' + | 'mediumslateblue' + | 'mediumspringgreen' + | 'mediumturquoise' + | 'mediumvioletred' + | 'midnightblue' + | 'mintcream' + | 'mistyrose' + | 'moccasin' + | 'navajowhite' + | 'navy' + | 'oldlace' + | 'olive' + | 'olivedrab' + | 'orange' + | 'orangered' + | 'orchid' + | 'palegoldenrod' + | 'palegreen' + | 'paleturquoise' + | 'palevioletred' + | 'papayawhip' + | 'peachpuff' + | 'peru' + | 'pink' + | 'plum' + | 'powderblue' + | 'purple' + | 'rebeccapurple' + | 'red' + | 'rosybrown' + | 'royalblue' + | 'saddlebrown' + | 'salmon' + | 'sandybrown' + | 'seagreen' + | 'seashell' + | 'sienna' + | 'silver' + | 'skyblue' + | 'slateblue' + | 'slategray' + | 'snow' + | 'springgreen' + | 'steelblue' + | 'tan' + | 'teal' + | 'thistle' + | 'tomato' + | 'turquoise' + | 'violet' + | 'wheat' + | 'white' + | 'whitesmoke' + | 'yellow' + | 'yellowgreen'; + +export const htmlColorNames: NamedColor[] = [ + 'aliceblue', + 'antiquewhite', + 'aqua', + 'aquamarine', + 'azure', + 'beige', + 'bisque', + 'black', + 'blanchedalmond', + 'blue', + 'blueviolet', + 'brown', + 'burlywood', + 'cadetblue', + 'chartreuse', + 'chocolate', + 'coral', + 'cornflowerblue', + 'cornsilk', + 'crimson', + 'cyan', + 'darkblue', + 'darkcyan', + 'darkgoldenrod', + 'darkgray', + 'darkgreen', + 'darkkhaki', + 'darkmagenta', + 'darkolivegreen', + 'darkorange', + 'darkorchid', + 'darkred', + 'darksalmon', + 'darkseagreen', + 'darkslateblue', + 'darkslategray', + 'darkturquoise', + 'darkviolet', + 'deeppink', + 'deepskyblue', + 'dimgray', + 'dodgerblue', + 'firebrick', + 'floralwhite', + 'forestgreen', + 'fuchsia', + 'gainsboro', + 'ghostwhite', + 'gold', + 'goldenrod', + 'gray', + 'green', + 'greenyellow', + 'honeydew', + 'hotpink', + 'indianred', + 'indigo', + 'ivory', + 'khaki', + 'lavender', + 'lavenderblush', + 'lawngreen', + 'lemonchiffon', + 'lightblue', + 'lightcoral', + 'lightcyan', + 'lightgoldenrodyellow', + 'lightgray', + 'lightgreen', + 'lightpink', + 'lightsalmon', + 'lightsalmon', + 'lightseagreen', + 'lightskyblue', + 'lightslategray', + 'lightsteelblue', + 'lightyellow', + 'lime', + 'limegreen', + 'linen', + 'magenta', + 'maroon', + 'mediumaquamarine', + 'mediumblue', + 'mediumorchid', + 'mediumpurple', + 'mediumseagreen', + 'mediumslateblue', + 'mediumslateblue', + 'mediumspringgreen', + 'mediumturquoise', + 'mediumvioletred', + 'midnightblue', + 'mintcream', + 'mistyrose', + 'moccasin', + 'navajowhite', + 'navy', + 'oldlace', + 'olive', + 'olivedrab', + 'orange', + 'orangered', + 'orchid', + 'palegoldenrod', + 'palegreen', + 'paleturquoise', + 'palevioletred', + 'papayawhip', + 'peachpuff', + 'peru', + 'pink', + 'plum', + 'powderblue', + 'purple', + 'rebeccapurple', + 'red', + 'rosybrown', + 'royalblue', + 'saddlebrown', + 'salmon', + 'sandybrown', + 'seagreen', + 'seashell', + 'sienna', + 'silver', + 'skyblue', + 'slateblue', + 'slategray', + 'snow', + 'springgreen', + 'steelblue', + 'tan', + 'teal', + 'thistle', + 'tomato', + 'turquoise', + 'violet', + 'wheat', + 'white', + 'whitesmoke', + 'yellow', + 'yellowgreen', +]; diff --git a/packages/integrations/image/src/loaders/index.ts b/packages/integrations/image/src/loaders/index.ts index 9da71b448..c031b60f0 100644 --- a/packages/integrations/image/src/loaders/index.ts +++ b/packages/integrations/image/src/loaders/index.ts @@ -1,3 +1,5 @@ +import { htmlColorNames, type NamedColor } from './colornames.js'; + /// export type InputFormat = | 'heic' @@ -10,16 +12,35 @@ export type InputFormat = | 'webp' | 'gif'; -export type OutputFormat = 'avif' | 'jpeg' | 'jpg' | 'png' | 'webp'; +export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp'; +export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg'; + +export type ColorDefinition = + | NamedColor + | `#${string}` + | `rgb(${number}, ${number}, ${number})` + | `rgb(${number},${number},${number})`; export function isOutputFormat(value: string): value is OutputFormat { return ['avif', 'jpeg', 'jpg', 'png', 'webp'].includes(value); } +export function isOutputFormatSupportsAlpha(value: string): value is OutputFormatSupportsAlpha { + return ['avif', 'png', 'webp'].includes(value); +} + export function isAspectRatioString(value: string): value is `${number}:${number}` { return /^\d*:\d*$/.test(value); } +export function isColor(value: string): value is ColorDefinition { + return ( + (htmlColorNames as string[]).includes(value.toLowerCase()) || + /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value) || + /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(value) + ); +} + export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) { if (!aspectRatio) { return undefined; @@ -75,6 +96,15 @@ export interface TransformOptions { * @example "16:9" - strings can be used in the format of `{ratioWidth}:{ratioHeight}`. */ aspectRatio?: number | `${number}:${number}`; + /** + * The background color to use when converting from a transparent image format to a + * non-transparent format. This is useful for converting PNGs to JPEGs. + * + * @example "white" - a named color + * @example "#ffffff" - a hex color + * @example "rgb(255, 255, 255)" - an rgb color + */ + background?: ColorDefinition; } export interface HostedImageService { @@ -161,6 +191,10 @@ export abstract class BaseSSRService implements SSRImageService { searchParams.append('ar', transform.aspectRatio.toString()); } + if (transform.background) { + searchParams.append('bg', transform.background); + } + searchParams.append('href', transform.src); return { searchParams }; @@ -202,6 +236,10 @@ export abstract class BaseSSRService implements SSRImageService { } } + if (searchParams.has('bg')) { + transform.background = searchParams.get('bg') as ColorDefinition; + } + return transform; } diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index 591023ee4..0ec918687 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -15,6 +15,11 @@ class SharpService extends BaseSSRService { sharpImage.resize(width, height); } + // remove alpha channel and replace with background color if requested + if (transform.background) { + sharpImage.flatten({ background: transform.background }); + } + if (transform.format) { sharpImage.toFormat(transform.format, { quality: transform.quality }); } diff --git a/packages/integrations/image/src/utils/paths.ts b/packages/integrations/image/src/utils/paths.ts index cf62ba0cf..1f691f35a 100644 --- a/packages/integrations/image/src/utils/paths.ts +++ b/packages/integrations/image/src/utils/paths.ts @@ -14,7 +14,7 @@ function extname(src: string, format?: OutputFormat) { const index = src.lastIndexOf('.'); if (index <= 0) { - return undefined; + return ''; } return src.substring(index); @@ -38,11 +38,12 @@ export function propsToFilename(transform: TransformOptions) { // strip off the querystring first, then remove the file extension let filename = removeQueryString(transform.src); filename = basename(filename); + const ext = extname(filename); filename = removeExtname(filename); - const ext = transform.format || extname(transform.src)?.substring(1); + const outputExt = transform.format ? `.${transform.format}` : ext; - return `/${filename}_${shorthash(JSON.stringify(transform))}.${ext}`; + return `/${filename}_${shorthash(JSON.stringify(transform))}${outputExt}`; } export function appendForwardSlash(path: string) { diff --git a/packages/integrations/image/test/background-color-image-ssg.test.js b/packages/integrations/image/test/background-color-image-ssg.test.js new file mode 100644 index 000000000..3c488a3ff --- /dev/null +++ b/packages/integrations/image/test/background-color-image-ssg.test.js @@ -0,0 +1,116 @@ +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import sharp from 'sharp'; +import { fileURLToPath } from 'url'; +import { loadFixture } from './test-utils.js'; + +describe('SSG image with background - dev', function () { + let fixture; + let devServer; + let $; + + before(async () => { + fixture = await loadFixture({ root: './fixtures/background-color-image/' }); + devServer = await fixture.startDevServer(); + const html = await fixture.fetch('/').then((res) => res.text()); + $ = cheerio.load(html); + }); + + after(async () => { + await devServer.stop(); + }); + + [ + { + title: 'Named color', + id: '#named', + bg: 'dimgray', + }, + { + title: 'Hex color', + id: '#hex', + bg: '#696969', + }, + { + title: 'Hex color short', + id: '#hex-short', + bg: '#666', + }, + { + title: 'RGB color', + id: '#rgb', + bg: 'rgb(105,105,105)', + }, + { + title: 'RGB color with spaces', + id: '#rgb-spaced', + bg: 'rgb(105, 105, 105)', + }, + ].forEach(({ title, id, bg }) => { + it(title, async () => { + const image = $(id); + const src = image.attr('src'); + const [_, params] = src.split('?'); + const searchParams = new URLSearchParams(params); + expect(searchParams.get('bg')).to.equal(bg); + }); + }); +}); + +describe('SSG image with background - build', function () { + let fixture; + let $; + let html; + + before(async () => { + fixture = await loadFixture({ root: './fixtures/background-color-image/' }); + await fixture.build(); + + html = await fixture.readFile('/index.html'); + $ = cheerio.load(html); + }); + + async function verifyImage(pathname, expectedBg) { + const url = new URL('./fixtures/background-color-image/dist/' + pathname, import.meta.url); + const dist = fileURLToPath(url); + const data = await sharp(dist).raw().toBuffer(); + // check that the first RGB pixel indeed has the requested background color + expect(data[0]).to.equal(expectedBg[0]); + expect(data[1]).to.equal(expectedBg[1]); + expect(data[2]).to.equal(expectedBg[2]); + } + + [ + { + title: 'Named color', + id: '#named', + bg: [105, 105, 105], + }, + { + title: 'Hex color', + id: '#hex', + bg: [105, 105, 105], + }, + { + title: 'Hex color short', + id: '#hex-short', + bg: [102, 102, 102], + }, + { + title: 'RGB color', + id: '#rgb', + bg: [105, 105, 105], + }, + { + title: 'RGB color with spaces', + id: '#rgb-spaced', + bg: [105, 105, 105], + }, + ].forEach(({ title, id, bg }) => { + it(title, async () => { + const image = $(id); + const src = image.attr('src'); + await verifyImage(src, bg); + }); + }); +}); diff --git a/packages/integrations/image/test/background-color-image-ssr.test.js b/packages/integrations/image/test/background-color-image-ssr.test.js new file mode 100644 index 000000000..ff4c208f8 --- /dev/null +++ b/packages/integrations/image/test/background-color-image-ssr.test.js @@ -0,0 +1,98 @@ +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; +import testAdapter from '../../../astro/test/test-adapter.js'; + +let fixture; + +describe('SSR image with background', function () { + before(async () => { + fixture = await loadFixture({ + root: './fixtures/background-color-image/', + adapter: testAdapter({ streaming: false }), + output: 'server', + }); + await fixture.build(); + }); + + [ + { + title: 'Named color', + id: '#named', + query: { + f: 'jpeg', + w: '256', + h: '256', + href: /^\/assets\/file-icon.\w{8}.png/, + bg: 'dimgray', + }, + }, + { + title: 'Hex color', + id: '#hex', + query: { + f: 'avif', + w: '256', + h: '256', + href: /^\/assets\/file-icon.\w{8}.png/, + bg: '#696969', + }, + }, + { + title: 'Hex color short', + id: '#hex-short', + query: { + f: 'png', + w: '256', + h: '256', + href: /^\/assets\/file-icon.\w{8}.png/, + bg: '#666', + }, + }, + { + title: 'RGB color', + id: '#rgb', + query: { + f: 'webp', + w: '256', + h: '256', + href: /^\/assets\/file-icon.\w{8}.png/, + bg: 'rgb(105,105,105)', + }, + }, + { + title: 'RGB color with spaces', + id: '#rgb-spaced', + query: { + f: 'jpeg', + w: '256', + h: '256', + href: /^\/assets\/file-icon.\w{8}.png/, + bg: 'rgb(105, 105, 105)', + }, + }, + ].forEach(({ title, id, query }) => { + it(title, async () => { + const app = await fixture.loadTestAdapterApp(); + + const request = new Request('http://example.com/'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerio.load(html); + + const image = $(id); + const src = image.attr('src'); + const [_, params] = src.split('?'); + + const searchParams = new URLSearchParams(params); + + for (const [key, value] of Object.entries(query)) { + if (typeof value === 'string') { + expect(searchParams.get(key)).to.equal(value); + } else { + expect(searchParams.get(key)).to.match(value); + } + } + }); + }); +}); diff --git a/packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs b/packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs new file mode 100644 index 000000000..7dafac3b6 --- /dev/null +++ b/packages/integrations/image/test/fixtures/background-color-image/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import image from '@astrojs/image'; + +// https://astro.build/config +export default defineConfig({ + site: 'http://localhost:3000', + integrations: [image({ logLevel: 'silent' })] +}); diff --git a/packages/integrations/image/test/fixtures/background-color-image/package.json b/packages/integrations/image/test/fixtures/background-color-image/package.json new file mode 100644 index 000000000..bca4ff178 --- /dev/null +++ b/packages/integrations/image/test/fixtures/background-color-image/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/background-color-image", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/image": "workspace:*", + "@astrojs/node": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico b/packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico new file mode 100644 index 000000000..578ad458b Binary files /dev/null and b/packages/integrations/image/test/fixtures/background-color-image/public/favicon.ico differ diff --git a/packages/integrations/image/test/fixtures/background-color-image/server/server.mjs b/packages/integrations/image/test/fixtures/background-color-image/server/server.mjs new file mode 100644 index 000000000..d7a0a7a40 --- /dev/null +++ b/packages/integrations/image/test/fixtures/background-color-image/server/server.mjs @@ -0,0 +1,44 @@ +import { createServer } from 'http'; +import fs from 'fs'; +import mime from 'mime'; +import { handler as ssrHandler } from '../dist/server/entry.mjs'; + +const clientRoot = new URL('../dist/client/', import.meta.url); + +async function handle(req, res) { + ssrHandler(req, res, async (err) => { + if (err) { + res.writeHead(500); + res.end(err.stack); + return; + } + + let local = new URL('.' + req.url, clientRoot); + try { + const data = await fs.promises.readFile(local); + res.writeHead(200, { + 'Content-Type': mime.getType(req.url), + }); + res.end(data); + } catch { + res.writeHead(404); + res.end(); + } + }); +} + +const server = createServer((req, res) => { + handle(req, res).catch((err) => { + console.error(err); + res.writeHead(500, { + 'Content-Type': 'text/plain', + }); + res.end(err.toString()); + }); +}); + +server.listen(8085); +console.log('Serving at http://localhost:8085'); + +// Silence weird