diff --git a/.changeset/breezy-frogs-learn.md b/.changeset/breezy-frogs-learn.md deleted file mode 100644 index b3f2f86b9..000000000 --- a/.changeset/breezy-frogs-learn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/cloudflare': minor ---- - -More efficient \_routes.json diff --git a/.changeset/green-islands-repeat.md b/.changeset/green-islands-repeat.md new file mode 100644 index 000000000..9894fde96 --- /dev/null +++ b/.changeset/green-islands-repeat.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix AstroConfigSchema type export diff --git a/.changeset/lazy-pillows-burn.md b/.changeset/lazy-pillows-burn.md new file mode 100644 index 000000000..009955c7e --- /dev/null +++ b/.changeset/lazy-pillows-burn.md @@ -0,0 +1,5 @@ +--- +'@astrojs/sitemap': patch +--- + +docs: fix github search link in README.md diff --git a/.changeset/moody-houses-drum.md b/.changeset/moody-houses-drum.md new file mode 100644 index 000000000..1dfaefba2 --- /dev/null +++ b/.changeset/moody-houses-drum.md @@ -0,0 +1,5 @@ +--- +'create-astro': minor +--- + +Reduce dependency installation size, swap `execa` for light `node:child_process` wrapper diff --git a/.changeset/odd-plants-tie.md b/.changeset/odd-plants-tie.md new file mode 100644 index 000000000..b57376dee --- /dev/null +++ b/.changeset/odd-plants-tie.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Add support for non-awaited imports to the Image component and `getImage` diff --git a/.changeset/olive-queens-drum.md b/.changeset/olive-queens-drum.md new file mode 100644 index 000000000..258d9c726 --- /dev/null +++ b/.changeset/olive-queens-drum.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Add second type argument to the AstroGlobal type to type Astro.self. This change will ultimately allow our editor tooling to provide props completions and intellisense for `` diff --git a/.changeset/soft-colts-heal.md b/.changeset/soft-colts-heal.md new file mode 100644 index 000000000..4ceaf2a9b --- /dev/null +++ b/.changeset/soft-colts-heal.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +`astro add` now passes down `--save-prod`, `--save-dev`, `--save-exact`, and `--no-save` flags for installation diff --git a/.changeset/thin-plums-drop.md b/.changeset/thin-plums-drop.md new file mode 100644 index 000000000..ab3fb875a --- /dev/null +++ b/.changeset/thin-plums-drop.md @@ -0,0 +1,5 @@ +--- +'@astrojs/react': patch +--- + +fix a bug where react identifierPrefix was set to null for client:only components causing React.useId to generate ids prefixed with null diff --git a/.changeset/wild-jobs-tan.md b/.changeset/wild-jobs-tan.md deleted file mode 100644 index 8372c01b1..000000000 --- a/.changeset/wild-jobs-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Move hoisted script analysis optimization behind the `experimental.optimizeHoistedScript` option diff --git a/examples/blog/public/blog-placeholder-5.jpg b/examples/blog/public/blog-placeholder-5.jpg index 1a59ad9b6..c5646746c 100644 Binary files a/examples/blog/public/blog-placeholder-5.jpg and b/examples/blog/public/blog-placeholder-5.jpg differ diff --git a/examples/blog/public/placeholder-about.jpg b/examples/blog/public/placeholder-about.jpg deleted file mode 100644 index 2f736d92a..000000000 Binary files a/examples/blog/public/placeholder-about.jpg and /dev/null differ diff --git a/examples/blog/src/components/Header.astro b/examples/blog/src/components/Header.astro index 647320834..c9ab99f1d 100644 --- a/examples/blog/src/components/Header.astro +++ b/examples/blog/src/components/Header.astro @@ -23,7 +23,7 @@ import { SITE_TITLE } from '../consts'; Follow Astro on Twitter -

{id}

; +} diff --git a/packages/astro/e2e/fixtures/react-component/src/pages/index.astro b/packages/astro/e2e/fixtures/react-component/src/pages/index.astro index f9e0ae395..a3a0a4c1f 100644 --- a/packages/astro/e2e/fixtures/react-component/src/pages/index.astro +++ b/packages/astro/e2e/fixtures/react-component/src/pages/index.astro @@ -2,6 +2,7 @@ import Counter from '../components/Counter.jsx'; import ReactComponent from '../components/JSXComponent.jsx'; import Suffix from '../components/Suffix.react'; +import WithId from '../components/WithId.jsx'; const someProps = { count: 0, @@ -36,5 +37,11 @@ const someProps = { + + + + + + diff --git a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs index c0df0074c..8a9f43bcc 100644 --- a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs +++ b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs @@ -1,8 +1,11 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; +import nodejs from '@astrojs/node'; // https://astro.build/config export default defineConfig({ + output: 'server', + adapter: nodejs({ mode: 'standalone' }), integrations: [react()], experimental: { viewTransitions: true, diff --git a/packages/astro/e2e/fixtures/view-transitions/package.json b/packages/astro/e2e/fixtures/view-transitions/package.json index 90a07f839..f4ba9b17b 100644 --- a/packages/astro/e2e/fixtures/view-transitions/package.json +++ b/packages/astro/e2e/fixtures/view-transitions/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "astro": "workspace:*", + "@astrojs/node": "workspace:*", "@astrojs/react": "workspace:*", "react": "^18.1.0", "react-dom": "^18.1.0" diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro b/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro index 7016a1bf7..1c0f9bbdf 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro @@ -20,6 +20,16 @@ const { link } = Astro.props as Props; + +
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro new file mode 100644 index 000000000..44dd03ce0 --- /dev/null +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro @@ -0,0 +1,9 @@ +--- +import Layout from '../components/Layout.astro'; + +const page = Astro.url.searchParams.get('page') || 1; +--- + +

Page {page}

+
go to 2 + diff --git a/packages/astro/e2e/react-component.test.js b/packages/astro/e2e/react-component.test.js index 00d747079..b19a071d6 100644 --- a/packages/astro/e2e/react-component.test.js +++ b/packages/astro/e2e/react-component.test.js @@ -34,3 +34,22 @@ test.describe('dev', () => { expect(await suffix.textContent()).toBe('suffix toggle true'); }); }); + +test.describe('React client id generation', () => { + test('react components generate unique ids', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + const components = page.locator('.react-use-id'); + await expect(components).toHaveCount(5); + const staticId = await components.nth(0).getAttribute('id'); + const hydratedId0 = await components.nth(1).getAttribute('id'); + const hydratedId1 = await components.nth(2).getAttribute('id'); + const clientOnlyId0 = await components.nth(3).getAttribute('id'); + const clientOnlyId1 = await components.nth(4).getAttribute('id'); + console.log('ho ho', staticId, hydratedId0, hydratedId1, clientOnlyId0, clientOnlyId1); + expect(staticId).not.toEqual(hydratedId0); + expect(hydratedId0).not.toEqual(hydratedId1); + expect(hydratedId1).not.toEqual(clientOnlyId0); + expect(clientOnlyId0).not.toEqual(clientOnlyId1); + }); +}); diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index f30cd9902..7aeb6502a 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -279,4 +279,41 @@ test.describe('View Transitions', () => { // Count should remain await expect(cnt).toHaveText('6'); }); + + test('Scripts are only executed once', async ({ page, astro }) => { + // Go to page 1 + await page.goto(astro.resolveUrl('/one')); + const p = page.locator('#one'); + await expect(p, 'should have content').toHaveText('Page 1'); + + // go to page 2 + await page.click('#click-two'); + const article = page.locator('#twoarticle'); + await expect(article, 'should have script content').toHaveText('works'); + + const meta = page.locator('[name="script-executions"]'); + await expect(meta).toHaveAttribute('content', '0'); + }); + + test('Navigating to the same path but with different query params should result in transition', async ({ + page, + astro, + }) => { + const loads = []; + page.addListener('load', (p) => { + loads.push(p.title()); + }); + + // Go to page 1 + await page.goto(astro.resolveUrl('/query')); + let p = page.locator('#query-page'); + await expect(p, 'should have content').toHaveText('Page 1'); + + // go to page 2 + await page.click('#click-two'); + p = page.locator('#query-page'); + await expect(p, 'should have content').toHaveText('Page 2'); + + await expect(loads.length, 'There should only be 1 page load').toEqual(1); + }); }); diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 8bdd35173..d070b9825 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -13,10 +13,9 @@ import type { AddressInfo } from 'node:net'; import type * as rollup from 'rollup'; import type { TsConfigJson } from 'tsconfig-resolver'; import type * as vite from 'vite'; -import type { z } from 'zod'; import type { SerializedSSRManifest } from '../core/app/types'; import type { PageBuildData } from '../core/build/types'; -import type { AstroConfigSchema } from '../core/config'; +import type { AstroConfigType } from '../core/config'; import type { AstroTimer } from '../core/config/timer'; import type { AstroCookies } from '../core/cookies'; import type { LogOptions, LoggerLevel } from '../core/logger/core'; @@ -141,8 +140,10 @@ export interface CLIFlags { * * [Astro reference](https://docs.astro.build/reference/api-reference/#astro-global) */ -export interface AstroGlobal = Record> - extends AstroGlobalPartial, +export interface AstroGlobal< + Props extends Record = Record, + Self = AstroComponentFactory +> extends AstroGlobalPartial, AstroSharedContext { /** * A full URL object of the request URL. @@ -219,7 +220,7 @@ export interface AstroGlobal = Record { +export interface AstroConfig extends AstroConfigType { // Public: // This is a more detailed type than zod validation gives us. // TypeScript still confirms zod validation matches this type. diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index c56b5369c..06e4f8cc0 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -1,7 +1,12 @@ import type { AstroSettings } from '../@types/astro.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { isLocalService, type ImageService } from './services/service.js'; -import type { GetImageResult, ImageMetadata, ImageTransform } from './types.js'; +import type { + GetImageResult, + ImageMetadata, + ImageTransform, + UnresolvedImageTransform, +} from './types.js'; export function injectImageEndpoint(settings: AstroSettings) { settings.injectedRoutes.push({ @@ -37,7 +42,7 @@ export async function getConfiguredImageService(): Promise { } export async function getImage( - options: ImageTransform, + options: ImageTransform | UnresolvedImageTransform, serviceConfig: Record ): Promise { if (!options || typeof options !== 'object') { @@ -48,9 +53,19 @@ export async function getImage( } const service = await getConfiguredImageService(); + + // If the user inlined an import, something fairly common especially in MDX, await it for them + const resolvedOptions: ImageTransform = { + ...options, + src: + typeof options.src === 'object' && 'then' in options.src + ? (await options.src).default + : options.src, + }; + const validatedOptions = service.validateOptions - ? await service.validateOptions(options, serviceConfig) - : options; + ? await service.validateOptions(resolvedOptions, serviceConfig) + : resolvedOptions; let imageURL = await service.getURL(validatedOptions, serviceConfig); @@ -60,7 +75,7 @@ export async function getImage( } return { - rawOptions: options, + rawOptions: resolvedOptions, options: validatedOptions, src: imageURL, attributes: diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts index 5632d7691..9c5990cb7 100644 --- a/packages/astro/src/assets/types.ts +++ b/packages/astro/src/assets/types.ts @@ -27,6 +27,10 @@ export interface ImageMetadata { orientation?: number; } +export type UnresolvedImageTransform = Omit & { + src: Promise<{ default: ImageMetadata }>; +}; + /** * Options accepted by the image transformation service. */ @@ -93,7 +97,7 @@ export type LocalImageProps = ImageSharedProps & { * ... * ``` */ - src: ImageMetadata; + src: ImageMetadata | Promise<{ default: ImageMetadata }>; /** * Desired output format for the image. Defaults to `webp`. * diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index 55502fc40..f09d74a08 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -628,6 +628,18 @@ async function getInstallIntegrationsCommand({ } } +// Allow forwarding of standard `npm install` flags +// See https://docs.npmjs.com/cli/v8/commands/npm-install#description +const INHERITED_FLAGS = new Set([ + 'P', + 'save-prod', + 'D', + 'save-dev', + 'E', + 'save-exact', + 'no-save', +]); + async function tryToInstallIntegrations({ integrations, cwd, @@ -641,12 +653,24 @@ async function tryToInstallIntegrations({ }): Promise { const installCommand = await getInstallIntegrationsCommand({ integrations, cwd }); + const inheritedFlags = Object.entries(flags) + .map(([flag]) => { + if (flag == '_') return; + if (INHERITED_FLAGS.has(flag)) { + if (flag.length === 1) return `-${flag}`; + return `--${flag}`; + } + }) + .filter(Boolean) + .flat() as string[]; + if (installCommand === null) { return UpdateResult.none; } else { const coloredOutput = `${bold(installCommand.pm)} ${installCommand.command}${[ '', ...installCommand.flags, + ...inheritedFlags, ].join(' ')} ${cyan(installCommand.dependencies.join(' '))}`; const message = `\n${boxen(coloredOutput, { margin: 0.5, @@ -666,14 +690,20 @@ async function tryToInstallIntegrations({ try { await execa( installCommand.pm, - [installCommand.command, ...installCommand.flags, ...installCommand.dependencies], + [ + installCommand.command, + ...installCommand.flags, + ...inheritedFlags, + ...installCommand.dependencies, + ], { cwd } ); spinner.succeed(); return UpdateResult.updated; } catch (err) { - debug('add', 'Error installing dependencies', err); spinner.fail(); + debug('add', 'Error installing dependencies', err); + console.error('\n', (err as any).stdout, '\n'); return UpdateResult.failure; } } else { diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts index a1afb2201..7b8654ee7 100644 --- a/packages/astro/src/content/runtime.ts +++ b/packages/astro/src/content/runtime.ts @@ -69,7 +69,8 @@ export function createGetCollection({ // Cache `getCollection()` calls in production only // prevents stale cache in development if (import.meta.env.PROD && cacheEntriesByCollection.has(collection)) { - entries = cacheEntriesByCollection.get(collection)!; + // Always return a new instance so consumers can safely mutate it + entries = [...cacheEntriesByCollection.get(collection)!]; } else { entries = await Promise.all( lazyImports.map(async (lazyImport) => { diff --git a/packages/astro/src/core/build/plugins/plugin-middleware.ts b/packages/astro/src/core/build/plugins/plugin-middleware.ts index 8afca2fbc..5ed532d5e 100644 --- a/packages/astro/src/core/build/plugins/plugin-middleware.ts +++ b/packages/astro/src/core/build/plugins/plugin-middleware.ts @@ -56,7 +56,7 @@ export function vitePluginMiddleware( if (chunk.type === 'asset') { continue; } - if (chunk.fileName === 'middleware.mjs') { + if (chunk.fileName === 'middleware.mjs' && opts.settings.config.build.excludeMiddleware) { internals.middlewareEntryPoint = new URL(chunkName, opts.settings.config.build.server); } } diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts index 23db73382..4699a624c 100644 --- a/packages/astro/src/core/config/index.ts +++ b/packages/astro/src/core/config/index.ts @@ -1,6 +1,6 @@ export { resolveConfig, resolveConfigPath, resolveFlags, resolveRoot } from './config.js'; export { createNodeLogging } from './logging.js'; export { mergeConfig } from './merge.js'; -export type { AstroConfigSchema } from './schema'; +export type { AstroConfigType } from './schema'; export { createSettings } from './settings.js'; export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js'; diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 59bd76b53..9b7f42327 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -274,6 +274,8 @@ export const AstroConfigSchema = z.object({ legacy: z.object({}).optional().default({}), }); +export type AstroConfigType = z.infer; + export function createRelativeSchema(cmd: string, fileProtocolRoot: string) { // We need to extend the global schema to add transforms that are relative to root. // This is type checked against the global schema to make sure we still match. diff --git a/packages/astro/src/template/4xx.ts b/packages/astro/src/template/4xx.ts index c5d87929e..45f4ac10a 100644 --- a/packages/astro/src/template/4xx.ts +++ b/packages/astro/src/template/4xx.ts @@ -1,5 +1,4 @@ import { escape } from 'html-escaper'; -import { baseCSS } from './css.js'; interface ErrorTemplateOptions { /** a short description of the error */ @@ -28,14 +27,40 @@ export default function template({ ${tabTitle} diff --git a/packages/astro/src/template/css.ts b/packages/astro/src/template/css.ts deleted file mode 100644 index 87fbfd1b8..000000000 --- a/packages/astro/src/template/css.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * CSS is exported as a string so the error pages: - * 1. don’t need to resolve a deep internal CSS import - * 2. don’t need external dependencies to render (they may be shown because of a dep!) - */ - -// Base CSS: shared CSS among pages -export const baseCSS = ` -:root { - --gray-10: hsl(258, 7%, 10%); - --gray-20: hsl(258, 7%, 20%); - --gray-30: hsl(258, 7%, 30%); - --gray-40: hsl(258, 7%, 40%); - --gray-50: hsl(258, 7%, 50%); - --gray-60: hsl(258, 7%, 60%); - --gray-70: hsl(258, 7%, 70%); - --gray-80: hsl(258, 7%, 80%); - --gray-90: hsl(258, 7%, 90%); - --orange: #ff5d01; -} - -* { - box-sizing: border-box; -} - -body { - background-color: var(--gray-10); - color: var(--gray-80); - font-family: monospace; - line-height: 1.5; - margin: 0; -} - -a { - color: var(--orange); -} - -h1 { - font-weight: 800; - margin-top: 1rem; - margin-bottom: 0; -} - -pre { - color:; - font-size: 1.2em; - margin-top: 0; - max-width: 60em; -} -`; diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index cb74f9d9a..dfaf976bf 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -44,12 +44,11 @@ export default function createVitePluginAstroServer({ return () => { // Push this middleware to the front of the stack so that it can intercept responses. - if (settings.config.base !== '/') { - viteServer.middlewares.stack.unshift({ - route: '', - handle: baseMiddleware(settings, logging), - }); - } + // fix(#6067): always inject this to ensure zombie base handling is killed after restarts + viteServer.middlewares.stack.unshift({ + route: '', + handle: baseMiddleware(settings, logging), + }); // Note that this function has a name so other middleware can find it. viteServer.middlewares.use(async function astroDevHandler(request, response) { if (request.url === undefined || !request.method) { diff --git a/packages/astro/test/content-collections-render.test.js b/packages/astro/test/content-collections-render.test.js index e1107b10f..f6a9c3c72 100644 --- a/packages/astro/test/content-collections-render.test.js +++ b/packages/astro/test/content-collections-render.test.js @@ -168,6 +168,22 @@ describe('Content Collections - render()', () => { expect(h2).to.have.a.lengthOf(1); expect(h2.attr('data-components-export-applied')).to.equal('true'); }); + + it('getCollection should return new instances of the array to be mutated safely', async () => { + const app = await fixture.loadTestAdapterApp(); + + let request = new Request('http://example.com/sort-blog-collection'); + let response = await app.render(request); + let html = await response.text(); + let $ = cheerio.load(html); + expect($('li').first().text()).to.equal('With Layout Prop'); + + request = new Request('http://example.com/'); + response = await app.render(request); + html = await response.text(); + $ = cheerio.load(html); + expect($('li').first().text()).to.equal('Hello world'); + }); }); describe('Dev - SSG', () => { diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index 4d5f467bf..8c09de245 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -147,6 +147,19 @@ describe('astro:image', () => { }) ).to.be.true; }); + + it('supports inlined imports', async () => { + let res = await fixture.fetch('/inlineImport'); + let html = await res.text(); + $ = cheerio.load(html); + + let $img = $('img'); + expect($img).to.have.a.lengthOf(1); + + let src = $img.attr('src'); + res = await fixture.fetch(src); + expect(res.status).to.equal(200); + }); }); describe('vite-isms', () => { diff --git a/packages/astro/test/fixtures/content/src/pages/sort-blog-collection.astro b/packages/astro/test/fixtures/content/src/pages/sort-blog-collection.astro new file mode 100644 index 000000000..542606819 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/pages/sort-blog-collection.astro @@ -0,0 +1,22 @@ +--- +import { getCollection } from 'astro:content'; + +const blog = await getCollection('blog'); + +// Sort descending by title, make sure mutating `blog` doesn't mutate other pages that call `getCollection` too +blog.sort((a, b) => a.data.title < b.data.title ? 1 : -1) +--- + + + Index + + +

Blog Posts

+ +
    + {blog.map(post => ( +
  • { post.data.title }
  • + ))} +
+ + diff --git a/packages/astro/test/fixtures/core-image/src/pages/inlineImport.astro b/packages/astro/test/fixtures/core-image/src/pages/inlineImport.astro new file mode 100644 index 000000000..323587993 --- /dev/null +++ b/packages/astro/test/fixtures/core-image/src/pages/inlineImport.astro @@ -0,0 +1,7 @@ +--- +import { getImage } from "astro:assets"; + +const optimizedImage = await getImage({src: import('../assets/penguin1.jpg')}) +--- + + diff --git a/packages/astro/test/middleware.test.js b/packages/astro/test/middleware.test.js index 1ed857d5b..3796a341f 100644 --- a/packages/astro/test/middleware.test.js +++ b/packages/astro/test/middleware.test.js @@ -118,13 +118,7 @@ describe('Middleware API in PROD mode, SSR', () => { fixture = await loadFixture({ root: './fixtures/middleware-dev/', output: 'server', - adapter: testAdapter({ - setEntryPoints(entryPointsOrMiddleware) { - if (entryPointsOrMiddleware instanceof URL) { - middlewarePath = entryPointsOrMiddleware; - } - }, - }), + adapter: testAdapter({}), }); await fixture.build(); }); @@ -218,6 +212,21 @@ describe('Middleware API in PROD mode, SSR', () => { }); it('the integration should receive the path to the middleware', async () => { + fixture = await loadFixture({ + root: './fixtures/middleware-dev/', + output: 'server', + build: { + excludeMiddleware: true, + }, + adapter: testAdapter({ + setEntryPoints(entryPointsOrMiddleware) { + if (entryPointsOrMiddleware instanceof URL) { + middlewarePath = entryPointsOrMiddleware; + } + }, + }), + }); + await fixture.build(); expect(middlewarePath).to.not.be.undefined; try { const path = fileURLToPath(middlewarePath); diff --git a/packages/create-astro/CHANGELOG.md b/packages/create-astro/CHANGELOG.md index 76642c962..519133c71 100644 --- a/packages/create-astro/CHANGELOG.md +++ b/packages/create-astro/CHANGELOG.md @@ -6,6 +6,12 @@ - [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. +## 3.1.13 + +### Patch Changes + +- [#8028](https://github.com/withastro/astro/pull/8028) [`8292c4131`](https://github.com/withastro/astro/commit/8292c41311ec41d9d50921fbb2bdeed69e039443) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Improve yarn berry support + ## 3.1.12 ### Patch Changes diff --git a/packages/create-astro/src/actions/dependencies.ts b/packages/create-astro/src/actions/dependencies.ts index ed19fe485..339e3379f 100644 --- a/packages/create-astro/src/actions/dependencies.ts +++ b/packages/create-astro/src/actions/dependencies.ts @@ -1,6 +1,8 @@ import { color } from '@astrojs/cli-kit'; -import { execa } from 'execa'; +import fs from 'node:fs'; +import path from 'node:path'; import { error, info, spinner, title } from '../messages.js'; +import { shell } from '../shell.js'; import type { Context } from './context'; export async function dependencies( @@ -46,10 +48,12 @@ export async function dependencies( } async function install({ pkgManager, cwd }: { pkgManager: string; cwd: string }) { - const installExec = execa(pkgManager, ['install'], { cwd }); - return new Promise((resolve, reject) => { - setTimeout(() => reject(`Request timed out after one minute`), 60_000); - installExec.on('error', (e) => reject(e)); - installExec.on('close', () => resolve()); - }); + if (pkgManager === 'yarn') await ensureYarnLock({ cwd }); + return shell(pkgManager, ['install'], { cwd, timeout: 90_000, stdio: 'ignore' }); +} + +async function ensureYarnLock({ cwd }: { cwd: string }) { + const yarnLock = path.join(cwd, 'yarn.lock'); + if (fs.existsSync(yarnLock)) return; + return fs.promises.writeFile(yarnLock, '', { encoding: 'utf-8' }); } diff --git a/packages/create-astro/src/actions/git.ts b/packages/create-astro/src/actions/git.ts index 00c42dae5..c2a59b0b3 100644 --- a/packages/create-astro/src/actions/git.ts +++ b/packages/create-astro/src/actions/git.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import type { Context } from './context'; import { color } from '@astrojs/cli-kit'; -import { execa } from 'execa'; import { error, info, spinner, title } from '../messages.js'; +import { shell } from '../shell.js'; export async function git(ctx: Pick) { if (fs.existsSync(path.join(ctx.cwd, '.git'))) { @@ -45,9 +45,9 @@ export async function git(ctx: Pick { const packageManager = detectPackageManager()?.name || 'npm'; try { - const { stdout } = await execa(packageManager, ['config', 'get', 'registry']); + const { stdout } = await shell(packageManager, ['config', 'get', 'registry']); return stdout?.trim()?.replace(/\/$/, '') || 'https://registry.npmjs.org'; } catch (e) { return 'https://registry.npmjs.org'; diff --git a/packages/create-astro/src/shell.ts b/packages/create-astro/src/shell.ts new file mode 100644 index 000000000..d2d7ef033 --- /dev/null +++ b/packages/create-astro/src/shell.ts @@ -0,0 +1,49 @@ +// This is an extremely simplified version of [`execa`](https://github.com/sindresorhus/execa) +// intended to keep our dependency size down +import type { StdioOptions } from 'node:child_process'; +import type { Readable } from 'node:stream'; + +import { spawn } from 'node:child_process'; +import { text as textFromStream } from 'node:stream/consumers'; +import { setTimeout as sleep } from 'node:timers/promises'; + +export interface ExecaOptions { + cwd?: string | URL; + stdio?: StdioOptions; + timeout?: number; +} +export interface Output { + stdout: string; + stderr: string; + exitCode: number; +} +const text = (stream: NodeJS.ReadableStream | Readable | null) => + stream ? textFromStream(stream).then((t) => t.trimEnd()) : ''; + +export async function shell( + command: string, + flags: string[], + opts: ExecaOptions = {} +): Promise { + const controller = opts.timeout ? new AbortController() : undefined; + const child = spawn(command, flags, { + cwd: opts.cwd, + shell: true, + stdio: opts.stdio, + signal: controller?.signal, + }); + const stdout = await text(child.stdout); + const stderr = await text(child.stderr); + if (opts.timeout) { + sleep(opts.timeout).then(() => { + controller!.abort(); + throw { stdout, stderr, exitCode: 1 }; + }); + } + await new Promise((resolve) => child.on('exit', resolve)); + const { exitCode } = child; + if (exitCode !== 0) { + throw { stdout, stderr, exitCode }; + } + return { stdout, stderr, exitCode }; +} diff --git a/packages/create-astro/test/fixtures/not-empty/git.json b/packages/create-astro/test/fixtures/not-empty/git.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/packages/create-astro/test/fixtures/not-empty/git.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/create-astro/test/git.test.js b/packages/create-astro/test/git.test.js index c8fa86e86..d05ad5bdc 100644 --- a/packages/create-astro/test/git.test.js +++ b/packages/create-astro/test/git.test.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; - -import { execa } from 'execa'; -import fs from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { rmSync } from 'node:fs'; import { git } from '../dist/index.js'; import { setup } from './utils.js'; @@ -16,22 +15,6 @@ describe('git', () => { expect(fixture.hasMessage('Skipping Git initialization')).to.be.true; }); - it('already initialized', async () => { - const context = { - git: true, - cwd: './test/fixtures/not-empty', - dryRun: true, - prompt: () => ({ git: false }), - }; - await execa('git', ['init'], { cwd: './test/fixtures/not-empty' }); - await git(context); - - expect(fixture.hasMessage('Git has already been initialized')).to.be.true; - - // Cleanup - fs.rmSync('./test/fixtures/not-empty/.git', { recursive: true, force: true }); - }); - it('yes (--dry-run)', async () => { const context = { cwd: '', dryRun: true, prompt: () => ({ git: true }) }; await git(context); @@ -46,3 +29,29 @@ describe('git', () => { expect(fixture.hasMessage('Skipping Git initialization')).to.be.true; }); }); + +describe('git initialized', () => { + const fixture = setup(); + const dir = new URL(new URL('./fixtures/not-empty/.git', import.meta.url)); + + before(async () => { + await mkdir(dir, { recursive: true }); + await writeFile(new URL('./git.json', dir), '{}', { encoding: 'utf8' }); + }); + + it('already initialized', async () => { + const context = { + git: true, + cwd: './test/fixtures/not-empty', + dryRun: false, + prompt: () => ({ git: false }), + }; + await git(context); + + expect(fixture.hasMessage('Git has already been initialized')).to.be.true; + }); + + after(() => { + rmSync(dir, { recursive: true, force: true }); + }); +}); diff --git a/packages/integrations/cloudflare/CHANGELOG.md b/packages/integrations/cloudflare/CHANGELOG.md index 74c6f8c43..6111c047e 100644 --- a/packages/integrations/cloudflare/CHANGELOG.md +++ b/packages/integrations/cloudflare/CHANGELOG.md @@ -52,6 +52,35 @@ - astro@3.0.0-beta.0 - @astrojs/underscore-redirects@0.3.0-beta.0 +## 6.8.0 + +### Minor Changes + +- [#7541](https://github.com/withastro/astro/pull/7541) [`ffcfcddb7`](https://github.com/withastro/astro/commit/ffcfcddb7575030d62b4ef979d46a74425e6d3fe) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - The `getRuntime` utility has been deprecated and should be updated to the new [`Astro.locals`](https://docs.astro.build/en/guides/middleware/#locals) API. + + ```diff + - import { getRuntime } from '@astrojs/cloudflare/runtime'; + - getRuntime(Astro.request); + + + const runtime = Astro.locals.runtime; + ``` + +### Patch Changes + +- Updated dependencies [[`1b8d30209`](https://github.com/withastro/astro/commit/1b8d3020990130dabfaaf753db73a32c6e0c896a), [`405913cdf`](https://github.com/withastro/astro/commit/405913cdf20b26407aa351c090f0a0859a4e6f54), [`87d4b1843`](https://github.com/withastro/astro/commit/87d4b18437c7565c48cad4bea81831c2a244ebb8), [`c23377caa`](https://github.com/withastro/astro/commit/c23377caafbc75deb91c33b9678c1b6868ad40ea), [`86bee2812`](https://github.com/withastro/astro/commit/86bee2812185df6e14025e5962a335f51853587b)]: + - astro@2.10.6 + +## 6.7.0 + +### Minor Changes + +- [#7846](https://github.com/withastro/astro/pull/7846) [`ea30a9d4f`](https://github.com/withastro/astro/commit/ea30a9d4f2d7a12345869e971f3051cf803dbe74) Thanks [@schummar](https://github.com/schummar)! - More efficient \_routes.json + +### Patch Changes + +- Updated dependencies [[`5b1e39ef6`](https://github.com/withastro/astro/commit/5b1e39ef6ec6dcebea96584f95d9530bd9aa715d)]: + - astro@2.10.5 + ## 6.6.2 ### Patch Changes diff --git a/packages/integrations/cloudflare/README.md b/packages/integrations/cloudflare/README.md index 45f8e01ba..61db6effc 100644 --- a/packages/integrations/cloudflare/README.md +++ b/packages/integrations/cloudflare/README.md @@ -73,12 +73,10 @@ It's then possible to update the preview script in your `package.json` to `"prev ## Access to the Cloudflare runtime -You can access all the Cloudflare bindings and environment variables from Astro components and API routes through the adapter API. +You can access all the Cloudflare bindings and environment variables from Astro components and API routes through `Astro.locals`. ```js -import { getRuntime } from '@astrojs/cloudflare/runtime'; - -getRuntime(Astro.request); +const env = Astro.locals.runtime.env; ``` Depending on your adapter mode (advanced = worker, directory = pages), the runtime object will look a little different due to differences in the Cloudflare API. diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index c378a195f..6b64a0a1f 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -230,7 +230,7 @@ export default function createIntegration(args?: Options): AstroIntegration { } } - // // // throw the server folder in the bin + // throw the server folder in the bin const serverUrl = new URL(_buildConfig.server); await fs.promises.rm(serverUrl, { recursive: true, force: true }); diff --git a/packages/integrations/cloudflare/src/runtime.ts b/packages/integrations/cloudflare/src/runtime.ts index cd3dfff47..03c15d4a3 100644 --- a/packages/integrations/cloudflare/src/runtime.ts +++ b/packages/integrations/cloudflare/src/runtime.ts @@ -1,3 +1,4 @@ +// TODO: remove `getRuntime()` in Astro 3.0 import type { Cache, CacheStorage, IncomingRequestCfProperties } from '@cloudflare/workers-types'; export type WorkerRuntime = { @@ -21,6 +22,16 @@ export type PagesRuntime = { cf?: IncomingRequestCfProperties; }; +/** + * @deprecated since version 6.8.0 + * The `getRuntime` utility has been deprecated and should be updated to the new [`Astro.locals`](https://docs.astro.build/en/guides/middleware/#locals) API. + * ```diff + * - import { getRuntime } from '@astrojs/cloudflare/runtime'; + * - getRuntime(Astro.request); + * + * + const runtime = Astro.locals.runtime; + * ``` + */ export function getRuntime( request: Request ): WorkerRuntime | PagesRuntime { diff --git a/packages/integrations/cloudflare/src/server.advanced.ts b/packages/integrations/cloudflare/src/server.advanced.ts index 9758b8b19..175756d6a 100644 --- a/packages/integrations/cloudflare/src/server.advanced.ts +++ b/packages/integrations/cloudflare/src/server.advanced.ts @@ -12,10 +12,21 @@ type Env = { name: string; }; +interface WorkerRuntime { + runtime: { + waitUntil: (promise: Promise) => void; + env: Env; + cf: CFRequest['cf']; + caches: typeof caches; + }; +} + export function createExports(manifest: SSRManifest) { const app = new App(manifest); const fetch = async (request: Request & CFRequest, env: Env, context: ExecutionContext) => { + // TODO: remove this any cast in the future + // REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv' process.env = env as any; const { pathname } = new URL(request.url); @@ -32,6 +43,9 @@ export function createExports(manifest: SSRManifest) { Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip') ); + + // `getRuntime()` is deprecated, currently available additionally to new Astro.locals.runtime + // TODO: remove `getRuntime()` in Astro 3.0 Reflect.set(request, Symbol.for('runtime'), { env, name: 'cloudflare', @@ -42,7 +56,19 @@ export function createExports(manifest: SSRManifest) { context.waitUntil(promise); }, }); - let response = await app.render(request, routeData); + + const locals: WorkerRuntime = { + runtime: { + waitUntil: (promise: Promise) => { + context.waitUntil(promise); + }, + env: env, + cf: request.cf, + caches: caches, + }, + }; + + let response = await app.render(request, routeData, locals); if (app.setCookieHeaders) { for (const setCookieHeader of app.setCookieHeaders(response)) { diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts index 3e9531a56..d4e4094de 100644 --- a/packages/integrations/cloudflare/src/server.directory.ts +++ b/packages/integrations/cloudflare/src/server.directory.ts @@ -7,28 +7,30 @@ if (!isNode) { process.env = getProcessEnvProxy(); } +interface FunctionRuntime { + runtime: { + waitUntil: (promise: Promise) => void; + env: EventContext['env']; + cf: CFRequest['cf']; + caches: typeof caches; + }; +} + export function createExports(manifest: SSRManifest) { const app = new App(manifest); - const onRequest = async ({ - request, - next, - ...runtimeEnv - }: { - request: Request & CFRequest; - next: (request: Request) => void; - waitUntil: EventContext['waitUntil']; - } & Record) => { - process.env = runtimeEnv.env as any; + const onRequest = async (context: EventContext) => { + const request = context.request as CFRequest & Request; + const { next, env } = context; + + // TODO: remove this any cast in the future + // REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv' + process.env = env as any; const { pathname } = new URL(request.url); // static assets fallback, in case default _routes.json is not used if (manifest.assets.has(pathname)) { - // we need this so the page does not error - // https://developers.cloudflare.com/pages/platform/functions/advanced-mode/#set-up-a-function - return (runtimeEnv.env as EventContext['env']).ASSETS.fetch( - request - ); + return env.ASSETS.fetch(request); } let routeData = app.match(request, { matchNotFound: true }); @@ -38,17 +40,32 @@ export function createExports(manifest: SSRManifest) { Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip') ); + + // `getRuntime()` is deprecated, currently available additionally to new Astro.locals.runtime + // TODO: remove `getRuntime()` in Astro 3.0 Reflect.set(request, Symbol.for('runtime'), { - ...runtimeEnv, + ...context, waitUntil: (promise: Promise) => { - runtimeEnv.waitUntil(promise); + context.waitUntil(promise); }, name: 'cloudflare', next, caches, cf: request.cf, }); - let response = await app.render(request, routeData); + + const locals: FunctionRuntime = { + runtime: { + waitUntil: (promise: Promise) => { + context.waitUntil(promise); + }, + env: context.env, + cf: request.cf, + caches: caches, + }, + }; + + let response = await app.render(request, routeData, locals); if (app.setCookieHeaders) { for (const setCookieHeader of app.setCookieHeaders(response)) { diff --git a/packages/integrations/cloudflare/test/cf.test.js b/packages/integrations/cloudflare/test/cf.test.js index 559df5c76..f8ab9c02f 100644 --- a/packages/integrations/cloudflare/test/cf.test.js +++ b/packages/integrations/cloudflare/test/cf.test.js @@ -17,7 +17,7 @@ describe('Cf metadata and caches', () => { }); await fixture.build(); - cli = runCLI('./fixtures/cf/', { silent: true, port: 8788 }); + cli = runCLI('./fixtures/cf/', { silent: false, port: 8788 }); await cli.ready; }); diff --git a/packages/integrations/cloudflare/test/fixtures/runtime/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/runtime/astro.config.mjs new file mode 100644 index 000000000..f92829843 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/runtime/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import cloudflare from '@astrojs/cloudflare'; + + +export default defineConfig({ + adapter: cloudflare(), + output: 'server', +}); diff --git a/packages/integrations/cloudflare/test/fixtures/runtime/package.json b/packages/integrations/cloudflare/test/fixtures/runtime/package.json new file mode 100644 index 000000000..71ac16647 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/runtime/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-cloudflare-runtime", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/cloudflare/test/fixtures/runtime/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/runtime/src/pages/index.astro new file mode 100644 index 000000000..320e8e162 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/runtime/src/pages/index.astro @@ -0,0 +1,15 @@ +--- +const runtime = Astro.locals.runtime; +const env = runtime.env; +--- + + + Testing + + +

Testing

+
{JSON.stringify(runtime.cf)}
+
{JSON.stringify(env)}
+
{!!runtime.caches}
+ + diff --git a/packages/integrations/cloudflare/test/runtime.test.js b/packages/integrations/cloudflare/test/runtime.test.js new file mode 100644 index 000000000..243c1dd67 --- /dev/null +++ b/packages/integrations/cloudflare/test/runtime.test.js @@ -0,0 +1,38 @@ +import { loadFixture, runCLI } from './test-utils.js'; +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; +import cloudflare from '../dist/index.js'; + +describe('Runtime Locals', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + /** @type {import('./test-utils.js').WranglerCLI} */ + let cli; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/runtime/', + output: 'server', + adapter: cloudflare(), + }); + await fixture.build(); + + cli = runCLI('./fixtures/runtime/', { silent: true, port: 8793 }); + await cli.ready; + }); + + after(async () => { + await cli.stop(); + }); + + it('has CF and Caches', async () => { + let res = await fetch(`http://localhost:8793/`); + expect(res.status).to.equal(200); + let html = await res.text(); + let $ = cheerio.load(html); + expect($('#cf').text()).to.contain('city'); + expect($('#env').text()).to.contain('SECRET_STUFF'); + expect($('#env').text()).to.contain('secret'); + expect($('#hasCache').text()).to.equal('true'); + }); +}); diff --git a/packages/integrations/netlify/CHANGELOG.md b/packages/integrations/netlify/CHANGELOG.md index a3ec39ce2..0d1e64302 100644 --- a/packages/integrations/netlify/CHANGELOG.md +++ b/packages/integrations/netlify/CHANGELOG.md @@ -106,6 +106,32 @@ - astro@3.0.0-beta.0 - @astrojs/underscore-redirects@0.3.0-beta.0 +## 2.6.0 + +### Minor Changes + +- [#7975](https://github.com/withastro/astro/pull/7975) [`f974c95a2`](https://github.com/withastro/astro/commit/f974c95a27ccbf91adbc66f6f1433f4cf11be33e) Thanks [@lilnasy](https://github.com/lilnasy)! - If you are using Netlify's On-demand Builders, you can now specify how long your pages should remain cached. By default, all pages will be rendered on first visit and reused on every subsequent visit until a redeploy. To set a custom revalidation time, call the `runtime.setBuildersTtl()` local in either your frontmatter or middleware. + + ```astro + --- + import Layout from '../components/Layout.astro'; + + if (import.meta.env.PROD) { + // revalidates every 45 seconds + Astro.locals.runtime.setBuildersTtl(45); + } + --- + + + {new Date(Date.now())} + + ``` + +### Patch Changes + +- Updated dependencies [[`1b8d30209`](https://github.com/withastro/astro/commit/1b8d3020990130dabfaaf753db73a32c6e0c896a), [`405913cdf`](https://github.com/withastro/astro/commit/405913cdf20b26407aa351c090f0a0859a4e6f54), [`87d4b1843`](https://github.com/withastro/astro/commit/87d4b18437c7565c48cad4bea81831c2a244ebb8), [`c23377caa`](https://github.com/withastro/astro/commit/c23377caafbc75deb91c33b9678c1b6868ad40ea), [`86bee2812`](https://github.com/withastro/astro/commit/86bee2812185df6e14025e5962a335f51853587b)]: + - astro@2.10.6 + ## 2.5.2 ### Patch Changes diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index 91a0c41d8..8b652180b 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -144,6 +144,30 @@ Once you run `astro build` there will be a `dist/_redirects` file. Netlify will > **Note** > You can still include a `public/_redirects` file for manual redirects. Any redirects you specify in the redirects config are appended to the end of your own. +### On-demand Builders + +[Netlify On-demand Builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are serverless functions used to generate web content as needed that’s automatically cached on Netlify’s Edge CDN. You can enable these functions using the [`builders` configuration](#builders). + +By default, all pages will be rendered on first visit and the rendered result will be reused for every subsequent visit until you redeploy. To set a revalidation time, call the [`runtime.setBuildersTtl(ttl)` local](https://docs.astro.build/en/guides/middleware/#locals) with the duration (in seconds). + +The following example sets a revalidation time of 45, causing Netlify to store the rendered HTML for 45 seconds. + +```astro +--- +import Layout from '../components/Layout.astro'; + +if (import.meta.env.PROD) { + Astro.locals.runtime.setBuildersTtl(45); +} +--- + + + {new Date(Date.now())} + +``` + +It is important to note that On-demand Builders ignore query params when checking for cached pages. For example, if `example.com/?x=y` is cached, it will be served for `example.com/?a=b` (different query params) and `example.com/` (no query params) as well. + ## Usage [Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) @@ -188,7 +212,7 @@ directory = "dist/functions" ### builders -[Netlify On-demand Builders](https://docs.netlify.com/configure-builds/on-demand-builders/) are serverless functions used to build and cache page content on Netlify’s Edge CDN. You can enable these functions with the `builders` option: +You can enable On-demand Builders using the `builders` option: ```js // astro.config.mjs diff --git a/packages/integrations/netlify/builders-types.d.ts b/packages/integrations/netlify/builders-types.d.ts new file mode 100644 index 000000000..7c778be4f --- /dev/null +++ b/packages/integrations/netlify/builders-types.d.ts @@ -0,0 +1,9 @@ +interface NetlifyLocals { + runtime: { + /** + * On-demand Builders support an optional time to live (TTL) pattern that allows you to set a fixed duration of time after which a cached builder response is invalidated. This allows you to force a refresh of a builder-generated response without a new deploy. + * @param ttl time to live, in seconds + */ + setBuildersTtl(ttl: number): void; + }; +} diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index 3da0718b0..8c051d9f6 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -68,18 +68,32 @@ export const createExports = (manifest: SSRManifest, args: Args) => { init.body = typeof requestBody === 'string' ? Buffer.from(requestBody, encoding) : requestBody; } + const request = new Request(rawUrl, init); const routeData = app.match(request); const ip = headers['x-nf-client-connection-ip']; Reflect.set(request, clientAddressSymbol, ip); - let locals = {}; + + let locals: Record = {}; + if (request.headers.has(ASTRO_LOCALS_HEADER)) { let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER); if (localsAsString) { locals = JSON.parse(localsAsString); } } + + let responseTtl = undefined; + + locals.runtime = builders + ? { + setBuildersTtl(ttl: number) { + responseTtl = ttl; + }, + } + : {}; + const response: Response = await app.render(request, routeData, locals); const responseHeaders = Object.fromEntries(response.headers.entries()); @@ -99,6 +113,7 @@ export const createExports = (manifest: SSRManifest, args: Args) => { headers: responseHeaders, body: responseBody, isBase64Encoded: responseIsBase64Encoded, + ttl: responseTtl, }; const cookies = response.headers.get('set-cookie'); diff --git a/packages/integrations/netlify/test/functions/builders.test.js b/packages/integrations/netlify/test/functions/builders.test.js new file mode 100644 index 000000000..d47af92c0 --- /dev/null +++ b/packages/integrations/netlify/test/functions/builders.test.js @@ -0,0 +1,37 @@ +import { expect } from 'chai'; +import { loadFixture, testIntegration } from './test-utils.js'; +import netlifyAdapter from '../../dist/index.js'; + +describe('Builders', () => { + /** @type {import('../../../astro/test/test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/builders/', import.meta.url).toString(), + output: 'server', + adapter: netlifyAdapter({ + dist: new URL('./fixtures/builders/dist/', import.meta.url), + builders: true, + }), + site: `http://example.com`, + integrations: [testIntegration()], + }); + await fixture.build(); + }); + + it('A route can set builders ttl', async () => { + const entryURL = new URL( + './fixtures/builders/.netlify/functions-internal/entry.mjs', + import.meta.url + ); + const { handler } = await import(entryURL); + const resp = await handler({ + httpMethod: 'GET', + headers: {}, + rawUrl: 'http://example.com/', + isBase64Encoded: false, + }); + expect(resp.ttl).to.equal(45); + }); +}); diff --git a/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro new file mode 100644 index 000000000..ab8853785 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/builders/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +Astro.locals.runtime.setBuildersTtl(45) +--- + + + Astro on Netlify + + +

{new Date(Date.now())}

+ + diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index 884eb21f8..01e61307f 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -39,6 +39,15 @@ - Updated dependencies [[`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81), [`76ddef19c`](https://github.com/withastro/astro/commit/76ddef19ccab6e5f7d3a5740cd41acf10e334b38), [`9b4f70a62`](https://github.com/withastro/astro/commit/9b4f70a629f55e461759ba46f68af7097a2e9215), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`2f951cd40`](https://github.com/withastro/astro/commit/2f951cd403dfcc2c3ca6aae618ae3e1409516e32), [`c022a4217`](https://github.com/withastro/astro/commit/c022a4217a805d223c1494e9eda4e48bbf810388), [`67becaa58`](https://github.com/withastro/astro/commit/67becaa580b8f787df58de66b7008b7098f1209c), [`bc37331d8`](https://github.com/withastro/astro/commit/bc37331d8154e3e95a8df9131e4e014e78a7a9e7), [`dfc2d93e3`](https://github.com/withastro/astro/commit/dfc2d93e3c645995379358fabbdfa9aab99f43d8), [`3dc1ca2fa`](https://github.com/withastro/astro/commit/3dc1ca2fac8d9965cc5085a5d09e72ed87b4281a), [`1be84dfee`](https://github.com/withastro/astro/commit/1be84dfee3ce8e6f5cc624f99aec4e980f6fde37), [`35f01df79`](https://github.com/withastro/astro/commit/35f01df797d23315f2bee2fc3fd795adb0559c58), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`78de801f2`](https://github.com/withastro/astro/commit/78de801f21fd4ca1653950027d953bf08614566b), [`59d6e569f`](https://github.com/withastro/astro/commit/59d6e569f63e175c97e82e94aa7974febfb76f7c), [`7723c4cc9`](https://github.com/withastro/astro/commit/7723c4cc93298c2e6530e55da7afda048f22cf81), [`fb5cd6b56`](https://github.com/withastro/astro/commit/fb5cd6b56dc27a71366ed5e1ab8bfe9b8f96bac5), [`631b9c410`](https://github.com/withastro/astro/commit/631b9c410d5d66fa384674027ba95d69ebb5063f)]: - astro@3.0.0-beta.0 +## 5.3.3 + +### Patch Changes + +- [#6928](https://github.com/withastro/astro/pull/6928) [`b16cb787f`](https://github.com/withastro/astro/commit/b16cb787fd16ebaaf860d8bb183789caf01c0fb7) Thanks [@JerryWu1234](https://github.com/JerryWu1234)! - Support the `--host` flag when running the standalone server (also works for `astro preview --host`) + +- Updated dependencies [[`1b8d30209`](https://github.com/withastro/astro/commit/1b8d3020990130dabfaaf753db73a32c6e0c896a), [`405913cdf`](https://github.com/withastro/astro/commit/405913cdf20b26407aa351c090f0a0859a4e6f54), [`87d4b1843`](https://github.com/withastro/astro/commit/87d4b18437c7565c48cad4bea81831c2a244ebb8), [`c23377caa`](https://github.com/withastro/astro/commit/c23377caafbc75deb91c33b9678c1b6868ad40ea), [`86bee2812`](https://github.com/withastro/astro/commit/86bee2812185df6e14025e5962a335f51853587b)]: + - astro@2.10.6 + ## 5.3.2 ### Patch Changes diff --git a/packages/integrations/node/src/get-network-address.ts b/packages/integrations/node/src/get-network-address.ts new file mode 100644 index 000000000..3834c7617 --- /dev/null +++ b/packages/integrations/node/src/get-network-address.ts @@ -0,0 +1,48 @@ +import os from 'os'; +interface NetworkAddressOpt { + local: string[]; + network: string[]; +} + +const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']); +type Protocol = 'http' | 'https'; + +// this code from vite https://github.com/vitejs/vite/blob/d09bbd093a4b893e78f0bbff5b17c7cf7821f403/packages/vite/src/node/utils.ts#L892-L914 +export function getNetworkAddress( + protocol: Protocol = 'http', + hostname: string | undefined, + port: number, + base?: string +) { + const NetworkAddress: NetworkAddressOpt = { + local: [], + network: [], + }; + Object.values(os.networkInterfaces()) + .flatMap((nInterface) => nInterface ?? []) + .filter( + (detail) => + detail && + detail.address && + (detail.family === 'IPv4' || + // @ts-expect-error Node 18.0 - 18.3 returns number + detail.family === 4) + ) + .forEach((detail) => { + let host = detail.address.replace( + '127.0.0.1', + hostname === undefined || wildcardHosts.has(hostname) ? 'localhost' : hostname + ); + // ipv6 host + if (host.includes(':')) { + host = `[${host}]`; + } + const url = `${protocol}://${host}:${port}${base ? base : ''}`; + if (detail.address.includes('127.0.0.1')) { + NetworkAddress.local.push(url); + } else { + NetworkAddress.network.push(url); + } + }); + return NetworkAddress; +} diff --git a/packages/integrations/node/src/preview.ts b/packages/integrations/node/src/preview.ts index 92f9b86ba..4a4db4632 100644 --- a/packages/integrations/node/src/preview.ts +++ b/packages/integrations/node/src/preview.ts @@ -1,6 +1,7 @@ import type { CreatePreviewServer } from 'astro'; import type http from 'node:http'; import { fileURLToPath } from 'node:url'; +import { getNetworkAddress } from './get-network-address.js'; import { createServer } from './http-server.js'; import type { createExports } from './server'; @@ -67,9 +68,17 @@ const preview: CreatePreviewServer = async function ({ }, handler ); + const address = getNetworkAddress('http', host, port); - // eslint-disable-next-line no-console - console.log(`Preview server listening on http://${host}:${port}`); + if (host === undefined) { + // eslint-disable-next-line no-console + console.log( + `Preview server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n` + ); + } else { + // eslint-disable-next-line no-console + console.log(`Preview server listening on ${address.local[0]}`); + } return server; }; diff --git a/packages/integrations/node/src/standalone.ts b/packages/integrations/node/src/standalone.ts index 68b2cebcd..94dc26758 100644 --- a/packages/integrations/node/src/standalone.ts +++ b/packages/integrations/node/src/standalone.ts @@ -2,6 +2,7 @@ import type { NodeApp } from 'astro/app/node'; import https from 'https'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { getNetworkAddress } from './get-network-address.js'; import { createServer } from './http-server.js'; import middleware from './nodeMiddleware.js'; import type { Options } from './types'; @@ -55,9 +56,17 @@ export default function startServer(app: NodeApp, options: Options) { ); const protocol = server.server instanceof https.Server ? 'https' : 'http'; + const address = getNetworkAddress(protocol, host, port); - // eslint-disable-next-line no-console - console.log(`Server listening on ${protocol}://${host}:${port}`); + if (host === undefined) { + // eslint-disable-next-line no-console + console.log( + `Preview server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n` + ); + } else { + // eslint-disable-next-line no-console + console.log(`Preview server listening on ${address.local[0]}`); + } return { server, diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js index ef5929af1..d8948e7bb 100644 --- a/packages/integrations/react/client.js +++ b/packages/integrations/react/client.js @@ -31,7 +31,7 @@ export default (element) => } if (client === 'only') { return startTransition(() => { - createRoot(element, renderOptions).render(componentEl); + createRoot(element).render(componentEl); }); } return startTransition(() => { diff --git a/packages/integrations/sitemap/README.md b/packages/integrations/sitemap/README.md index ecf59a12d..b2fb5e9dd 100644 --- a/packages/integrations/sitemap/README.md +++ b/packages/integrations/sitemap/README.md @@ -339,7 +339,7 @@ The resulting sitemap looks like this: ## Examples - The official Astro website uses Astro Sitemap to generate [its sitemap](https://astro.build/sitemap-index.xml). -- [Browse projects with Astro Sitemap on GitHub](https://github.com/search?q=%22@astrojs/sitemap%22+filename:package.json&type=Code) for more examples! +- [Browse projects with Astro Sitemap on GitHub](https://github.com/search?q=%22%40astrojs%2Fsitemap%22+path%3Apackage.json&type=Code) for more examples! ## Troubleshooting diff --git a/packages/integrations/vercel/CHANGELOG.md b/packages/integrations/vercel/CHANGELOG.md index 42bbd00be..455efb793 100644 --- a/packages/integrations/vercel/CHANGELOG.md +++ b/packages/integrations/vercel/CHANGELOG.md @@ -113,6 +113,15 @@ - astro@3.0.0-beta.0 - @astrojs/internal-helpers@0.2.0-beta.0 +## 3.8.1 + +### Patch Changes + +- [#8039](https://github.com/withastro/astro/pull/8039) [`6b57628d1`](https://github.com/withastro/astro/commit/6b57628d128779290db3344bbb6de7282196fb97) Thanks [@matthewp](https://github.com/matthewp)! - Prevent Vercel NFT from scanning /dev + +- Updated dependencies [[`1b8d30209`](https://github.com/withastro/astro/commit/1b8d3020990130dabfaaf753db73a32c6e0c896a), [`405913cdf`](https://github.com/withastro/astro/commit/405913cdf20b26407aa351c090f0a0859a4e6f54), [`87d4b1843`](https://github.com/withastro/astro/commit/87d4b18437c7565c48cad4bea81831c2a244ebb8), [`c23377caa`](https://github.com/withastro/astro/commit/c23377caafbc75deb91c33b9678c1b6868ad40ea), [`86bee2812`](https://github.com/withastro/astro/commit/86bee2812185df6e14025e5962a335f51853587b)]: + - astro@2.10.6 + ## 3.8.0 ### Minor Changes diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts index 752f87251..10c298a1d 100644 --- a/packages/integrations/vercel/src/lib/nft.ts +++ b/packages/integrations/vercel/src/lib/nft.ts @@ -28,6 +28,9 @@ export async function copyDependenciesToFunction({ const { nodeFileTrace } = await import('@vercel/nft'); const result = await nodeFileTrace([entryPath], { base: fileURLToPath(base), + // If you have a route of /dev this appears in source and NFT will try to + // scan your local /dev :8 + ignore: ['/dev/**'], }); for (const error of result.warnings) {