diff --git a/.changeset/dull-weeks-trade.md b/.changeset/dull-weeks-trade.md new file mode 100644 index 0000000000000..da3e94d354f38 --- /dev/null +++ b/.changeset/dull-weeks-trade.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Throw an error when `build.split` is set to `true` but `output` isn't set to `"server"`. diff --git a/.changeset/great-days-judge.md b/.changeset/great-days-judge.md new file mode 100644 index 0000000000000..5c08417fe6afa --- /dev/null +++ b/.changeset/great-days-judge.md @@ -0,0 +1,10 @@ +--- +'@astrojs/netlify': minor +--- + +When a project uses the new option Astro `build.excludeMiddleware`, the +`@astrojs/netlify/functions` adapter will automatically create an Edge Middleware +that will automatically communicate with the Astro Middleware. + +Check the [documentation](https://github.com/withastro/astro/blob/main/packages/integrations/netlify/README.md#edge-middleware-with-astro-middleware) for more details. + diff --git a/.changeset/large-meals-joke.md b/.changeset/large-meals-joke.md new file mode 100644 index 0000000000000..45060ea9390a8 --- /dev/null +++ b/.changeset/large-meals-joke.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Fix a bug where asset redirects caused Cloudflare error diff --git a/.changeset/light-jars-cheat.md b/.changeset/light-jars-cheat.md new file mode 100644 index 0000000000000..c286810b4a22c --- /dev/null +++ b/.changeset/light-jars-cheat.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Handle inlining non-string boolean environment variables diff --git a/.changeset/six-baboons-allow.md b/.changeset/six-baboons-allow.md new file mode 100644 index 0000000000000..e55e044e3dfde --- /dev/null +++ b/.changeset/six-baboons-allow.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': patch +--- + +Fix build error when passing `includeFiles` diff --git a/.changeset/spicy-pugs-wonder.md b/.changeset/spicy-pugs-wonder.md new file mode 100644 index 0000000000000..568538810eb8d --- /dev/null +++ b/.changeset/spicy-pugs-wonder.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Fix runtime env var handling diff --git a/.changeset/sweet-bats-clap.md b/.changeset/sweet-bats-clap.md new file mode 100644 index 0000000000000..8cf15f8e530c1 --- /dev/null +++ b/.changeset/sweet-bats-clap.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Fix bug where `.ts` files are not renamed to `.js` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c94e8d4d9e84..329835b8035e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: - name: Use Deno uses: denoland/setup-deno@v1 with: - deno-version: v1.34.1 + deno-version: v1.35.0 - name: Install dependencies run: pnpm install diff --git a/examples/blog/package.json b/examples/blog/package.json index ef8f1ebe5da3a..720b1487c5c15 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -13,7 +13,7 @@ "dependencies": { "@astrojs/mdx": "^0.19.7", "@astrojs/rss": "^2.4.3", - "@astrojs/sitemap": "^1.3.3", + "@astrojs/sitemap": "^1.4.0", "astro": "^2.8.3" } } diff --git a/packages/astro/package.json b/packages/astro/package.json index 15412723b515a..38fb520981b02 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -107,8 +107,8 @@ "postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"", "test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js", "test:unit:match": "mocha --exit --timeout 30000 ./test/units/**/*.test.js -g", - "test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js", - "test:match": "mocha --timeout 20000 -g", + "test": "pnpm run test:unit && mocha --exit --timeout 30000 --ignore **/lit-element.test.js && mocha --timeout 30000 **/lit-element.test.js", + "test:match": "mocha --timeout 30000 -g", "test:e2e": "playwright test", "test:e2e:match": "playwright test -g" }, diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index b49a4728aa465..92f954bc52c95 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -2,23 +2,21 @@ import mime from 'mime'; import type { EndpointHandler, ManifestData, - MiddlewareResponseHandler, RouteData, SSRElement, SSRManifest, } from '../../@types/astro'; import type { SinglePageBuiltModule } from '../build/types'; import { attachToResponse, getSetCookiesFromResponse } from '../cookies/index.js'; -import { callEndpoint, createAPIContext } from '../endpoint/index.js'; +import { callEndpoint } from '../endpoint/index.js'; import { consoleLogDestination } from '../logger/console.js'; import { error, type LogOptions } from '../logger/core.js'; -import { callMiddleware } from '../middleware/callMiddleware.js'; import { prependForwardSlash, removeTrailingForwardSlash } from '../path.js'; import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; import { createEnvironment, createRenderContext, - renderPage, + tryRenderPage, type Environment, } from '../render/index.js'; import { RouteCache } from '../render/route-cache.js'; @@ -256,36 +254,8 @@ export class App { env: this.#env, }); - const apiContext = createAPIContext({ - request: renderContext.request, - params: renderContext.params, - props: renderContext.props, - site: this.#env.site, - adapterName: this.#env.adapterName, - }); - let response; - if (page.onRequest) { - response = await callMiddleware( - this.#env.logging, - page.onRequest as MiddlewareResponseHandler, - apiContext, - () => { - return renderPage({ - mod, - renderContext, - env: this.#env, - cookies: apiContext.cookies, - }); - } - ); - } else { - response = await renderPage({ - mod, - renderContext, - env: this.#env, - cookies: apiContext.cookies, - }); - } + const response = await tryRenderPage(renderContext, this.#env, mod, page.onRequest); + Reflect.set(request, responseSentSymbol, true); return response; } catch (err: any) { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 2bbc20c47db18..5be11e7474152 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -12,7 +12,6 @@ import type { GetStaticPathsItem, ImageTransform, MiddlewareHandler, - MiddlewareResponseHandler, RouteData, RouteType, SSRError, @@ -38,16 +37,15 @@ import { import { runHookBuildGenerated } from '../../integrations/index.js'; import { isServerLikeOutput } from '../../prerender/utils.js'; import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; -import { callEndpoint, createAPIContext, throwIfRedirectNotAllowed } from '../endpoint/index.js'; +import { callEndpoint, throwIfRedirectNotAllowed } from '../endpoint/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { debug, info } from '../logger/core.js'; -import { callMiddleware } from '../middleware/callMiddleware.js'; import { getRedirectLocationOrThrow, RedirectSinglePageBuiltModule, routeIsRedirect, } from '../redirects/index.js'; -import { createEnvironment, createRenderContext, renderPage } from '../render/index.js'; +import { createEnvironment, createRenderContext, tryRenderPage } from '../render/index.js'; import { callGetStaticPaths } from '../render/route-cache.js'; import { createAssetLink, @@ -575,36 +573,7 @@ async function generatePath( } else { let response: Response; try { - const apiContext = createAPIContext({ - request: renderContext.request, - params: renderContext.params, - props: renderContext.props, - site: env.site, - adapterName: env.adapterName, - }); - - if (onRequest) { - response = await callMiddleware( - env.logging, - onRequest as MiddlewareResponseHandler, - apiContext, - () => { - return renderPage({ - mod, - renderContext, - env, - cookies: apiContext.cookies, - }); - } - ); - } else { - response = await renderPage({ - mod, - renderContext, - env, - cookies: apiContext.cookies, - }); - } + response = await tryRenderPage(renderContext, env, mod, onRequest); } catch (err) { if (!AstroError.is(err) && !(err as SSRError).id && typeof err === 'object') { (err as SSRError).id = pageData.component; diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index 40cf9decd53ab..07223d29b2b2f 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -229,6 +229,14 @@ class AstroBuilder { ); } } + + if (config.build.split === true) { + if (config.output !== 'server') { + throw new Error( + 'The option `build.split` can only be used when `output` is set to `"server"`.' + ); + } + } } /** Stats */ diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 25834357e1e48..a4934f2772ba5 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -1,6 +1,13 @@ -import type { AstroCookies, ComponentInstance } from '../../@types/astro'; +import type { + AstroCookies, + ComponentInstance, + MiddlewareHandler, + MiddlewareResponseHandler, +} from '../../@types/astro'; import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js'; import { attachToResponse } from '../cookies/index.js'; +import { createAPIContext } from '../endpoint/index.js'; +import { callMiddleware } from '../middleware/callMiddleware.js'; import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js'; import type { RenderContext } from './context.js'; import type { Environment } from './environment.js'; @@ -13,7 +20,7 @@ export type RenderPage = { cookies: AstroCookies; }; -export async function renderPage({ mod, renderContext, env, cookies }: RenderPage) { +async function renderPage({ mod, renderContext, env, cookies }: RenderPage) { if (routeIsRedirect(renderContext.route)) { return new Response(null, { status: redirectRouteStatus(renderContext.route, renderContext.request.method), @@ -72,3 +79,48 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag return response; } + +/** + * It attempts to render a page. + * + * ## Errors + * + * It throws an error if the page can't be rendered. + */ +export async function tryRenderPage( + renderContext: Readonly, + env: Readonly, + mod: Readonly, + onRequest?: MiddlewareHandler +): Promise { + const apiContext = createAPIContext({ + request: renderContext.request, + params: renderContext.params, + props: renderContext.props, + site: env.site, + adapterName: env.adapterName, + }); + + if (onRequest) { + return await callMiddleware( + env.logging, + onRequest as MiddlewareResponseHandler, + apiContext, + () => { + return renderPage({ + mod, + renderContext, + env, + cookies: apiContext.cookies, + }); + } + ); + } else { + return await renderPage({ + mod, + renderContext, + env, + cookies: apiContext.cookies, + }); + } +} diff --git a/packages/astro/src/core/render/dev/index.ts b/packages/astro/src/core/render/dev/index.ts index 360832af47597..97b3a0c33109d 100644 --- a/packages/astro/src/core/render/dev/index.ts +++ b/packages/astro/src/core/render/dev/index.ts @@ -6,12 +6,10 @@ import type { SSRElement, } from '../../../@types/astro'; import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; -import { createAPIContext } from '../../endpoint/index.js'; import { enhanceViteSSRError } from '../../errors/dev/index.js'; import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js'; -import { callMiddleware } from '../../middleware/callMiddleware.js'; import { isPage, resolveIdToUrl, viteID } from '../../util.js'; -import { createRenderContext, loadRenderers, renderPage as coreRenderPage } from '../index.js'; +import { createRenderContext, loadRenderers, tryRenderPage } from '../index.js'; import { getStylesForURL } from './css.js'; import type { DevelopmentEnvironment } from './environment'; import { getComponentMetadata } from './metadata.js'; @@ -164,31 +162,7 @@ export async function renderPage(options: SSROptions): Promise { mod, env, }); - const apiContext = createAPIContext({ - request: options.request, - params: renderContext.params, - props: renderContext.props, - adapterName: options.env.adapterName, - }); - if (options.middleware) { - if (options.middleware?.onRequest) { - const onRequest = options.middleware.onRequest as MiddlewareResponseHandler; - const response = await callMiddleware(env.logging, onRequest, apiContext, () => { - return coreRenderPage({ - mod, - renderContext, - env: options.env, - cookies: apiContext.cookies, - }); - }); + const onRequest = options.middleware?.onRequest as MiddlewareResponseHandler | undefined; - return response; - } - } - return await coreRenderPage({ - mod, - renderContext, - env: options.env, - cookies: apiContext.cookies, - }); // NOTE: without "await", errors won’t get caught below + return tryRenderPage(renderContext, env, mod, onRequest); } diff --git a/packages/astro/src/core/render/index.ts b/packages/astro/src/core/render/index.ts index 9eb85f4d0d351..410998d0139a6 100644 --- a/packages/astro/src/core/render/index.ts +++ b/packages/astro/src/core/render/index.ts @@ -1,6 +1,6 @@ export { createRenderContext } from './context.js'; export type { RenderContext } from './context.js'; -export { renderPage } from './core.js'; +export { tryRenderPage } from './core.js'; export type { Environment } from './environment'; export { createEnvironment } from './environment.js'; export { getParamsAndProps } from './params-and-props.js'; diff --git a/packages/astro/src/vite-plugin-env/index.ts b/packages/astro/src/vite-plugin-env/index.ts index 8d5a9c1c3df89..75a37d693b85c 100644 --- a/packages/astro/src/vite-plugin-env/index.ts +++ b/packages/astro/src/vite-plugin-env/index.ts @@ -31,7 +31,11 @@ function getPrivateEnv( // Ignore public env var if (envPrefixes.every((prefix) => !key.startsWith(prefix))) { if (typeof process.env[key] !== 'undefined') { - const value = process.env[key]; + let value = process.env[key]; + // Replacements are always strings, so try to convert to strings here first + if (typeof value !== 'string') { + value = `${value}`; + } // Boolean values should be inlined to support `export const prerender` // We already know that these are NOT sensitive values, so inlining is safe if (value === '0' || value === '1' || value === 'true' || value === 'false') { diff --git a/packages/astro/test/astro-get-static-paths.test.js b/packages/astro/test/astro-get-static-paths.test.js index 6294e1926bf66..9ff97831c591e 100644 --- a/packages/astro/test/astro-get-static-paths.test.js +++ b/packages/astro/test/astro-get-static-paths.test.js @@ -54,7 +54,11 @@ describe('getStaticPaths - dev calls', () => { await devServer.stop(); }); - it('only calls getStaticPaths once', async () => { + it('only calls getStaticPaths once', async function () { + // Sometimes this fail in CI as the chokidar watcher triggers an update and invalidates the route cache, + // causing getStaticPaths to be called twice. Workaround this with 2 retries for now. + this.retries(2); + let res = await fixture.fetch('/a'); expect(res.status).to.equal(200); diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.js index ef6ef0b92228a..da4863fb13918 100644 --- a/packages/astro/test/units/render/head.test.js +++ b/packages/astro/test/units/render/head.test.js @@ -9,7 +9,7 @@ import { renderHead, Fragment, } from '../../../dist/runtime/server/index.js'; -import { createRenderContext, renderPage } from '../../../dist/core/render/index.js'; +import { createRenderContext, tryRenderPage } from '../../../dist/core/render/index.js'; import { createBasicEnvironment } from '../test-utils.js'; import * as cheerio from 'cheerio'; @@ -96,13 +96,7 @@ describe('core/render', () => { env, }); - const response = await renderPage({ - mod: PageModule, - renderContext: ctx, - env, - params: ctx.params, - props: ctx.props, - }); + const response = await tryRenderPage(ctx, env, PageModule); const html = await response.text(); const $ = cheerio.load(html); @@ -182,13 +176,7 @@ describe('core/render', () => { mod: PageModule, }); - const response = await renderPage({ - mod: PageModule, - renderContext: ctx, - env, - params: ctx.params, - props: ctx.props, - }); + const response = await tryRenderPage(ctx, env, PageModule); const html = await response.text(); const $ = cheerio.load(html); @@ -234,13 +222,7 @@ describe('core/render', () => { mod: PageModule, }); - const response = await renderPage({ - mod: PageModule, - renderContext: ctx, - env, - params: ctx.params, - props: ctx.props, - }); + const response = await tryRenderPage(ctx, env, PageModule); const html = await response.text(); const $ = cheerio.load(html); diff --git a/packages/astro/test/units/render/jsx.test.js b/packages/astro/test/units/render/jsx.test.js index 46058cbff85ec..61e5ee8030497 100644 --- a/packages/astro/test/units/render/jsx.test.js +++ b/packages/astro/test/units/render/jsx.test.js @@ -6,7 +6,11 @@ import { renderSlot, } from '../../../dist/runtime/server/index.js'; import { jsx } from '../../../dist/jsx-runtime/index.js'; -import { createRenderContext, renderPage, loadRenderer } from '../../../dist/core/render/index.js'; +import { + createRenderContext, + tryRenderPage, + loadRenderer, +} from '../../../dist/core/render/index.js'; import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js'; import { createBasicEnvironment } from '../test-utils.js'; @@ -46,11 +50,7 @@ describe('core/render', () => { mod, }); - const response = await renderPage({ - mod, - renderContext: ctx, - env, - }); + const response = await tryRenderPage(ctx, env, mod); expect(response.status).to.equal(200); @@ -94,11 +94,7 @@ describe('core/render', () => { env, mod, }); - const response = await renderPage({ - mod, - renderContext: ctx, - env, - }); + const response = await tryRenderPage(ctx, env, mod); expect(response.status).to.equal(200); @@ -124,11 +120,7 @@ describe('core/render', () => { mod, }); - const response = await renderPage({ - mod, - renderContext: ctx, - env, - }); + const response = await tryRenderPage(ctx, env, mod); try { await response.text(); diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index fa064e0ac2f62..22439c4ac7d6c 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -3,7 +3,7 @@ import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'ast import esbuild from 'esbuild'; import * as fs from 'fs'; import * as os from 'os'; -import { dirname } from 'path'; +import { sep } from 'path'; import glob from 'tiny-glob'; import { fileURLToPath, pathToFileURL } from 'url'; @@ -91,6 +91,14 @@ export default function createIntegration(args?: Options): AstroIntegration { } vite.ssr ||= {}; vite.ssr.target = 'webworker'; + + // Cloudflare env is only available per request. This isn't feasible for code that access env vars + // in a global way, so we shim their access as `process.env.*`. We will populate `process.env` later + // in its fetch handler. + vite.define = { + 'process.env': 'process.env', + ...vite.define, + }; } }, 'astro:build:ssr': ({ entryPoints }) => { @@ -104,13 +112,12 @@ export default function createIntegration(args?: Options): AstroIntegration { } if (isModeDirectory && _buildConfig.split) { - const entryPointsRouteData = [..._entryPoints.keys()]; const entryPointsURL = [..._entryPoints.values()]; const entryPaths = entryPointsURL.map((entry) => fileURLToPath(entry)); - const outputDir = fileURLToPath(new URL('.astro', _buildConfig.server)); + const outputUrl = new URL('$astro', _buildConfig.server); + const outputDir = fileURLToPath(outputUrl); - // NOTE: AFAIK, esbuild keeps the order of the entryPoints array - const { outputFiles } = await esbuild.build({ + await esbuild.build({ target: 'es2020', platform: 'browser', conditions: ['workerd', 'worker', 'browser'], @@ -126,28 +133,43 @@ export default function createIntegration(args?: Options): AstroIntegration { logOverride: { 'ignored-bare-import': 'silent', }, - write: false, }); - // loop through all bundled files and write them to the functions folder - for (const [index, outputFile] of outputFiles.entries()) { - // we need to make sure the filename in the functions folder - // matches to cloudflares routing capabilities (see their docs) - // IN: src/pages/[language]/files/[...path].astro - // OUT: [language]/files/[[path]].js - const fileName = entryPointsRouteData[index].component - .replace('src/pages/', '') - .replace('.astro', '.js') - .replace(/(\[\.\.\.)(\w+)(\])/g, (_match, _p1, p2) => { - return `[[${p2}]]`; + const outputFiles: Array = await glob(`**/*`, { + cwd: outputDir, + filesOnly: true, + }); + + // move the files into the functions folder + // & make sure the file names match Cloudflare syntax for routing + for (const outputFile of outputFiles) { + const path = outputFile.split(sep); + + const finalSegments = path.map((segment) => + segment + .replace(/(\_)(\w+)(\_)/g, (_, __, prop) => { + return `[${prop}]`; + }) + .replace(/(\_\-\-\-)(\w+)(\_)/g, (_, __, prop) => { + return `[[${prop}]]`; + }) + ); + + finalSegments[finalSegments.length - 1] = finalSegments[finalSegments.length - 1] + .replace('entry.', '') + .replace(/(.*)\.(\w+)\.(\w+)$/g, (_, fileName, __, newExt) => { + return `${fileName}.${newExt}`; }); - const fileUrl = new URL(fileName, functionsUrl); - const newFileDir = dirname(fileURLToPath(fileUrl)); - if (!fs.existsSync(newFileDir)) { - fs.mkdirSync(newFileDir, { recursive: true }); - } - await fs.promises.writeFile(fileUrl, outputFile.contents); + const finalDirPath = finalSegments.slice(0, -1).join(sep); + const finalPath = finalSegments.join(sep); + + const newDirUrl = new URL(finalDirPath, functionsUrl); + await fs.promises.mkdir(newDirUrl, { recursive: true }); + + const oldFileUrl = new URL(`$astro/${outputFile}`, outputUrl); + const newFileUrl = new URL(finalPath, functionsUrl); + await fs.promises.rename(oldFileUrl, newFileUrl); } } else { const entryPath = fileURLToPath(new URL(_buildConfig.serverEntry, _buildConfig.server)); diff --git a/packages/integrations/cloudflare/src/server.directory.ts b/packages/integrations/cloudflare/src/server.directory.ts index f9f71a33b5b8f..50607b65ad74c 100644 --- a/packages/integrations/cloudflare/src/server.directory.ts +++ b/packages/integrations/cloudflare/src/server.directory.ts @@ -24,7 +24,11 @@ export function createExports(manifest: SSRManifest) { const { pathname } = new URL(request.url); // static assets fallback, in case default _routes.json is not used if (manifest.assets.has(pathname)) { - return next(request); + // 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 + ); } let routeData = app.match(request, { matchNotFound: true }); diff --git a/packages/integrations/cloudflare/test/basics.test.js b/packages/integrations/cloudflare/test/basics.test.js index d382a9084e1cf..9aa78f98e4cac 100644 --- a/packages/integrations/cloudflare/test/basics.test.js +++ b/packages/integrations/cloudflare/test/basics.test.js @@ -2,31 +2,32 @@ import { loadFixture, runCLI } from './test-utils.js'; import { expect } from 'chai'; import * as cheerio from 'cheerio'; -describe.skip('Basic app', () => { +describe('Basic app', () => { /** @type {import('./test-utils').Fixture} */ let fixture; + /** @type {import('./test-utils').WranglerCLI} */ + let cli; before(async () => { fixture = await loadFixture({ root: './fixtures/basics/', }); await fixture.build(); - }); - it('can render', async () => { - const { ready, stop } = runCLI('./fixtures/basics/', { silent: true, port: 8789 }); + cli = runCLI('./fixtures/basics/', { silent: true, port: 8789 }); + await cli.ready; + }); - try { - await ready; + after(async () => { + await cli.stop(); + }); - let res = await fetch(`http://localhost:8789/`); - expect(res.status).to.equal(200); - let html = await res.text(); - let $ = cheerio.load(html); - expect($('h1').text()).to.equal('Testing'); - expect($('#env').text()).to.equal('secret'); - } finally { - await stop(); - } + it('can render', async () => { + let res = await fetch(`http://localhost:8789/`); + expect(res.status).to.equal(200); + let html = await res.text(); + let $ = cheerio.load(html); + expect($('h1').text()).to.equal('Testing'); + expect($('#env').text()).to.equal('secret'); }); }); diff --git a/packages/integrations/cloudflare/test/cf.test.js b/packages/integrations/cloudflare/test/cf.test.js index cf310f34d000c..559df5c7601e2 100644 --- a/packages/integrations/cloudflare/test/cf.test.js +++ b/packages/integrations/cloudflare/test/cf.test.js @@ -6,6 +6,8 @@ import cloudflare from '../dist/index.js'; describe('Cf metadata and caches', () => { /** @type {import('./test-utils').Fixture} */ let fixture; + /** @type {import('./test-utils').WranglerCLI} */ + let cli; before(async () => { fixture = await loadFixture({ @@ -14,22 +16,22 @@ describe('Cf metadata and caches', () => { adapter: cloudflare(), }); await fixture.build(); + + cli = runCLI('./fixtures/cf/', { silent: true, port: 8788 }); + await cli.ready; }); - it('Load cf and caches API', async () => { - const { ready, stop } = runCLI('./fixtures/cf/', { silent: true, port: 8788 }); + after(async () => { + await cli.stop(); + }); - try { - await ready; - let res = await fetch(`http://localhost:8788/`); - expect(res.status).to.equal(200); - let html = await res.text(); - let $ = cheerio.load(html); - // console.log($('#cf').text(), html); - expect($('#cf').text()).to.contain('city'); - expect($('#hasCache').text()).to.equal('true'); - } finally { - await stop(); - } + it('Load cf and caches API', async () => { + let res = await fetch(`http://localhost:8788/`); + expect(res.status).to.equal(200); + let html = await res.text(); + let $ = cheerio.load(html); + // console.log($('#cf').text(), html); + expect($('#cf').text()).to.contain('city'); + expect($('#hasCache').text()).to.equal('true'); }); }); diff --git a/packages/integrations/cloudflare/test/directory-split.test.js b/packages/integrations/cloudflare/test/directory-split.test.js index 384543a4b896d..6e6b0bfe29aaf 100644 --- a/packages/integrations/cloudflare/test/directory-split.test.js +++ b/packages/integrations/cloudflare/test/directory-split.test.js @@ -36,6 +36,9 @@ describe('Cloudflare SSR split', () => { expect(await fixture.pathExists('../functions/[person]/[car].js')).to.be.true; expect(await fixture.pathExists('../functions/files/[[path]].js')).to.be.true; expect(await fixture.pathExists('../functions/[language]/files/[[path]].js')).to.be.true; + expect(await fixture.pathExists('../functions/trpc/[trpc].js')).to.be.true; + expect(await fixture.pathExists('../functions/javascript.js')).to.be.true; + expect(await fixture.pathExists('../functions/test.json.js')).to.be.true; }); it('generates pre-rendered files', async () => { diff --git a/packages/integrations/cloudflare/test/fixtures/split/src/pages/javascript.js b/packages/integrations/cloudflare/test/fixtures/split/src/pages/javascript.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/integrations/cloudflare/test/fixtures/split/src/pages/test.json.ts b/packages/integrations/cloudflare/test/fixtures/split/src/pages/test.json.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/integrations/cloudflare/test/fixtures/split/src/pages/trpc/[trpc].ts b/packages/integrations/cloudflare/test/fixtures/split/src/pages/trpc/[trpc].ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/integrations/cloudflare/test/test-utils.js b/packages/integrations/cloudflare/test/test-utils.js index e0fc90a64e454..bff7fb2a42ff3 100644 --- a/packages/integrations/cloudflare/test/test-utils.js +++ b/packages/integrations/cloudflare/test/test-utils.js @@ -5,6 +5,7 @@ import { fileURLToPath } from 'url'; export { fixLineEndings } from '../../../astro/test/test-utils.js'; /** + * @typedef {{ ready: Promise, stop: Promise }} WranglerCLI * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture */ @@ -19,6 +20,9 @@ const wranglerPath = fileURLToPath( new URL('../node_modules/wrangler/bin/wrangler.js', import.meta.url) ); +/** + * @returns {WranglerCLI} + */ export function runCLI(basePath, { silent, port = 8787 }) { const script = fileURLToPath(new URL(`${basePath}/dist/_worker.js`, import.meta.url)); const p = spawn('node', [wranglerPath, 'dev', '-l', script, '--port', port]); diff --git a/packages/integrations/cloudflare/test/with-solid-js.test.js b/packages/integrations/cloudflare/test/with-solid-js.test.js index 270c387b54dbb..90c1c07223024 100644 --- a/packages/integrations/cloudflare/test/with-solid-js.test.js +++ b/packages/integrations/cloudflare/test/with-solid-js.test.js @@ -5,27 +5,28 @@ import * as cheerio from 'cheerio'; describe('With SolidJS', () => { /** @type {import('./test-utils').Fixture} */ let fixture; + /** @type {import('./test-utils').WranglerCLI} */ + let cli; before(async () => { fixture = await loadFixture({ root: './fixtures/with-solid-js/', }); await fixture.build(); - }); - it('renders the solid component', async () => { - const { ready, stop } = runCLI('./fixtures/with-solid-js/', { silent: true, port: 8790 }); + cli = runCLI('./fixtures/with-solid-js/', { silent: true, port: 8790 }); + await cli.ready; + }); - try { - await ready; + after(async () => { + await cli.stop(); + }); - let res = await fetch(`http://localhost:8790/`); - expect(res.status).to.equal(200); - let html = await res.text(); - let $ = cheerio.load(html); - expect($('.solid').text()).to.equal('Solid Content'); - } finally { - await stop(); - } + it('renders the solid component', async () => { + let res = await fetch(`http://localhost:8790/`); + expect(res.status).to.equal(200); + let html = await res.text(); + let $ = cheerio.load(html); + expect($('.solid').text()).to.equal('Solid Content'); }); }); diff --git a/packages/integrations/netlify/README.md b/packages/integrations/netlify/README.md index 5e7895217643e..f20c535024682 100644 --- a/packages/integrations/netlify/README.md +++ b/packages/integrations/netlify/README.md @@ -115,6 +115,63 @@ 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. +### Edge Middleware with Astro middleware + +The `@astrojs/netlify/functions` adapter can automatically create an edge function that will act as "Edge Middleware", from an Astro middleware in your code base. + +This is an opt-in feature and the `build.excludeMiddleware` option needs to be set to `true`: + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import netlify from '@astrojs/netlify/functions'; +export default defineConfig({ + output: 'server', + adapter: netlify(), + build: { + excludeMiddleware: true, + }, +}); +``` + +Optionally, you can create a file recognized by the adapter named `netlify-edge-middleware.(js|ts)` in the [`srcDir`](https://docs.astro.build/en/reference/configuration-reference/#srcdir) folder to create [`Astro.locals`](https://docs.astro.build/en/reference/api-reference/#astrolocals). + +Typings require the [`https://edge.netlify.com`](https://docs.netlify.com/edge-functions/api/#reference) types. + +> Netlify edge functions run in a Deno environment, so you would need to import types using URLs. +> +> You can find more in the [Netlify documentation page](https://docs.netlify.com/edge-functions/api/#runtime-environment) + +```ts +// src/netlify-edge-middleware.ts +import type { Context } from 'https://edge.netlify.com'; + +export default function ({ request, context }: { request: Request; context: Context }): object { + // do something with request and context + return { + title: "Spider-man's blog", + }; +} +``` + +The data returned by this function will be passed to Astro middleware. + +The function: + +- must export a **default** function; +- must **return** an `object`; +- accepts an object with a `request` and `context` as properties; +- `request` is typed as [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request); +- `context` is typed as [`Context`](https://docs.netlify.com/edge-functions/api/#edge-function-types); + +#### Limitations and constraints + +When you opt-in to this feature, there are a few constraints to note: + +- The Edge middleware will always be the **first** function to receive the `Request` and the last function to receive `Response`. This is an architectural constraint that follows the [boundaries set by Netlify](https://docs.netlify.com/edge-functions/overview/#use-cases). +- Only `request` and `context` may be used to produce an `Astro.locals` object. Operations like redirects, etc. should be delegated to Astro middleware. +- `Astro.locals` **must be serializable**. Failing to do so will result in a **runtime error**. This means that you **cannot** store complex types like `Map`, `function`, `Set`, etc. + ## Usage [Read the full deployment guide here.](https://docs.astro.build/en/guides/deploy/netlify/) diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 0b3ada36bb89d..632e5af198a9a 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -33,8 +33,8 @@ "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", "test-fn": "mocha --exit --timeout 20000 --file \"./test/setup.js\" test/functions/", - "test-edge": "deno test --allow-run --allow-read --allow-net --allow-env ./test/edge-functions/", - "test": "npm run test-fn" + "test-edge": "deno test --allow-run --allow-read --allow-net --allow-env --allow-write ./test/edge-functions/", + "test": "pnpm test-fn" }, "dependencies": { "@astrojs/underscore-redirects": "^0.2.0", diff --git a/packages/integrations/netlify/src/integration-edge-functions.ts b/packages/integrations/netlify/src/integration-edge-functions.ts index 72721bd592d11..ac7c124fb93e8 100644 --- a/packages/integrations/netlify/src/integration-edge-functions.ts +++ b/packages/integrations/netlify/src/integration-edge-functions.ts @@ -1,21 +1,10 @@ import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; -import esbuild from 'esbuild'; -import * as fs from 'fs'; -import * as npath from 'path'; -import { fileURLToPath } from 'url'; -import { createRedirects } from './shared.js'; - -interface BuildConfig { - server: URL; - client: URL; - serverEntry: string; - assets: string; -} - -const SHIM = `globalThis.process = { - argv: [], - env: Deno.env.toObject(), -};`; +import { + bundleServerEntry, + createEdgeManifest, + createRedirects, + type NetlifyEdgeFunctionsOptions, +} from './shared.js'; export function getAdapter(): AstroAdapter { return { @@ -25,92 +14,10 @@ export function getAdapter(): AstroAdapter { }; } -interface NetlifyEdgeFunctionsOptions { - dist?: URL; -} - -interface NetlifyEdgeFunctionManifestFunctionPath { - function: string; - path: string; -} - -interface NetlifyEdgeFunctionManifestFunctionPattern { - function: string; - pattern: string; -} - -type NetlifyEdgeFunctionManifestFunction = - | NetlifyEdgeFunctionManifestFunctionPath - | NetlifyEdgeFunctionManifestFunctionPattern; - -interface NetlifyEdgeFunctionManifest { - functions: NetlifyEdgeFunctionManifestFunction[]; - version: 1; -} - -async function createEdgeManifest(routes: RouteData[], entryFile: string, dir: URL) { - const functions: NetlifyEdgeFunctionManifestFunction[] = []; - for (const route of routes) { - if (route.pathname) { - functions.push({ - function: entryFile, - path: route.pathname, - }); - } else { - functions.push({ - function: entryFile, - // Make route pattern serializable to match expected - // Netlify Edge validation format. Mirrors Netlify's own edge bundler: - // https://github.com/netlify/edge-bundler/blob/main/src/manifest.ts#L34 - pattern: route.pattern.source.replace(/\\\//g, '/').toString(), - }); - } - } - - const manifest: NetlifyEdgeFunctionManifest = { - functions, - version: 1, - }; - - const baseDir = new URL('./.netlify/edge-functions/', dir); - await fs.promises.mkdir(baseDir, { recursive: true }); - - const manifestURL = new URL('./manifest.json', baseDir); - const _manifest = JSON.stringify(manifest, null, ' '); - await fs.promises.writeFile(manifestURL, _manifest, 'utf-8'); -} - -async function bundleServerEntry({ serverEntry, server }: BuildConfig, vite: any) { - const entryUrl = new URL(serverEntry, server); - const pth = fileURLToPath(entryUrl); - await esbuild.build({ - target: 'es2020', - platform: 'browser', - entryPoints: [pth], - outfile: pth, - allowOverwrite: true, - format: 'esm', - bundle: true, - external: ['@astrojs/markdown-remark'], - banner: { - js: SHIM, - }, - }); - - // Remove chunks, if they exist. Since we have bundled via esbuild these chunks are trash. - try { - const chunkFileNames = - vite?.build?.rollupOptions?.output?.chunkFileNames ?? `chunks/chunk.[hash].mjs`; - const chunkPath = npath.dirname(chunkFileNames); - const chunksDirUrl = new URL(chunkPath + '/', server); - await fs.promises.rm(chunksDirUrl, { recursive: true, force: true }); - } catch {} -} - export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}): AstroIntegration { let _config: AstroConfig; let entryFile: string; - let _buildConfig: BuildConfig; + let _buildConfig: AstroConfig['build']; let _vite: any; return { name: '@astrojs/netlify/edge-functions', @@ -164,7 +71,8 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {}) } }, 'astro:build:done': async ({ routes, dir }) => { - await bundleServerEntry(_buildConfig, _vite); + const entryUrl = new URL(_buildConfig.serverEntry, _buildConfig.server); + await bundleServerEntry(entryUrl, _buildConfig.server, _vite); await createEdgeManifest(routes, entryFile, _config.root); const dynamicTarget = `/.netlify/edge-functions/${entryFile}`; const map: [RouteData, string][] = routes.map((route) => { diff --git a/packages/integrations/netlify/src/integration-functions.ts b/packages/integrations/netlify/src/integration-functions.ts index 3e8c476e583b3..f8b9171af9263 100644 --- a/packages/integrations/netlify/src/integration-functions.ts +++ b/packages/integrations/netlify/src/integration-functions.ts @@ -1,9 +1,13 @@ import type { AstroAdapter, AstroConfig, AstroIntegration, RouteData } from 'astro'; import { extname } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { generateEdgeMiddleware } from './middleware.js'; import type { Args } from './netlify-functions.js'; import { createRedirects } from './shared.js'; +export const NETLIFY_EDGE_MIDDLEWARE_FILE = 'netlify-edge-middleware'; +export const ASTRO_LOCALS_HEADER = 'x-astro-locals'; + export function getAdapter(args: Args = {}): AstroAdapter { return { name: '@astrojs/netlify/functions', @@ -27,6 +31,7 @@ function netlifyFunctions({ let _config: AstroConfig; let _entryPoints: Map; let ssrEntryFile: string; + let _middlewareEntryPoint: URL; return { name: '@astrojs/netlify', hooks: { @@ -40,7 +45,10 @@ function netlifyFunctions({ }, }); }, - 'astro:build:ssr': ({ entryPoints }) => { + 'astro:build:ssr': async ({ entryPoints, middlewareEntryPoint }) => { + if (middlewareEntryPoint) { + _middlewareEntryPoint = middlewareEntryPoint; + } _entryPoints = entryPoints; }, 'astro:config:done': ({ config, setAdapter }) => { @@ -85,6 +93,18 @@ function netlifyFunctions({ await createRedirects(_config, routeToDynamicTargetMap, dir); } + if (_middlewareEntryPoint) { + const outPath = fileURLToPath(new URL('./.netlify/edge-functions/', _config.root)); + const netlifyEdgeMiddlewareHandlerPath = new URL( + NETLIFY_EDGE_MIDDLEWARE_FILE, + _config.srcDir + ); + await generateEdgeMiddleware( + _middlewareEntryPoint, + outPath, + netlifyEdgeMiddlewareHandlerPath + ); + } }, }, }; diff --git a/packages/integrations/netlify/src/middleware.ts b/packages/integrations/netlify/src/middleware.ts new file mode 100644 index 0000000000000..3c2f4f6974be7 --- /dev/null +++ b/packages/integrations/netlify/src/middleware.ts @@ -0,0 +1,75 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { ASTRO_LOCALS_HEADER } from './integration-functions.js'; +import { DENO_SHIM } from './shared.js'; + +/** + * It generates a Netlify edge function. + * + */ +export async function generateEdgeMiddleware( + astroMiddlewareEntryPointPath: URL, + outPath: string, + netlifyEdgeMiddlewareHandlerPath: URL +): Promise { + const entryPointPathURLAsString = JSON.stringify( + fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/') + ); + + const code = edgeMiddlewareTemplate(entryPointPathURLAsString, netlifyEdgeMiddlewareHandlerPath); + const bundledFilePath = join(outPath, 'edgeMiddleware.js'); + const esbuild = await import('esbuild'); + await esbuild.build({ + stdin: { + contents: code, + resolveDir: process.cwd(), + }, + target: 'es2020', + platform: 'browser', + outfile: bundledFilePath, + allowOverwrite: true, + format: 'esm', + bundle: true, + minify: false, + banner: { + js: DENO_SHIM, + }, + }); + return pathToFileURL(bundledFilePath); +} + +function edgeMiddlewareTemplate(middlewarePath: string, netlifyEdgeMiddlewareHandlerPath: URL) { + const filePathEdgeMiddleware = fileURLToPath(netlifyEdgeMiddlewareHandlerPath); + let handlerTemplateImport = ''; + let handlerTemplateCall = '{}'; + if (existsSync(filePathEdgeMiddleware + '.js') || existsSync(filePathEdgeMiddleware + '.ts')) { + const stringified = JSON.stringify(filePathEdgeMiddleware.replace(/\\/g, '/')); + handlerTemplateImport = `import handler from ${stringified}`; + handlerTemplateCall = `handler({ request, context })`; + } else { + } + return ` + ${handlerTemplateImport} +import { onRequest } from ${middlewarePath}; +import { createContext, trySerializeLocals } from 'astro/middleware'; +export default async function middleware(request, context) { + const url = new URL(request.url); + const ctx = createContext({ + request, + params: {} + }); + ctx.locals = ${handlerTemplateCall}; + const next = async () => { + request.headers.set(${JSON.stringify(ASTRO_LOCALS_HEADER)}, trySerializeLocals(ctx.locals)); + return await context.next(); + }; + + return onRequest(ctx, next); +} + +export const config = { + path: "/*" +} +`; +} diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts index 915f72955105e..8d0196d5e4591 100644 --- a/packages/integrations/netlify/src/netlify-functions.ts +++ b/packages/integrations/netlify/src/netlify-functions.ts @@ -2,6 +2,7 @@ import { polyfill } from '@astrojs/webapi'; import { builder, type Handler } from '@netlify/functions'; import type { SSRManifest } from 'astro'; import { App } from 'astro/app'; +import { ASTRO_LOCALS_HEADER } from './integration-functions.js'; polyfill(globalThis, { exclude: 'window document', @@ -80,8 +81,14 @@ export const createExports = (manifest: SSRManifest, args: Args) => { const ip = headers['x-nf-client-connection-ip']; Reflect.set(request, clientAddressSymbol, ip); - - const response: Response = await app.render(request, routeData); + let locals = {}; + if (request.headers.has(ASTRO_LOCALS_HEADER)) { + let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER); + if (localsAsString) { + locals = JSON.parse(localsAsString); + } + } + const response: Response = await app.render(request, routeData, locals); const responseHeaders = Object.fromEntries(response.headers.entries()); const responseContentType = parseContentType(responseHeaders['content-type']); diff --git a/packages/integrations/netlify/src/shared.ts b/packages/integrations/netlify/src/shared.ts index ca45dc752c359..91ce49ef6e400 100644 --- a/packages/integrations/netlify/src/shared.ts +++ b/packages/integrations/netlify/src/shared.ts @@ -1,6 +1,37 @@ import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects'; import type { AstroConfig, RouteData } from 'astro'; +import esbuild from 'esbuild'; import fs from 'node:fs'; +import npath from 'path'; +import { fileURLToPath } from 'url'; + +export const DENO_SHIM = `globalThis.process = { + argv: [], + env: Deno.env.toObject(), +};`; + +export interface NetlifyEdgeFunctionsOptions { + dist?: URL; +} + +export interface NetlifyEdgeFunctionManifestFunctionPath { + function: string; + path: string; +} + +export interface NetlifyEdgeFunctionManifestFunctionPattern { + function: string; + pattern: string; +} + +export type NetlifyEdgeFunctionManifestFunction = + | NetlifyEdgeFunctionManifestFunctionPath + | NetlifyEdgeFunctionManifestFunctionPattern; + +export interface NetlifyEdgeFunctionManifest { + functions: NetlifyEdgeFunctionManifestFunction[]; + version: 1; +} export async function createRedirects( config: AstroConfig, @@ -21,3 +52,63 @@ export async function createRedirects( // If the file does not exist yet, appendFile() automatically creates it. await fs.promises.appendFile(_redirectsURL, content, 'utf-8'); } + +export async function createEdgeManifest(routes: RouteData[], entryFile: string, dir: URL) { + const functions: NetlifyEdgeFunctionManifestFunction[] = []; + for (const route of routes) { + if (route.pathname) { + functions.push({ + function: entryFile, + path: route.pathname, + }); + } else { + functions.push({ + function: entryFile, + // Make route pattern serializable to match expected + // Netlify Edge validation format. Mirrors Netlify's own edge bundler: + // https://github.com/netlify/edge-bundler/blob/main/src/manifest.ts#L34 + pattern: route.pattern.source.replace(/\\\//g, '/').toString(), + }); + } + } + + const manifest: NetlifyEdgeFunctionManifest = { + functions, + version: 1, + }; + + const baseDir = new URL('./.netlify/edge-functions/', dir); + await fs.promises.mkdir(baseDir, { recursive: true }); + + const manifestURL = new URL('./manifest.json', baseDir); + const _manifest = JSON.stringify(manifest, null, ' '); + await fs.promises.writeFile(manifestURL, _manifest, 'utf-8'); +} + +export async function bundleServerEntry(entryUrl: URL, serverUrl?: URL, vite?: any | undefined) { + const pth = fileURLToPath(entryUrl); + await esbuild.build({ + target: 'es2020', + platform: 'browser', + entryPoints: [pth], + outfile: pth, + allowOverwrite: true, + format: 'esm', + bundle: true, + external: ['@astrojs/markdown-remark', 'astro/middleware'], + banner: { + js: DENO_SHIM, + }, + }); + + // Remove chunks, if they exist. Since we have bundled via esbuild these chunks are trash. + if (vite && serverUrl) { + try { + const chunkFileNames = + vite?.build?.rollupOptions?.output?.chunkFileNames ?? `chunks/chunk.[hash].mjs`; + const chunkPath = npath.dirname(chunkFileNames); + const chunksDirUrl = new URL(chunkPath + '/', serverUrl); + await fs.promises.rm(chunksDirUrl, { recursive: true, force: true }); + } catch {} + } +} diff --git a/packages/integrations/netlify/test/edge-functions/deps.ts b/packages/integrations/netlify/test/edge-functions/deps.ts index c6ced8814faca..6d729970d0990 100644 --- a/packages/integrations/netlify/test/edge-functions/deps.ts +++ b/packages/integrations/netlify/test/edge-functions/deps.ts @@ -5,7 +5,7 @@ export { assert, assertExists, } from 'https://deno.land/std@0.132.0/testing/asserts.ts'; -export * from 'https://deno.land/x/deno_dom/deno-dom-wasm.ts'; +export { DOMParser } from 'https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts'; export * from 'https://deno.land/std@0.142.0/streams/conversion.ts'; export * as cheerio from 'https://cdn.skypack.dev/cheerio?dts'; export * as fs from 'https://deno.land/std/fs/mod.ts'; diff --git a/packages/integrations/netlify/test/edge-functions/dynamic-import.test.js b/packages/integrations/netlify/test/edge-functions/dynamic-import.test.ts similarity index 65% rename from packages/integrations/netlify/test/edge-functions/dynamic-import.test.js rename to packages/integrations/netlify/test/edge-functions/dynamic-import.test.ts index bfa895290d5ee..89a640b0b856b 100644 --- a/packages/integrations/netlify/test/edge-functions/dynamic-import.test.js +++ b/packages/integrations/netlify/test/edge-functions/dynamic-import.test.ts @@ -1,10 +1,12 @@ -import { runBuild, runApp } from './test-utils.ts'; +import { loadFixture } from './test-utils.ts'; import { assertEquals, assert, DOMParser } from './deps.ts'; Deno.test({ name: 'Dynamic imports', + permissions: 'inherit', async fn() { - await runBuild('./fixtures/dynimport/'); + const { runApp, runBuild } = await loadFixture('./fixtures/dynimport/'); + await runBuild(); const stop = await runApp('./fixtures/dynimport/prod.js'); try { @@ -14,8 +16,10 @@ Deno.test({ assert(html, 'got some html'); const doc = new DOMParser().parseFromString(html, `text/html`); - const div = doc.querySelector('#thing'); - assert(div, 'div exists'); + if (doc) { + const div = doc.querySelector('#thing'); + assert(div, 'div exists'); + } } catch (err) { console.error(err); } finally { diff --git a/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts b/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts index 9f2a7bde3ac74..699ab0014a644 100644 --- a/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts +++ b/packages/integrations/netlify/test/edge-functions/edge-basic.test.ts @@ -3,30 +3,34 @@ import { assertEquals, assert, DOMParser } from './deps.ts'; Deno.env.set('SECRET_STUFF', 'secret'); -// @ts-expect-error Deno.test({ - // TODO: debug why build cannot be found in "await import" ignore: true, name: 'Edge Basics', - skip: true, - async fn() { + permissions: 'inherit', + async fn(t) { const fixture = loadFixture('./fixtures/edge-basic/'); - await fixture.runBuild(); - const { default: handler } = await import( - './fixtures/edge-basic/.netlify/edge-functions/entry.js' - ); - const response = await handler(new Request('http://example.com/')); - assertEquals(response.status, 200); - const html = await response.text(); - assert(html, 'got some html'); + await t.step('Run the build', async () => { + await fixture.runBuild(); + }); + await t.step('Should correctly render the response', async () => { + const { default: handler } = await import( + './fixtures/edge-basic/.netlify/edge-functions/entry.js' + ); + const response = await handler(new Request('http://example.com/')); + assertEquals(response.status, 200); + const html = await response.text(); + assert(html, 'got some html'); - const doc = new DOMParser().parseFromString(html, `text/html`)!; - const div = doc.querySelector('#react'); - assert(div, 'div exists'); + const doc = new DOMParser().parseFromString(html, `text/html`)!; + const div = doc.querySelector('#react'); + assert(div, 'div exists'); - const envDiv = doc.querySelector('#env'); - assertEquals(envDiv?.innerText, 'secret'); + const envDiv = doc.querySelector('#env'); + assertEquals(envDiv?.innerText, 'secret'); + }); - await fixture.cleanup(); + await t.step('Clean up', async () => { + await fixture.cleanup(); + }); }, }); diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs index 310088c88618a..ac15ad4e9c497 100644 --- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs +++ b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/astro.config.mjs @@ -1,6 +1,5 @@ import { defineConfig } from 'astro/config'; import { netlifyEdgeFunctions } from '@astrojs/netlify'; -import react from "@astrojs/react"; // test env var process.env.SECRET_STUFF = 'secret' @@ -9,6 +8,5 @@ export default defineConfig({ adapter: netlifyEdgeFunctions({ dist: new URL('./dist/', import.meta.url), }), - integrations: [react()], output: 'server', }) diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json index e84f1d5f371a4..16ff300883d2f 100644 --- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json +++ b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/package.json @@ -5,8 +5,6 @@ "dependencies": { "astro": "workspace:*", "@astrojs/react": "workspace:*", - "@astrojs/netlify": "workspace:*", - "react": "^18.1.0", - "react-dom": "^18.1.0" + "@astrojs/netlify": "workspace:*" } } diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx deleted file mode 100644 index c6cf39aa55a87..0000000000000 --- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/components/React.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -export default function() { - return ( -
testing
- ) -} diff --git a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro index 1247ba8f6403d..a480cf46ce552 100644 --- a/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro +++ b/packages/integrations/netlify/test/edge-functions/fixtures/edge-basic/src/pages/index.astro @@ -1,6 +1,3 @@ ---- -import ReactComponent from '../components/React.jsx'; ---- Testing @@ -9,7 +6,6 @@ import ReactComponent from '../components/React.jsx'; -
{import.meta.env.SECRET_STUFF}
diff --git a/packages/integrations/netlify/test/edge-functions/prerender.test.ts b/packages/integrations/netlify/test/edge-functions/prerender.test.ts index 4d4dfc9c6914c..2c066b9b87640 100644 --- a/packages/integrations/netlify/test/edge-functions/prerender.test.ts +++ b/packages/integrations/netlify/test/edge-functions/prerender.test.ts @@ -3,12 +3,16 @@ import { assertEquals, assertExists, cheerio, fs } from './deps.ts'; Deno.test({ name: 'Prerender', + permissions: 'inherit', async fn(t) { const environmentVariables = { PRERENDER: 'true', }; - const fixture = loadFixture('./fixtures/prerender/', environmentVariables); - await fixture.runBuild(); + const { runBuild, cleanup } = loadFixture('./fixtures/prerender/', environmentVariables); + + await t.step('Run the build', async () => { + await runBuild(); + }); await t.step('Handler can process requests to non-existing routes', async () => { const { default: handler } = await import( @@ -16,7 +20,7 @@ Deno.test({ ); assertExists(handler); const response = await handler(new Request('http://example.com/index.html')); - assertEquals(response, undefined, "No response because this route doesn't exist"); + assertEquals(response.status, 404, "No response because this route doesn't exist"); }); await t.step('Prerendered route exists', async () => { @@ -31,22 +35,28 @@ Deno.test({ }); Deno.env.delete('PRERENDER'); - await fixture.cleanup(); + await cleanup(); }, }); Deno.test({ name: 'Hybrid rendering', + permissions: 'inherit', async fn(t) { const environmentVariables = { PRERENDER: 'false', }; const fixture = loadFixture('./fixtures/prerender/', environmentVariables); - await fixture.runBuild(); + await t.step('Run the build', async () => { + await fixture.runBuild(); + }); const stop = await fixture.runApp('./fixtures/prerender/prod.js'); await t.step('Can fetch server route', async () => { - const response = await fetch('http://127.0.0.1:8085/'); + const { default: handler } = await import( + './fixtures/prerender/.netlify/edge-functions/entry.js' + ); + const response = await handler(new Request('http://example.com/')); assertEquals(response.status, 200); const html = await response.text(); @@ -60,7 +70,7 @@ Deno.test({ './fixtures/prerender/.netlify/edge-functions/entry.js' ); const response = await handler(new Request('http://example.com/index.html')); - assertEquals(response, undefined, "No response because this route doesn't exist"); + assertEquals(response.status, 404, "No response because this route doesn't exist"); }); await t.step('Has no prerendered route', async () => { diff --git a/packages/integrations/netlify/test/functions/edge-middleware.test.js b/packages/integrations/netlify/test/functions/edge-middleware.test.js new file mode 100644 index 0000000000000..219fd1ced80c4 --- /dev/null +++ b/packages/integrations/netlify/test/functions/edge-middleware.test.js @@ -0,0 +1,43 @@ +import netlifyAdapter from '../../dist/index.js'; +import { testIntegration, loadFixture } from './test-utils.js'; +import { expect } from 'chai'; + +describe('Middleware', () => { + it('with edge handle file, should successfully build the middleware', async () => { + /** @type {import('./test-utils').Fixture} */ + const fixture = await loadFixture({ + root: new URL('./fixtures/middleware-with-handler-file/', import.meta.url).toString(), + output: 'server', + adapter: netlifyAdapter({ + dist: new URL('./fixtures/middleware-with-handler-file/dist/', import.meta.url), + }), + site: `http://example.com`, + integrations: [testIntegration()], + build: { + excludeMiddleware: true, + }, + }); + await fixture.build(); + const contents = await fixture.readFile('../.netlify/edge-functions/edgeMiddleware.js'); + expect(contents.includes('"Hello world"')).to.be.true; + }); + + it('without edge handle file, should successfully build the middleware', async () => { + /** @type {import('./test-utils').Fixture} */ + const fixture = await loadFixture({ + root: new URL('./fixtures/middleware-without-handler-file/', import.meta.url).toString(), + output: 'server', + adapter: netlifyAdapter({ + dist: new URL('./fixtures/middleware-without-handler-file/dist/', import.meta.url), + }), + site: `http://example.com`, + integrations: [testIntegration()], + build: { + excludeMiddleware: true, + }, + }); + await fixture.build(); + const contents = await fixture.readFile('../.netlify/edge-functions/edgeMiddleware.js'); + expect(contents.includes('"Hello world"')).to.be.false; + }); +}); diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts new file mode 100644 index 0000000000000..8cab418c1c15d --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/middleware.ts @@ -0,0 +1,5 @@ +export const onRequest = (context, next) => { + context.locals.title = 'Middleware'; + + return next(); +}; diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js new file mode 100644 index 0000000000000..bf69edb3e8bfa --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/netlify-edge-middleware.js @@ -0,0 +1,5 @@ +export default function ({ request, context }) { + return { + title: 'Hello world', + }; +} diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro new file mode 100644 index 0000000000000..d97f7069897a9 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware-with-handler-file/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +const title = Astro.locals.title; +--- + + + + {title} + + +

{title}

+ + diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts new file mode 100644 index 0000000000000..8cab418c1c15d --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/middleware.ts @@ -0,0 +1,5 @@ +export const onRequest = (context, next) => { + context.locals.title = 'Middleware'; + + return next(); +}; diff --git a/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro new file mode 100644 index 0000000000000..d97f7069897a9 --- /dev/null +++ b/packages/integrations/netlify/test/functions/fixtures/middleware-without-handler-file/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +const title = Astro.locals.title; +--- + + + + {title} + + +

{title}

+ + diff --git a/packages/integrations/netlify/tsconfig.json b/packages/integrations/netlify/tsconfig.json index 9f96de7cd0ed0..4442d4c3670a1 100644 --- a/packages/integrations/netlify/tsconfig.json +++ b/packages/integrations/netlify/tsconfig.json @@ -6,6 +6,7 @@ "module": "ES2022", "outDir": "./dist", "target": "ES2021", - "typeRoots": ["node_modules/@types", "node_modules/@netlify"] + "typeRoots": ["node_modules/@types", "node_modules/@netlify"], + "allowImportingTsExtensions": true } } diff --git a/packages/integrations/sitemap/CHANGELOG.md b/packages/integrations/sitemap/CHANGELOG.md index 32ba79a61a3ba..66a811d431f00 100644 --- a/packages/integrations/sitemap/CHANGELOG.md +++ b/packages/integrations/sitemap/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/sitemap +## 1.4.0 + +### Minor Changes + +- [#7655](https://github.com/withastro/astro/pull/7655) [`c258492b7`](https://github.com/withastro/astro/commit/c258492b7218cc7e5b7be38f48ec1bb1296292d5) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Ensure sitemap only excludes numerical pages matching `/404` and `/500` exactly + ## 1.3.3 ### Patch Changes diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json index e347f30c28c73..1bab2f7411a0b 100644 --- a/packages/integrations/sitemap/package.json +++ b/packages/integrations/sitemap/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/sitemap", "description": "Generate a sitemap for your Astro site", - "version": "1.3.3", + "version": "1.4.0", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/sitemap/src/generate-sitemap.ts b/packages/integrations/sitemap/src/generate-sitemap.ts index 03985f08d0e2d..b10771ce481af 100644 --- a/packages/integrations/sitemap/src/generate-sitemap.ts +++ b/packages/integrations/sitemap/src/generate-sitemap.ts @@ -2,13 +2,11 @@ import type { EnumChangefreq } from 'sitemap'; import type { SitemapItem, SitemapOptions } from './index.js'; import { parseUrl } from './utils/parse-url.js'; -const STATUS_CODE_PAGE_REGEXP = /\/[0-9]{3}\/?$/; - /** Construct sitemap.xml given a set of URLs */ export function generateSitemap(pages: string[], finalSiteUrl: string, opts: SitemapOptions) { const { changefreq, priority, lastmod: lastmodSrc, i18n } = opts!; // TODO: find way to respect URLs here - const urls = [...pages].filter((url) => !STATUS_CODE_PAGE_REGEXP.test(url)); + const urls = [...pages]; urls.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time const lastmod = lastmodSrc?.toISOString(); diff --git a/packages/integrations/sitemap/src/index.ts b/packages/integrations/sitemap/src/index.ts index b5ff67bd6bd9f..f22384b69e92f 100644 --- a/packages/integrations/sitemap/src/index.ts +++ b/packages/integrations/sitemap/src/index.ts @@ -49,6 +49,7 @@ function formatConfigErrorMessage(err: ZodError) { const PKG_NAME = '@astrojs/sitemap'; const OUTFILE = 'sitemap-index.xml'; +const STATUS_CODE_PAGES = new Set(['/404', '/500']); const createPlugin = (options?: SitemapOptions): AstroIntegration => { let config: AstroConfig; @@ -85,12 +86,14 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => { return; } - let pageUrls = pages.map((p) => { - if (p.pathname !== '' && !finalSiteUrl.pathname.endsWith('/')) - finalSiteUrl.pathname += '/'; - const path = finalSiteUrl.pathname + p.pathname; - return new URL(path, finalSiteUrl).href; - }); + let pageUrls = pages + .filter((p) => !STATUS_CODE_PAGES.has('/' + p.pathname.slice(0, -1))) + .map((p) => { + if (p.pathname !== '' && !finalSiteUrl.pathname.endsWith('/')) + finalSiteUrl.pathname += '/'; + const path = finalSiteUrl.pathname + p.pathname; + return new URL(path, finalSiteUrl).href; + }); let routeUrls = routes.reduce((urls, r) => { // Only expose pages, not endpoints or redirects @@ -100,6 +103,7 @@ const createPlugin = (options?: SitemapOptions): AstroIntegration => { * Dynamic URLs have entries with `undefined` pathnames */ if (r.pathname) { + if (STATUS_CODE_PAGES.has(r.pathname)) return urls; /** * remove the initial slash from relative pathname * because `finalSiteUrl` always has trailing slash diff --git a/packages/integrations/sitemap/test/filter.test.js b/packages/integrations/sitemap/test/filter.test.js index 50a34007dd5d4..b2623248170a6 100644 --- a/packages/integrations/sitemap/test/filter.test.js +++ b/packages/integrations/sitemap/test/filter.test.js @@ -12,7 +12,7 @@ describe('Filter support', () => { root: './fixtures/static/', integrations: [ sitemap({ - filter: (page) => page !== 'http://example.com/two/', + filter: (page) => page === 'http://example.com/one/', }), ], }); @@ -32,7 +32,7 @@ describe('Filter support', () => { root: './fixtures/ssr/', integrations: [ sitemap({ - filter: (page) => page !== 'http://example.com/two/', + filter: (page) => page === 'http://example.com/one/', }), ], }); diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/123.astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/123.astro new file mode 100644 index 0000000000000..115292de96e0b --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/123.astro @@ -0,0 +1,8 @@ + + + 123 + + +

123

+ + diff --git a/packages/integrations/sitemap/test/fixtures/static/src/pages/404.astro b/packages/integrations/sitemap/test/fixtures/static/src/pages/404.astro new file mode 100644 index 0000000000000..9e307c5c292c6 --- /dev/null +++ b/packages/integrations/sitemap/test/fixtures/static/src/pages/404.astro @@ -0,0 +1,8 @@ + + + 404 + + +

404

+ + diff --git a/packages/integrations/sitemap/test/staticPaths.test.js b/packages/integrations/sitemap/test/staticPaths.test.js index bb818e7cd0fd9..3365ff1e84292 100644 --- a/packages/integrations/sitemap/test/staticPaths.test.js +++ b/packages/integrations/sitemap/test/staticPaths.test.js @@ -4,19 +4,29 @@ import { expect } from 'chai'; describe('getStaticPaths support', () => { /** @type {import('./test-utils.js').Fixture} */ let fixture; + /** @type {string[]} */ + let urls; before(async () => { fixture = await loadFixture({ root: './fixtures/static/', }); await fixture.build(); - }); - it('getStaticPath pages require zero config', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); - const urls = data.urlset.url; + urls = data.urlset.url.map((url) => url.loc[0]); + }); + + it('requires zero config for getStaticPaths', async () => { + expect(urls).to.include('http://example.com/one/'); + expect(urls).to.include('http://example.com/two/'); + }); + + it('does not include 404 pages', () => { + expect(urls).to.not.include('http://example.com/404/'); + }); - expect(urls[0].loc[0]).to.equal('http://example.com/one/'); - expect(urls[1].loc[0]).to.equal('http://example.com/two/'); + it('includes numerical pages', () => { + expect(urls).to.include('http://example.com/123/'); }); }); diff --git a/packages/integrations/vercel/CHANGELOG.md b/packages/integrations/vercel/CHANGELOG.md index 1dcfa51f340de..20974cc61f1e4 100644 --- a/packages/integrations/vercel/CHANGELOG.md +++ b/packages/integrations/vercel/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/vercel +## 3.7.2 + +### Patch Changes + +- [#7659](https://github.com/withastro/astro/pull/7659) [`57a5eff5c`](https://github.com/withastro/astro/commit/57a5eff5cee9852dca1e328e233949581edc5fb9) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Fix critical build regression. `@vercel/nft` is excluded from the bundle automatically. + ## 3.7.1 ### Patch Changes diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 2d287d2874c05..fb9a242cd0e0a 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/vercel", "description": "Deploy your site to Vercel", - "version": "3.7.1", + "version": "3.7.2", "type": "module", "author": "withastro", "license": "MIT", diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 3c2d39116e95c..5af00dfce0047 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -66,6 +66,9 @@ export default function vercelEdge({ }, vite: { define: viteDefine, + ssr: { + external: ['@vercel/nft'], + }, }, ...getImageConfig(imageService, imagesConfig, command), }); diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 79e0a91ae235f..592f3618ff6d3 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -47,6 +47,8 @@ export default function vercelServerless({ let buildTempFolder: URL; let serverEntry: string; let _entryPoints: Map; + // Extra files to be merged with `includeFiles` during build + const extraFilesToInclude: URL[] = []; async function createFunctionFolder(funcName: string, entry: URL, inc: URL[]) { const functionFolder = new URL(`./functions/${funcName}.func/`, _config.outDir); @@ -74,8 +76,6 @@ export default function vercelServerless({ }); } - const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || []; - return { name: PACKAGE_NAME, hooks: { @@ -94,6 +94,9 @@ export default function vercelServerless({ }, vite: { define: viteDefine, + ssr: { + external: ['@vercel/nft'], + }, }, ...getImageConfig(imageService, imagesConfig, command), }); @@ -127,7 +130,7 @@ export default function vercelServerless({ vercelEdgeMiddlewareHandlerPath ); // let's tell the adapter that we need to save this file - filesToInclude.push(bundledMiddlewarePath); + extraFilesToInclude.push(bundledMiddlewarePath); } }, @@ -137,7 +140,7 @@ export default function vercelServerless({ const mergeGlobbedIncludes = (globPattern: unknown) => { if (typeof globPattern === 'string') { const entries = glob.sync(globPattern).map((p) => pathToFileURL(p)); - filesToInclude.push(...entries); + extraFilesToInclude.push(...entries); } else if (Array.isArray(globPattern)) { for (const pattern of globPattern) { mergeGlobbedIncludes(pattern); @@ -149,6 +152,8 @@ export default function vercelServerless({ } const routeDefinitions: { src: string; dest: string }[] = []; + const filesToInclude = includeFiles?.map((file) => new URL(file, _config.root)) || []; + filesToInclude.push(...extraFilesToInclude); // Multiple entrypoint support if (_entryPoints.size) { diff --git a/packages/integrations/vercel/test/fixtures/serverless-prerender/astro.config.mjs b/packages/integrations/vercel/test/fixtures/serverless-prerender/astro.config.mjs index 03228c5df506e..84b96c27d409d 100644 --- a/packages/integrations/vercel/test/fixtures/serverless-prerender/astro.config.mjs +++ b/packages/integrations/vercel/test/fixtures/serverless-prerender/astro.config.mjs @@ -2,6 +2,9 @@ import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel/serverless'; export default defineConfig({ - adapter: vercel(), + adapter: vercel({ + // Pass some value to make sure it doesn't error out + includeFiles: ['included.js'] + }), output: 'server' }); diff --git a/packages/integrations/vercel/test/fixtures/serverless-prerender/included.js b/packages/integrations/vercel/test/fixtures/serverless-prerender/included.js new file mode 100644 index 0000000000000..4e64b2d616bf1 --- /dev/null +++ b/packages/integrations/vercel/test/fixtures/serverless-prerender/included.js @@ -0,0 +1 @@ +'works' \ No newline at end of file diff --git a/packages/integrations/vercel/test/serverless-prerender.test.js b/packages/integrations/vercel/test/serverless-prerender.test.js index 18b16534047a3..292e92b69d307 100644 --- a/packages/integrations/vercel/test/serverless-prerender.test.js +++ b/packages/integrations/vercel/test/serverless-prerender.test.js @@ -10,12 +10,20 @@ describe('Serverless prerender', () => { fixture = await loadFixture({ root: './fixtures/serverless-prerender/', }); + await fixture.build(); }); it('build successful', async () => { - await fixture.build(); expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok; }); + + it('includeFiles work', async () => { + expect( + await fixture.readFile( + '../.vercel/output/functions/render.func/packages/integrations/vercel/test/fixtures/serverless-prerender/included.js' + ) + ).to.be.ok; + }); }); describe('Serverless hybrid rendering', () => { @@ -28,10 +36,10 @@ describe('Serverless hybrid rendering', () => { root: './fixtures/serverless-prerender/', output: 'hybrid', }); + await fixture.build(); }); it('build successful', async () => { - await fixture.build(); expect(await fixture.readFile('../.vercel/output/static/index.html')).to.be.ok; }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d974b2de5624..4e9db8a0f6ce6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,7 +139,7 @@ importers: specifier: ^2.4.3 version: link:../../packages/astro-rss '@astrojs/sitemap': - specifier: ^1.3.3 + specifier: ^1.4.0 version: link:../../packages/integrations/sitemap astro: specifier: ^2.8.3 @@ -4464,12 +4464,6 @@ importers: astro: specifier: workspace:* version: link:../../../../../../astro - react: - specifier: ^18.1.0 - version: 18.2.0 - react-dom: - specifier: ^18.1.0 - version: 18.2.0(react@18.2.0) packages/integrations/netlify/test/edge-functions/fixtures/prerender: dependencies: