diff --git a/.changeset/breezy-hairs-yell.md b/.changeset/breezy-hairs-yell.md new file mode 100644 index 000000000000..173d9934dbea --- /dev/null +++ b/.changeset/breezy-hairs-yell.md @@ -0,0 +1,5 @@ +--- +'@astrojs/react': patch +--- + +Prevents unsupported `forwardRef` components created by Preact from being rendered by React diff --git a/.changeset/eight-sheep-nail.md b/.changeset/eight-sheep-nail.md new file mode 100644 index 000000000000..a4730e5d214c --- /dev/null +++ b/.changeset/eight-sheep-nail.md @@ -0,0 +1,5 @@ +--- +'@astrojs/node': patch +--- + +Fix typo in @astrojs/node README diff --git a/.changeset/friendly-tables-worry.md b/.changeset/friendly-tables-worry.md new file mode 100644 index 000000000000..fbcfe07b6818 --- /dev/null +++ b/.changeset/friendly-tables-worry.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +update import created for `astro create netlify` diff --git a/.changeset/modern-humans-think.md b/.changeset/modern-humans-think.md new file mode 100644 index 000000000000..71ad5e0a5b11 --- /dev/null +++ b/.changeset/modern-humans-think.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes importing dev toolbar apps from integrations on Windows diff --git a/.changeset/real-bags-hope.md b/.changeset/real-bags-hope.md new file mode 100644 index 000000000000..86fb6678329d --- /dev/null +++ b/.changeset/real-bags-hope.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix Astro failing to build on certain exotic platform that reports their CPU count incorrectly diff --git a/.changeset/selfish-rings-occur.md b/.changeset/selfish-rings-occur.md new file mode 100644 index 000000000000..e5722bb95f23 --- /dev/null +++ b/.changeset/selfish-rings-occur.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Correctly handle the error in case the middleware throws a runtime error diff --git a/.changeset/silly-cycles-clap.md b/.changeset/silly-cycles-clap.md new file mode 100644 index 000000000000..dc456728eddb --- /dev/null +++ b/.changeset/silly-cycles-clap.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes an issue where redirects did not replace slugs when the target of the redirect rule was not a verbatim route in the project. diff --git a/.changeset/warm-bats-eat.md b/.changeset/warm-bats-eat.md new file mode 100644 index 000000000000..27a059a3d79d --- /dev/null +++ b/.changeset/warm-bats-eat.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes incorrect hoisted script paths when custom rollup output file names are configured diff --git a/examples/ssr/src/pages/api/cart.ts b/examples/ssr/src/pages/api/cart.ts index 6aa546903f34..2449e0c945b5 100644 --- a/examples/ssr/src/pages/api/cart.ts +++ b/examples/ssr/src/pages/api/cart.ts @@ -2,7 +2,7 @@ import { APIContext } from 'astro'; import { userCartItems } from '../../models/session'; export function GET({ cookies }: APIContext) { - let userId = cookies.get('user-id').value; + let userId = cookies.get('user-id')?.value; if (!userId || !userCartItems.has(userId)) { return Response.json({ items: [] }); diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index fa8c33920ac0..9df1b2bf12b3 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -677,7 +677,7 @@ export interface AstroUserConfig { * [See our Server-side Rendering guide](https://docs.astro.build/en/guides/server-side-rendering/) for more on SSR, and [our deployment guides](https://docs.astro.build/en/guides/deploy/) for a complete list of hosts. * * ```js - * import netlify from '@astrojs/netlify/functions'; + * import netlify from '@astrojs/netlify'; * { * // Example: Build for Netlify serverless deployment * adapter: netlify(), diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index 76323ca0b789..06c20eab3b60 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -71,7 +71,7 @@ public-hoist-pattern[]=*lit* `; const OFFICIAL_ADAPTER_TO_IMPORT_MAP: Record = { - netlify: '@astrojs/netlify/functions', + netlify: '@astrojs/netlify', vercel: '@astrojs/vercel/serverless', cloudflare: '@astrojs/cloudflare', node: '@astrojs/node', diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 486fac9fbcf0..9dc80f8223f0 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -220,7 +220,7 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn .reduce((a, b) => a + b, 0); const cpuCount = os.cpus().length; const assetsCreationEnvironment = await prepareAssetsGenerationEnv(pipeline, totalCount); - const queue = new PQueue({ concurrency: cpuCount }); + const queue = new PQueue({ concurrency: Math.max(cpuCount, 1) }); const assetsTimer = performance.now(); for (const [originalPath, transforms] of staticImageList) { diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 1a313b6bbdb4..ebdb4734e304 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -194,8 +194,9 @@ function buildManifest( if (route.prerender || !pageData) continue; const scripts: SerializedRouteInfo['scripts'] = []; if (pageData.hoistedScript) { + const shouldPrefixAssetPath = pageData.hoistedScript.type === 'external'; const hoistedValue = pageData.hoistedScript.value; - const value = hoistedValue.endsWith('.js') ? prefixAssetPath(hoistedValue) : hoistedValue; + const value = shouldPrefixAssetPath ? prefixAssetPath(hoistedValue) : hoistedValue; scripts.unshift( Object.assign({}, pageData.hoistedScript, { value, diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 047bb533721c..1e00e47ace6d 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -780,6 +780,26 @@ export const LocalsNotAnObject = { hint: 'If you tried to remove some information from the `locals` object, try to use `delete` or set the property to `undefined`.', } satisfies ErrorData; +/** + * @docs + * @description + * Thrown in development mode when middleware throws an error while attempting to loading it. + * + * For example: + * ```ts + * import {defineMiddleware} from "astro:middleware"; + * throw new Error("Error thrown while loading the middleware.") + * export const onRequest = defineMiddleware(() => { + * return "string" + * }); + * ``` + */ +export const MiddlewareCantBeLoaded = { + name: 'MiddlewareCantBeLoaded', + title: "Can't load the middleware.", + message: 'The middleware threw an error while Astro was trying to loading it.', +} satisfies ErrorData; + /** * @docs * @see diff --git a/packages/astro/src/core/middleware/loadMiddleware.ts b/packages/astro/src/core/middleware/loadMiddleware.ts index 4f524ee52756..135f7fbc479e 100644 --- a/packages/astro/src/core/middleware/loadMiddleware.ts +++ b/packages/astro/src/core/middleware/loadMiddleware.ts @@ -1,5 +1,7 @@ import type { ModuleLoader } from '../module-loader/index.js'; import { MIDDLEWARE_MODULE_ID } from './vite-plugin.js'; +import { MiddlewareCantBeLoaded } from '../errors/errors-data.js'; +import { AstroError } from '../errors/index.js'; /** * It accepts a module loader and the astro settings, and it attempts to load the middlewares defined in the configuration. @@ -8,9 +10,9 @@ import { MIDDLEWARE_MODULE_ID } from './vite-plugin.js'; */ export async function loadMiddleware(moduleLoader: ModuleLoader) { try { - const module = await moduleLoader.import(MIDDLEWARE_MODULE_ID); - return module; - } catch { - return void 0; + return await moduleLoader.import(MIDDLEWARE_MODULE_ID); + } catch (error: any) { + const astroError = new AstroError(MiddlewareCantBeLoaded, undefined, { cause: error }); + throw astroError; } } diff --git a/packages/astro/src/core/redirects/helpers.ts b/packages/astro/src/core/redirects/helpers.ts index 697cb0fd896f..1faab7c4ecfe 100644 --- a/packages/astro/src/core/redirects/helpers.ts +++ b/packages/astro/src/core/redirects/helpers.ts @@ -20,7 +20,14 @@ export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): s if (typeof routeData !== 'undefined') { return routeData?.generate(data) || routeData?.pathname || '/'; } else if (typeof route === 'string') { - return route; + // TODO: this logic is duplicated between here and manifest/create.ts + let target = route; + for (const param of Object.keys(data)) { + const paramValue = data[param]!; + target = target.replace(`[${param}]`, paramValue); + target = target.replace(`[...${param}]`, paramValue); + } + return target; } else if (typeof route === 'undefined') { return '/'; } diff --git a/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts b/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts index 201e6aac63f5..31cdb8f71ac0 100644 --- a/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts +++ b/packages/astro/src/vite-plugin-dev-overlay/vite-plugin-dev-overlay.ts @@ -17,7 +17,7 @@ export default function astroDevOverlay({ settings }: AstroPluginOptions): vite. return ` export const loadDevOverlayPlugins = async () => { return [${settings.devToolbarApps - .map((plugin) => `(await import('${plugin}')).default`) + .map((plugin) => `(await import(${JSON.stringify(plugin)})).default`) .join(',')}]; }; `; diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.js index 63a09312478f..fef889be591a 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.js @@ -15,6 +15,13 @@ describe('Astro.redirect', () => { redirects: { '/api/redirect': '/test', '/external/redirect': 'https://example.com/', + // for example, the real file handling the target path may be called + // src/pages/not-verbatim/target1/[something-other-than-dynamic].astro + '/source/[dynamic]': '/not-verbatim/target1/[dynamic]', + // may be called src/pages/not-verbatim/target2/[abc]/[xyz].astro + '/source/[dynamic]/[route]': '/not-verbatim/target2/[dynamic]/[route]', + // may be called src/pages/not-verbatim/target3/[...rest].astro + '/source/[...spread]': '/not-verbatim/target3/[...spread]', }, }); await fixture.build(); @@ -68,6 +75,27 @@ describe('Astro.redirect', () => { const response = await app.render(request); expect(response.status).to.equal(308); }); + + it('Forwards params to the target path - single param', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/source/x'); + const response = await app.render(request); + expect(response.headers.get('Location')).to.equal('/not-verbatim/target1/x'); + }); + + it('Forwards params to the target path - multiple params', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/source/x/y'); + const response = await app.render(request); + expect(response.headers.get('Location')).to.equal('/not-verbatim/target2/x/y'); + }); + + it('Forwards params to the target path - spread param', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/source/x/y/z'); + const response = await app.render(request); + expect(response.headers.get('Location')).to.equal('/not-verbatim/target3/x/y/z'); + }); }); }); diff --git a/packages/astro/test/ssr-hoisted-script.test.js b/packages/astro/test/ssr-hoisted-script.test.js index e9549151e252..bebb766fbd69 100644 --- a/packages/astro/test/ssr-hoisted-script.test.js +++ b/packages/astro/test/ssr-hoisted-script.test.js @@ -11,46 +11,203 @@ async function fetchHTML(fixture, path) { return html; } -describe('Hoisted scripts in SSR', () => { +/** @type {import('./test-utils').AstroInlineConfig} */ +const defaultFixtureOptions = { + root: './fixtures/ssr-hoisted-script/', + output: 'server', + adapter: testAdapter(), +}; + +describe('Hoisted inline scripts in SSR', () => { /** @type {import('./test-utils').Fixture} */ let fixture; describe('without base path', () => { before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-hoisted-script/', - output: 'server', - adapter: testAdapter(), - }); + fixture = await loadFixture(defaultFixtureOptions); await fixture.build(); }); - it('Inlined scripts get included', async () => { + it('scripts get included', async () => { const html = await fetchHTML(fixture, '/'); const $ = cheerioLoad(html); expect($('script').length).to.equal(1); }); }); + + describe('with base path', () => { + const base = '/hello'; + + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + base, + }); + await fixture.build(); + }); + + it('Inlined scripts get included without base path in the script', async () => { + const html = await fetchHTML(fixture, '/hello/'); + const $ = cheerioLoad(html); + expect($('script').html()).to.equal('console.log("hello world");\n'); + }); + }); }); -describe('Hoisted scripts in SSR with base path', () => { +describe('Hoisted external scripts in SSR', () => { /** @type {import('./test-utils').Fixture} */ let fixture; - const base = '/hello'; - before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-hoisted-script/', - output: 'server', - adapter: testAdapter(), - base, + describe('without base path', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + vite: { + build: { + assetsInlineLimit: 0, + }, + }, + }); + await fixture.build(); + }); + + it('script has correct path', async () => { + const html = await fetchHTML(fixture, '/'); + const $ = cheerioLoad(html); + expect($('script').attr('src')).to.match(/^\/_astro\/hoisted\..{8}\.js$/); + }); + }); + + describe('with base path', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + vite: { + build: { + assetsInlineLimit: 0, + }, + }, + base: '/hello', + }); + await fixture.build(); + }); + + it('script has correct path', async () => { + const html = await fetchHTML(fixture, '/hello/'); + const $ = cheerioLoad(html); + expect($('script').attr('src')).to.match(/^\/hello\/_astro\/hoisted\..{8}\.js$/); + }); + }); + + describe('with assetsPrefix', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + vite: { + build: { + assetsInlineLimit: 0, + }, + }, + build: { + assetsPrefix: 'https://cdn.example.com', + }, + }); + await fixture.build(); + }); + + it('script has correct path', async () => { + const html = await fetchHTML(fixture, '/'); + const $ = cheerioLoad(html); + expect($('script').attr('src')).to.match( + /^https:\/\/cdn\.example\.com\/_astro\/hoisted\..{8}\.js$/ + ); }); - await fixture.build(); }); - it('Inlined scripts get included without base path in the script', async () => { - const html = await fetchHTML(fixture, '/hello/'); - const $ = cheerioLoad(html); - expect($('script').html()).to.equal('console.log("hello world");\n'); + describe('with custom rollup output file names', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + vite: { + build: { + assetsInlineLimit: 0, + rollupOptions: { + output: { + entryFileNames: 'assets/entry.[hash].mjs', + chunkFileNames: 'assets/chunks/chunk.[hash].mjs', + assetFileNames: 'assets/asset.[hash][extname]', + }, + }, + }, + }, + }); + await fixture.build(); + }); + + it('script has correct path', async () => { + const html = await fetchHTML(fixture, '/'); + const $ = cheerioLoad(html); + expect($('script').attr('src')).to.match(/^\/assets\/entry\..{8}\.mjs$/); + }); + }); + + describe('with custom rollup output file names and base', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + vite: { + build: { + assetsInlineLimit: 0, + rollupOptions: { + output: { + entryFileNames: 'assets/entry.[hash].mjs', + chunkFileNames: 'assets/chunks/chunk.[hash].mjs', + assetFileNames: 'assets/asset.[hash][extname]', + }, + }, + }, + }, + base: '/hello', + }); + await fixture.build(); + }); + + it('script has correct path', async () => { + const html = await fetchHTML(fixture, '/hello/'); + const $ = cheerioLoad(html); + expect($('script').attr('src')).to.match(/^\/hello\/assets\/entry\..{8}\.mjs$/); + }); + }); + + describe('with custom rollup output file names and assetsPrefix', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + vite: { + build: { + assetsInlineLimit: 0, + rollupOptions: { + output: { + entryFileNames: 'assets/entry.[hash].mjs', + chunkFileNames: 'assets/chunks/chunk.[hash].mjs', + assetFileNames: 'assets/asset.[hash][extname]', + }, + }, + }, + }, + build: { + assetsPrefix: 'https://cdn.example.com', + }, + }); + await fixture.build(); + }); + + it('script has correct path', async () => { + const html = await fetchHTML(fixture, '/'); + const $ = cheerioLoad(html); + expect($('script').attr('src')).to.match( + /^https:\/\/cdn\.example\.com\/assets\/entry\..{8}\.mjs$/ + ); + }); }); }); diff --git a/packages/integrations/node/README.md b/packages/integrations/node/README.md index af11405c0474..d7e86a5b7425 100644 --- a/packages/integrations/node/README.md +++ b/packages/integrations/node/README.md @@ -158,7 +158,7 @@ In standalone mode a server starts when the server entrypoint is run. By default node ./dist/server/entry.mjs ``` -For standalone mode the server handles file servering in addition to the page and API routes. +For standalone mode the server handles file serving in addition to the page and API routes. #### Custom host and port diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js index 05ee66c6a817..4c1aac9334d4 100644 --- a/packages/integrations/react/server.js +++ b/packages/integrations/react/server.js @@ -23,6 +23,10 @@ async function check(Component, props, children) { } if (typeof Component !== 'function') return false; + // Preact forwarded-ref components can be functions, which React does not support + if (typeof Component === 'function' && Component['$$typeof'] === Symbol.for('react.forward_ref')) + return false; + if (Component.prototype != null && typeof Component.prototype.render === 'function') { return React.Component.isPrototypeOf(Component) || React.PureComponent.isPrototypeOf(Component); }