0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-30 22:03:56 -05:00

Merge branch 'main' into main

This commit is contained in:
Ben 2024-12-16 14:19:05 +07:00 committed by GitHub
commit 45f3753179
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
122 changed files with 928 additions and 311 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/db': patch
---
Fixes `isDbError()` guard for `LibsqlError`

View file

@ -1,5 +0,0 @@
---
"astro": patch
---
Updates a reference in an error message

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes an issue where Astro couldn't correctly handle i18n fallback when using the i18n middleware

View file

@ -0,0 +1,5 @@
---
'@astrojs/db': patch
---
Fixes the publishing of the package

View file

@ -0,0 +1,5 @@
---
"astro": patch
---
Adds type support for the `closedby` attribute for `<dialog>` elements

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a bug where Astro couldn't correctly parse `params` and `props` when receiving i18n fallback URLs

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Trailing slash support for actions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a bug that caused errors in dev when editing sites with large numbers of MDX pages

View file

@ -1,5 +0,0 @@
---
'@astrojs/markdown-remark': patch
---
Avoids parsing frontmatter that are not at the top of a file

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
"Added `inert` to htmlBooleanAttributes"

View file

@ -1,5 +0,0 @@
---
"@astrojs/markdown-remark": patch
---
Removes trailing new line in code blocks to prevent generating a trailing empty `<span />` tag

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fixes a case where failing content entries in `astro check` would not be surfaced

View file

@ -1,5 +0,0 @@
---
'@astrojs/markdown-remark': patch
---
Fixes frontmatter parsing if file is encoded in UTF8 with BOM

View file

@ -1,6 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { pathToFileURL } from 'node:url';
import mri from 'mri';
import { makeProject } from './bench/_util.js';

View file

@ -1,4 +1,4 @@
import type { AstroAdapter, AstroIntegration } from 'astro';
import type { AstroIntegration } from 'astro';
export default function createIntegration(): AstroIntegration {
return {

View file

@ -34,6 +34,7 @@
"correctness": {
"noUnusedVariables": "info",
"noUnusedFunctionParameters": "info",
"noUnusedImports": "warn",
},
},
},
@ -87,11 +88,12 @@
},
},
{
"include": ["*.astro", "client.d.ts"],
"include": ["*.astro", "client.d.ts", "jsx-runtime.d.ts"],
"linter": {
"rules": {
"correctness": {
"noUnusedVariables": "off",
"noUnusedImports": "off",
},
},
},

View file

@ -10,6 +10,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.0.1",
"@astrojs/mdx": "^4.0.2",
"@astrojs/rss": "^4.0.10",
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
"astro": "^5.0.3"
"astro": "^5.0.5"
},
"peerDependencies": {
"astro": "^4.0.0 || ^5.0.0"

View file

@ -11,8 +11,8 @@
"test": "vitest run"
},
"dependencies": {
"@astrojs/react": "^4.0.0",
"astro": "^5.0.3",
"@astrojs/react": "^4.1.0",
"astro": "^5.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vitest": "^2.1.6"

View file

@ -13,6 +13,6 @@
"@astrojs/alpinejs": "^0.4.0",
"@types/alpinejs": "^3.13.10",
"alpinejs": "^3.14.3",
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -11,13 +11,13 @@
},
"dependencies": {
"@astrojs/preact": "^4.0.0",
"@astrojs/react": "^4.0.0",
"@astrojs/react": "^4.1.0",
"@astrojs/solid-js": "^5.0.0",
"@astrojs/svelte": "^7.0.1",
"@astrojs/vue": "^5.0.1",
"@astrojs/vue": "^5.0.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.3",
"astro": "^5.0.5",
"preact": "^10.24.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View file

@ -12,7 +12,7 @@
"dependencies": {
"@astrojs/preact": "^4.0.0",
"@preact/signals": "^1.3.0",
"astro": "^5.0.3",
"astro": "^5.0.5",
"preact": "^10.24.3"
}
}

View file

@ -10,10 +10,10 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^4.0.0",
"@astrojs/react": "^4.1.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.3",
"astro": "^5.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}

View file

@ -11,7 +11,7 @@
},
"dependencies": {
"@astrojs/solid-js": "^5.0.0",
"astro": "^5.0.3",
"astro": "^5.0.5",
"solid-js": "^1.9.3"
}
}

View file

@ -11,7 +11,7 @@
},
"dependencies": {
"@astrojs/svelte": "^7.0.1",
"astro": "^5.0.3",
"astro": "^5.0.5",
"svelte": "^5.1.16"
}
}

View file

@ -10,8 +10,8 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/vue": "^5.0.1",
"astro": "^5.0.3",
"@astrojs/vue": "^5.0.2",
"astro": "^5.0.5",
"vue": "^3.5.12"
}
}

View file

@ -11,6 +11,6 @@
},
"dependencies": {
"@astrojs/node": "^9.0.0",
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
"astro": "^5.0.3"
"astro": "^5.0.5"
},
"peerDependencies": {
"astro": "^4.0.0"

View file

@ -10,6 +10,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -10,6 +10,6 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/node": "^9.0.0",
"@astrojs/svelte": "^7.0.1",
"astro": "^5.0.3",
"astro": "^5.0.5",
"svelte": "^5.1.16"
}
}

View file

@ -9,7 +9,7 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.0.3",
"astro": "^5.0.5",
"sass": "^1.80.6",
"sharp": "^0.33.3"
}

View file

@ -15,6 +15,6 @@
"./app": "./dist/app.js"
},
"devDependencies": {
"astro": "^5.0.3"
"astro": "^5.0.5"
}
}

View file

@ -10,7 +10,7 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/markdoc": "^0.12.1",
"astro": "^5.0.3"
"@astrojs/markdoc": "^0.12.3",
"astro": "^5.0.5"
}
}

View file

@ -10,9 +10,9 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.0.1",
"@astrojs/mdx": "^4.0.2",
"@astrojs/preact": "^4.0.0",
"astro": "^5.0.3",
"astro": "^5.0.5",
"preact": "^10.24.3"
}
}

View file

@ -12,7 +12,7 @@
"dependencies": {
"@astrojs/preact": "^4.0.0",
"@nanostores/preact": "^0.5.2",
"astro": "^5.0.3",
"astro": "^5.0.5",
"nanostores": "^0.11.3",
"preact": "^10.24.3"
}

View file

@ -10,10 +10,10 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.0.1",
"@astrojs/mdx": "^4.0.2",
"@astrojs/tailwind": "^5.1.3",
"@types/canvas-confetti": "^1.6.4",
"astro": "^5.0.3",
"astro": "^5.0.5",
"autoprefixer": "^10.4.20",
"canvas-confetti": "^1.9.3",
"postcss": "^8.4.49",

View file

@ -11,7 +11,7 @@
"test": "vitest"
},
"dependencies": {
"astro": "^5.0.3",
"astro": "^5.0.5",
"vitest": "^2.1.6"
}
}

View file

@ -1,5 +1,34 @@
# astro
## 5.0.5
### Patch Changes
- [#12705](https://github.com/withastro/astro/pull/12705) [`0d1eab5`](https://github.com/withastro/astro/commit/0d1eab560d56c51c359bbd35e8bfb51e238611ee) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug where MDX files with certain characters in the name would cause builds to fail
- [#12707](https://github.com/withastro/astro/pull/12707) [`2aaed2d`](https://github.com/withastro/astro/commit/2aaed2d2a96ab35461af24e8d12b20f1da33983f) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug where the middleware was incorrectly imported during the build
- [#12697](https://github.com/withastro/astro/pull/12697) [`1c4a032`](https://github.com/withastro/astro/commit/1c4a032247747c830be94dbdd0c953511a6bfa53) Thanks [@ascorbic](https://github.com/ascorbic)! - Fix a bug that caused builds to fail if an image had a quote mark in its name
- [#12694](https://github.com/withastro/astro/pull/12694) [`495f46b`](https://github.com/withastro/astro/commit/495f46bca78665732e51c629d93a68fa392b88a4) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug where the experimental feature `experimental.svg` was incorrectly used when generating ESM images
- [#12658](https://github.com/withastro/astro/pull/12658) [`3169593`](https://github.com/withastro/astro/commit/316959355c3d59723ecb3e0f417becf1f03ddd74) Thanks [@jurajkapsz](https://github.com/jurajkapsz)! - Fixes astro info copy to clipboard process not returning to prompt in certain cases.
- [#12712](https://github.com/withastro/astro/pull/12712) [`b01c74a`](https://github.com/withastro/astro/commit/b01c74aeccc4ec76b64fa75d163df58274b37970) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug which misidentified pages as markdown if a query string ended in a markdown extension
## 5.0.4
### Patch Changes
- [#12653](https://github.com/withastro/astro/pull/12653) [`e21c7e6`](https://github.com/withastro/astro/commit/e21c7e67fde1155cf593fd2b40010c5e2c2cd3f2) Thanks [@sarah11918](https://github.com/sarah11918)! - Updates a reference in an error message
- [#12585](https://github.com/withastro/astro/pull/12585) [`a9373c0`](https://github.com/withastro/astro/commit/a9373c0c9a3c2e1773fc11bb14e156698b0d9d38) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Fixes a case where `process.env` would be frozen despite changes made to environment variables in development
- [#12695](https://github.com/withastro/astro/pull/12695) [`a203d5d`](https://github.com/withastro/astro/commit/a203d5dd582166674c45e807a5dc9113e26e24f0) Thanks [@ascorbic](https://github.com/ascorbic)! - Throws a more helpful error when images are missing
- Updated dependencies [[`f13417b`](https://github.com/withastro/astro/commit/f13417bfbf73130c224752379e2da33084f89554), [`87231b1`](https://github.com/withastro/astro/commit/87231b1168da66bb593f681206c42fa555dfcabc), [`a71e9b9`](https://github.com/withastro/astro/commit/a71e9b93b317edc0ded49d4d50f1b7841c8cd428)]:
- @astrojs/markdown-remark@6.0.1
## 5.0.3
### Patch Changes

View file

@ -679,6 +679,7 @@ declare namespace astroHTML.JSX {
interface DialogHTMLAttributes extends HTMLAttributes {
open?: boolean | string | undefined | null;
closedby?: 'none' | 'closerequest' | 'any' | undefined | null;
}
interface EmbedHTMLAttributes extends HTMLAttributes {

View file

@ -1,6 +1,6 @@
{
"name": "astro",
"version": "5.0.3",
"version": "5.0.5",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",

View file

@ -1,5 +1,6 @@
import type fsMod from 'node:fs';
import type { Plugin as VitePlugin } from 'vite';
import { shouldAppendForwardSlash } from '../core/build/util.js';
import type { AstroSettings } from '../types/astro.js';
import {
NOOP_ACTIONS,
@ -84,6 +85,12 @@ export function vitePluginActions({
code += `\nexport * from 'astro/actions/runtime/virtual/server.js';`;
} else {
code += `\nexport * from 'astro/actions/runtime/virtual/client.js';`;
code = code.replace(
"'/** @TRAILING_SLASH@ **/'",
JSON.stringify(
shouldAppendForwardSlash(settings.config.trailingSlash, settings.config.build.format),
),
);
}
return code;
},

View file

@ -1,6 +1,10 @@
import { z } from 'zod';
import type { Pipeline } from '../../../core/base-pipeline.js';
import { shouldAppendForwardSlash } from '../../../core/build/util.js';
import { ActionCalledFromServerError } from '../../../core/errors/errors-data.js';
import { AstroError } from '../../../core/errors/errors.js';
import { removeTrailingForwardSlash } from '../../../core/path.js';
import { apiContextRoutesSymbol } from '../../../core/render-context.js';
import type { APIContext } from '../../../types/public/index.js';
import { ACTION_RPC_ROUTE_PATTERN } from '../../consts.js';
import {
@ -279,7 +283,15 @@ export function getActionContext(context: APIContext): ActionMiddlewareContext {
calledFrom: callerInfo.from,
name: callerInfo.name,
handler: async () => {
const baseAction = await getAction(callerInfo.name);
const pipeline: Pipeline = Reflect.get(context, apiContextRoutesSymbol);
const callerInfoName = shouldAppendForwardSlash(
pipeline.manifest.trailingSlash,
pipeline.manifest.buildFormat,
)
? removeTrailingForwardSlash(callerInfo.name)
: callerInfo.name;
const baseAction = await getAction(callerInfoName);
let input;
try {
input = await parseRequestBody(context.request);

View file

@ -3,6 +3,7 @@ import type { z } from 'zod';
import { REDIRECT_STATUS_CODES } from '../../../core/constants.js';
import { ActionsReturnedInvalidDataError } from '../../../core/errors/errors-data.js';
import { AstroError } from '../../../core/errors/errors.js';
import { appendForwardSlash as _appendForwardSlash } from '../../../core/path.js';
import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from '../../consts.js';
import type {
ErrorInferenceObject,
@ -13,6 +14,8 @@ import type {
export type ActionAPIContext = _ActionAPIContext;
export const ACTION_QUERY_PARAMS = _ACTION_QUERY_PARAMS;
export const appendForwardSlash = _appendForwardSlash;
export const ACTION_ERROR_CODES = [
'BAD_REQUEST',
'UNAUTHORIZED',

View file

@ -1,5 +1,4 @@
import { toStyleString } from '../../runtime/server/render/util.js';
import type { AstroConfig } from '../../types/public/config.js';
import type { GetImageResult, ImageLayout, LocalImageProps, RemoteImageProps } from '../types.js';
export function addCSSVarsToStyle(

View file

@ -15,6 +15,7 @@ export async function emitESMImage(
_watchMode: boolean,
// FIX: in Astro 6, this function should not be passed in dev mode at all.
// Or rethink the API so that a function that throws isn't passed through.
experimentalSvgEnabled: boolean,
fileEmitter?: FileEmitter,
): Promise<ImageMetadataWithContents | undefined> {
if (!id) {
@ -44,7 +45,8 @@ export async function emitESMImage(
});
// Attach file data for SVGs
if (fileMetadata.format === 'svg') {
// TODO: this is a workaround to prevent a memory leak, and it must be fixed before we remove the experimental flag, see
if (fileMetadata.format === 'svg' && experimentalSvgEnabled === true) {
emittedImage.contents = fileData;
}

View file

@ -205,7 +205,12 @@ export default function assets({ settings }: { settings: AstroSettings }): vite.
}
const emitFile = shouldEmitFile ? this.emitFile : undefined;
const imageMetadata = await emitESMImage(id, this.meta.watchMode, emitFile);
const imageMetadata = await emitESMImage(
id,
this.meta.watchMode,
!!settings.config.experimental.svg,
emitFile,
);
if (!imageMetadata) {
throw new AstroError({

View file

@ -31,11 +31,7 @@ export async function check(flags: Flags) {
// NOTE: In the future, `@astrojs/check` can expose a `before lint` hook so that this works during `astro check --watch` too.
// For now, we run this once as usually `astro check --watch` is ran alongside `astro dev` which also calls `astro sync`.
const { default: sync } = await import('../../core/sync/index.js');
try {
await sync(flagsToAstroInlineConfig(flags));
} catch (_) {
return process.exit(1);
}
}
const { check: checker, parseArgsAsCheckConfig } = checkPackage;

View file

@ -66,7 +66,7 @@ export async function copyToClipboard(text: string, force?: boolean) {
// Unix: check if a supported command is installed
const unixCommands: Array<[string, Array<string>]> = [
['xclip', ['-sel', 'clipboard', '-l', '1']],
['xclip', ['-selection', 'clipboard', '-l', '1']],
['wl-copy', []],
];
for (const [unixCommand, unixArgs] of unixCommands) {
@ -101,7 +101,7 @@ export async function copyToClipboard(text: string, force?: boolean) {
}
try {
const result = spawnSync(command, args, { input: text });
const result = spawnSync(command, args, { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
if (result.error) {
throw result.error;
}

View file

@ -155,7 +155,6 @@ function createManifest(
i18n: manifest?.i18n,
checkOrigin: false,
middleware: manifest?.middleware ?? middlewareInstance,
envGetSecretEnabled: false,
key: createKey(),
};
}

View file

@ -206,6 +206,7 @@ export class ContentLayer {
},
collectionWithResolvedSchema,
false,
!!this.#settings.config.experimental.svg,
);
return parsedData;

View file

@ -9,6 +9,8 @@ import { contentModuleToId } from './utils.js';
const SAVE_DEBOUNCE_MS = 500;
const MAX_DEPTH = 10;
/**
* Extends the DataStore with the ability to change entries and write them to disk.
* This is kept as a separate class to avoid needing node builtins at runtime, when read-only access is all that is needed.
@ -86,7 +88,7 @@ export class MutableDataStore extends ImmutableDataStore {
if (this.#assetImports.size === 0) {
try {
await fs.writeFile(filePath, 'export default new Map();');
await this.#writeFileAtomic(filePath, 'export default new Map();');
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@ -102,7 +104,7 @@ export class MutableDataStore extends ImmutableDataStore {
const exports: Array<string> = [];
this.#assetImports.forEach((id) => {
const symbol = importIdToSymbolName(id);
imports.push(`import ${symbol} from '${id}';`);
imports.push(`import ${symbol} from ${JSON.stringify(id)};`);
exports.push(`[${JSON.stringify(id)}, ${symbol}]`);
});
const code = /* js */ `
@ -110,7 +112,7 @@ ${imports.join('\n')}
export default new Map([${exports.join(', ')}]);
`;
try {
await fs.writeFile(filePath, code);
await this.#writeFileAtomic(filePath, code);
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@ -122,7 +124,7 @@ export default new Map([${exports.join(', ')}]);
if (this.#moduleImports.size === 0) {
try {
await fs.writeFile(filePath, 'export default new Map();');
await this.#writeFileAtomic(filePath, 'export default new Map();');
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@ -137,13 +139,13 @@ export default new Map([${exports.join(', ')}]);
// We then export them all, mapped by the import id, so we can find them again in the build.
const lines: Array<string> = [];
for (const [fileName, specifier] of this.#moduleImports) {
lines.push(`['${fileName}', () => import('${specifier}')]`);
lines.push(`[${JSON.stringify(fileName)}, () => import(${JSON.stringify(specifier)})]`);
}
const code = `
export default new Map([\n${lines.join(',\n')}]);
`;
try {
await fs.writeFile(filePath, code);
await this.#writeFileAtomic(filePath, code);
} catch (err) {
throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause: err });
}
@ -190,6 +192,42 @@ export default new Map([\n${lines.join(',\n')}]);
}
}
#writing = new Set<string>();
#pending = new Set<string>();
async #writeFileAtomic(filePath: PathLike, data: string, depth = 0) {
if (depth > MAX_DEPTH) {
// If we hit the max depth, we skip a write to prevent the stack from growing too large
// In theory this means we may miss the latest data, but in practice this will only happen when the file is being written to very frequently
// so it will be saved on the next write. This is unlikely to ever happen in practice, as the writes are debounced. It requires lots of writes to very large files.
return;
}
const fileKey = filePath.toString();
// If we are already writing this file, instead of writing now, flag it as pending and write it when we're done.
if (this.#writing.has(fileKey)) {
this.#pending.add(fileKey);
return;
}
// Prevent concurrent writes to this file by flagging it as being written
this.#writing.add(fileKey);
const tempFile = filePath instanceof URL ? new URL(`${filePath.href}.tmp`) : `${filePath}.tmp`;
try {
// Write it to a temporary file first and then move it to prevent partial reads.
await fs.writeFile(tempFile, data);
await fs.rename(tempFile, filePath);
} finally {
// We're done writing. Unflag the file and check if there are any pending writes for this file.
this.#writing.delete(fileKey);
// If there are pending writes, we need to write again to ensure we flush the latest data.
if (this.#pending.has(fileKey)) {
this.#pending.delete(fileKey);
// Call ourself recursively to write the file again
await this.#writeFileAtomic(filePath, data, depth + 1);
}
}
}
scopedStore(collectionName: string): DataStore {
return {
get: <TData extends Record<string, unknown> = Record<string, unknown>>(key: string) =>
@ -298,7 +336,7 @@ export default new Map([\n${lines.join(',\n')}]);
return;
}
try {
await fs.writeFile(filePath, this.toString());
await this.#writeFileAtomic(filePath, this.toString());
this.#file = filePath;
this.#dirty = false;
} catch (err) {

View file

@ -7,6 +7,7 @@ export function createImage(
pluginContext: PluginContext,
shouldEmitFile: boolean,
entryFilePath: string,
experimentalSvgEnabled: boolean,
) {
return () => {
return z.string().transform(async (imagePath, ctx) => {
@ -14,6 +15,7 @@ export function createImage(
const metadata = (await emitESMImage(
resolvedFilePath,
pluginContext.meta.watchMode,
experimentalSvgEnabled,
shouldEmitFile ? pluginContext.emitFile : undefined,
)) as OmitBrand<ImageMetadata>;

View file

@ -164,6 +164,7 @@ export async function getEntryDataAndImages<
},
collectionConfig: CollectionConfig,
shouldEmitFile: boolean,
experimentalSvgEnabled: boolean,
pluginContext?: PluginContext,
): Promise<{ data: TOutputData; imageImports: Array<string> }> {
let data: TOutputData;
@ -182,7 +183,12 @@ export async function getEntryDataAndImages<
if (typeof schema === 'function') {
if (pluginContext) {
schema = schema({
image: createImage(pluginContext, shouldEmitFile, entry._internal.filePath),
image: createImage(
pluginContext,
shouldEmitFile,
entry._internal.filePath,
experimentalSvgEnabled,
),
});
} else if (collectionConfig.type === CONTENT_LAYER_TYPE) {
schema = schema({
@ -257,12 +263,14 @@ export async function getEntryData(
},
collectionConfig: CollectionConfig,
shouldEmitFile: boolean,
experimentalSvgEnabled: boolean,
pluginContext?: PluginContext,
) {
const { data } = await getEntryDataAndImages(
entry,
collectionConfig,
shouldEmitFile,
experimentalSvgEnabled,
pluginContext,
);
return data;

View file

@ -39,7 +39,7 @@ export function astroContentAssetPropagationPlugin({
? fileURLToPath(new URL(importerParam, settings.config.root))
: importer;
const resolved = this.resolve(base, importerPath, { skipSelf: true, ...opts });
const resolved = await this.resolve(base, importerPath, { skipSelf: true, ...opts });
if (!resolved) {
throw new AstroError({
...AstroErrorData.ImageNotFound,

View file

@ -245,6 +245,7 @@ async function getContentEntryModule(
{ id, collection, _internal, unvalidatedData },
collectionConfig,
params.shouldEmitFile,
!!params.config.experimental.svg,
pluginContext,
)
: unvalidatedData;
@ -280,6 +281,7 @@ async function getDataEntryModule(
{ id, collection, _internal, unvalidatedData },
collectionConfig,
params.shouldEmitFile,
!!params.config.experimental.svg,
pluginContext,
)
: unvalidatedData;

View file

@ -5,7 +5,6 @@ import {
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
clientAddressSymbol,
clientLocalsSymbol,
responseSentSymbol,
} from '../constants.js';
import { getSetCookiesFromResponse } from '../cookies/index.js';

View file

@ -69,7 +69,6 @@ export type SSRManifest = {
i18n: SSRManifestI18n | undefined;
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
checkOrigin: boolean;
envGetSecretEnabled: boolean;
};
export type SSRManifestI18n = {

View file

@ -559,6 +559,5 @@ function createBuildManifest(
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
key,
envGetSecretEnabled: false,
};
}

View file

@ -131,11 +131,11 @@ export class BuildPipeline extends Pipeline {
const renderers = await import(renderersEntryUrl.toString());
const middleware = internals.middlewareEntryPoint
? await import(internals.middlewareEntryPoint.toString()).then((mod) => {
return function () {
? async function () {
// @ts-expect-error: the compiler can't understand the previous check
const mod = await import(internals.middlewareEntryPoint.toString());
return { onRequest: mod.onRequest };
};
})
}
: manifest.middleware;
if (!renderers) {

View file

@ -5,7 +5,6 @@ import type { Plugin as VitePlugin } from 'vite';
import { getAssetsPrefix } from '../../../assets/utils/getAssetsPrefix.js';
import { normalizeTheLocale } from '../../../i18n/index.js';
import { toFallbackType, toRoutingStrategy } from '../../../i18n/utils.js';
import { unwrapSupportKind } from '../../../integrations/features-validation.js';
import { runHookBuildSsr } from '../../../integrations/hooks.js';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
import type {
@ -274,8 +273,5 @@ function buildManifest(
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
serverIslandNameMap: Array.from(settings.serverIslandNameMap),
key: encodedKey,
envGetSecretEnabled:
(unwrapSupportKind(settings.adapter?.supportedAstroFeatures.envGetSecret) ??
'unsupported') !== 'unsupported',
};
}

View file

@ -1,5 +1,4 @@
import type { Plugin as VitePlugin } from 'vite';
import type { AstroSettings } from '../../../types/astro.js';
import type { AstroAdapter } from '../../../types/public/integrations.js';
import { routeIsRedirect } from '../../redirects/index.js';
import { VIRTUAL_ISLAND_MAP_ID } from '../../server-islands/vite-plugin-server-islands.js';

View file

@ -12,6 +12,7 @@ import {
astroContentImportPlugin,
astroContentVirtualModPlugin,
} from '../content/index.js';
import { createEnvLoader } from '../env/env-loader.js';
import { astroEnv } from '../env/vite-plugin-env.js';
import astroInternationalization from '../i18n/vite-plugin-i18n.js';
import astroPrefetch from '../prefetch/vite-plugin-prefetch.js';
@ -123,6 +124,7 @@ export async function createVite(
});
const srcDirPattern = glob.convertPathToPattern(fileURLToPath(settings.config.srcDir));
const envLoader = createEnvLoader();
// Start with the Vite configuration that Astro core needs
const commonConfig: vite.InlineConfig = {
@ -146,8 +148,8 @@ export async function createVite(
// The server plugin is for dev only and having it run during the build causes
// the build to run very slow as the filewatcher is triggered often.
command === 'dev' && vitePluginAstroServer({ settings, logger, fs, manifest, ssrManifest }), // ssrManifest is only required in dev mode, where it gets created before a Vite instance is created, and get passed to this function
envVitePlugin({ settings }),
astroEnv({ settings, mode, sync }),
envVitePlugin({ envLoader }),
astroEnv({ settings, mode, sync, envLoader }),
markdownVitePlugin({ settings, logger }),
htmlVitePlugin(),
astroPostprocessVitePlugin(),

View file

@ -47,7 +47,8 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> {
base,
});
// The pathname used here comes from the server, which already encored.
if (!staticPaths.length) return {};
// The pathname used here comes from the server, which already encoded.
// Since we decided to not mess up with encoding anymore, we need to decode them back so the parameters can match
// the ones expected from the users
const params = getParams(route, decodeURI(pathname));
@ -77,7 +78,11 @@ export function getParams(route: RouteData, pathname: string): Params {
if (!route.params.length) return {};
// The RegExp pattern expects a decoded string, but the pathname is encoded
// when the URL contains non-English characters.
const paramsMatch = route.pattern.exec(pathname);
const paramsMatch =
route.pattern.exec(pathname) ||
route.fallbackRoutes
.map((fallbackRoute) => fallbackRoute.pattern.exec(pathname))
.find((x) => x);
if (!paramsMatch) return {};
const params: Params = {};
route.params.forEach((key, i) => {

View file

@ -5,7 +5,7 @@ import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import type { RouteType } from '../types/public/internal.js';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
import { removeTrailingForwardSlash, slash } from './path.js';
import { removeQueryString, removeTrailingForwardSlash, slash } from './path.js';
/** Returns true if argument is an object of any prototype/class (but not null). */
export function isObject(value: unknown): value is Record<string, any> {
@ -18,9 +18,10 @@ export function isURL(value: unknown): value is URL {
}
/** Check if a file is a markdown file based on its extension */
export function isMarkdownFile(fileId: string, option?: { suffix?: string }): boolean {
const id = removeQueryString(fileId);
const _suffix = option?.suffix ?? '';
for (let markdownFileExtension of SUPPORTED_MARKDOWN_FILE_EXTENSIONS) {
if (fileId.endsWith(`${markdownFileExtension}${_suffix}`)) return true;
if (id.endsWith(`${markdownFileExtension}${_suffix}`)) return true;
}
return false;
}

60
packages/astro/src/env/env-loader.ts vendored Normal file
View file

@ -0,0 +1,60 @@
import { fileURLToPath } from 'node:url';
import { loadEnv } from 'vite';
import type { AstroConfig } from '../types/public/index.js';
// Match valid JS variable names (identifiers), which accepts most alphanumeric characters,
// except that the first character cannot be a number.
const isValidIdentifierRe = /^[_$a-zA-Z][\w$]*$/;
function getPrivateEnv(
fullEnv: Record<string, string>,
astroConfig: AstroConfig,
): Record<string, string> {
const viteConfig = astroConfig.vite;
let envPrefixes: string[] = ['PUBLIC_'];
if (viteConfig.envPrefix) {
envPrefixes = Array.isArray(viteConfig.envPrefix)
? viteConfig.envPrefix
: [viteConfig.envPrefix];
}
const privateEnv: Record<string, string> = {};
for (const key in fullEnv) {
// Ignore public env var
if (isValidIdentifierRe.test(key) && envPrefixes.every((prefix) => !key.startsWith(prefix))) {
if (typeof process.env[key] !== 'undefined') {
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') {
privateEnv[key] = value;
} else {
privateEnv[key] = `process.env.${key}`;
}
} else {
privateEnv[key] = JSON.stringify(fullEnv[key]);
}
}
}
return privateEnv;
}
export const createEnvLoader = () => {
let privateEnv: Record<string, string> = {};
return {
load: (mode: string, config: AstroConfig) => {
const loaded = loadEnv(mode, config.vite.envDir ?? fileURLToPath(config.root), '');
privateEnv = getPrivateEnv(loaded, config);
return loaded;
},
getPrivateEnv: () => {
return privateEnv;
},
};
};
export type EnvLoader = ReturnType<typeof createEnvLoader>;

View file

@ -4,14 +4,14 @@ import type { ValidationResultInvalid } from './validators.js';
export { validateEnvVariable, getEnvFieldType } from './validators.js';
export type GetEnv = (key: string) => string | undefined;
type OnSetGetEnv = (reset: boolean) => void;
type OnSetGetEnv = () => void;
let _getEnv: GetEnv = (key) => process.env[key];
export function setGetEnv(fn: GetEnv, reset = false) {
export function setGetEnv(fn: GetEnv) {
_getEnv = fn;
_onSetGetEnv(reset);
_onSetGetEnv();
}
let _onSetGetEnv: OnSetGetEnv = () => {};

View file

@ -1,6 +1,5 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { type Plugin, loadEnv } from 'vite';
import type { Plugin } from 'vite';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import type { AstroSettings } from '../types/astro.js';
import {
@ -8,6 +7,7 @@ import {
VIRTUAL_MODULES_IDS,
VIRTUAL_MODULES_IDS_VALUES,
} from './constants.js';
import type { EnvLoader } from './env-loader.js';
import { type InvalidVariable, invalidVariablesToError } from './errors.js';
import type { EnvSchema } from './schema.js';
import { getEnvFieldType, validateEnvVariable } from './validators.js';
@ -16,23 +16,31 @@ interface AstroEnvPluginParams {
settings: AstroSettings;
mode: string;
sync: boolean;
envLoader: EnvLoader;
}
export function astroEnv({ settings, mode, sync }: AstroEnvPluginParams): Plugin {
export function astroEnv({ settings, mode, sync, envLoader }: AstroEnvPluginParams): Plugin {
const { schema, validateSecrets } = settings.config.env;
let isDev: boolean;
let templates: { client: string; server: string; internal: string } | null = null;
return {
name: 'astro-env-plugin',
enforce: 'pre',
config(_, { command }) {
isDev = command !== 'build';
},
buildStart() {
const loadedEnv = loadEnv(mode, fileURLToPath(settings.config.root), '');
const loadedEnv = envLoader.load(mode, settings.config);
if (!isDev) {
for (const [key, value] of Object.entries(loadedEnv)) {
if (value !== undefined) {
process.env[key] = value;
}
}
}
const validatedVariables = validatePublicVariables({
schema,
@ -42,7 +50,7 @@ export function astroEnv({ settings, mode, sync }: AstroEnvPluginParams): Plugin
});
templates = {
...getTemplates(schema, validatedVariables),
...getTemplates(schema, validatedVariables, isDev ? loadedEnv : null),
internal: `export const schema = ${JSON.stringify(schema)};`,
};
},
@ -122,6 +130,7 @@ function validatePublicVariables({
function getTemplates(
schema: EnvSchema,
validatedVariables: ReturnType<typeof validatePublicVariables>,
loadedEnv: Record<string, string> | null,
) {
let client = '';
let server = readFileSync(MODULE_TEMPLATE_URL, 'utf-8');
@ -142,10 +151,15 @@ function getTemplates(
}
server += `export let ${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
onSetGetEnv += `${key} = reset ? undefined : _internalGetSecret(${JSON.stringify(key)});\n`;
onSetGetEnv += `${key} = _internalGetSecret(${JSON.stringify(key)});\n`;
}
server = server.replace('// @@ON_SET_GET_ENV@@', onSetGetEnv);
if (loadedEnv) {
server = server.replace('// @@GET_ENV@@', `return (${JSON.stringify(loadedEnv)})[key];`);
} else {
server = server.replace('// @@GET_ENV@@', 'return _getEnv(key);');
}
return {
client,

View file

@ -298,9 +298,14 @@ export function redirectToDefaultLocale({
}
// NOTE: public function exported to the users via `astro:i18n` module
export function notFound({ base, locales }: MiddlewarePayload) {
export function notFound({ base, locales, fallback }: MiddlewarePayload) {
return function (context: APIContext, response?: Response): Response | undefined {
if (response?.headers.get(REROUTE_DIRECTIVE_HEADER) === 'no') return response;
if (
response?.headers.get(REROUTE_DIRECTIVE_HEADER) === 'no' &&
typeof fallback === 'undefined'
) {
return response;
}
const url = context.url;
// We return a 404 if:

View file

@ -83,7 +83,6 @@ export function createI18nMiddleware(
}
const { currentLocale } = context;
switch (i18n.strategy) {
// NOTE: theoretically, we should never hit this code path
case 'manual': {

View file

@ -7,7 +7,7 @@ import { HTMLString, markHTMLString } from '../escape.js';
export const voidElementNames =
/^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const htmlBooleanAttributes =
/^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i;
/^(?:allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|inert|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|selected|itemscope)$/i;
const AMPERSAND_REGEX = /&/g;
const DOUBLE_QUOTE_REGEX = /"/g;

View file

@ -378,10 +378,10 @@ if (i18n?.routing === 'manual') {
fallbackType = toFallbackType(customOptions);
const manifest: SSRManifest['i18n'] = {
...i18n,
fallback: undefined,
strategy,
domainLookupTable: {},
fallbackType,
fallback: i18n.fallback,
};
return I18nInternals.createMiddleware(manifest, base, trailingSlash, format);
};

View file

@ -191,7 +191,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
i18n: i18nManifest,
checkOrigin:
(settings.config.security?.checkOrigin && settings.buildOutput === 'server') ?? false,
envGetSecretEnabled: false,
key: hasEnvironmentKey() ? getEnvironmentKey() : createKey(),
middleware() {
return {

View file

@ -11,7 +11,7 @@ import { req } from '../core/messages.js';
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
import { routeIsRedirect } from '../core/redirects/index.js';
import { RenderContext } from '../core/render-context.js';
import { type SSROptions, getProps } from '../core/render/index.js';
import { getProps } from '../core/render/index.js';
import { createRequest } from '../core/request.js';
import { redirectTemplate } from '../core/routing/3xx.js';
import { matchAllRoutes } from '../core/routing/index.js';

View file

@ -1,63 +1,14 @@
import { fileURLToPath } from 'node:url';
import { transform } from 'esbuild';
import MagicString from 'magic-string';
import type * as vite from 'vite';
import { loadEnv } from 'vite';
import type { AstroSettings } from '../types/astro.js';
import type { AstroConfig } from '../types/public/config.js';
import type { EnvLoader } from '../env/env-loader.js';
interface EnvPluginOptions {
settings: AstroSettings;
envLoader: EnvLoader;
}
// Match `import.meta.env` directly without trailing property access
const importMetaEnvOnlyRe = /\bimport\.meta\.env\b(?!\.)/;
// Match valid JS variable names (identifiers), which accepts most alphanumeric characters,
// except that the first character cannot be a number.
const isValidIdentifierRe = /^[_$a-zA-Z][\w$]*$/;
function getPrivateEnv(
viteConfig: vite.ResolvedConfig,
astroConfig: AstroConfig,
): Record<string, string> {
let envPrefixes: string[] = ['PUBLIC_'];
if (viteConfig.envPrefix) {
envPrefixes = Array.isArray(viteConfig.envPrefix)
? viteConfig.envPrefix
: [viteConfig.envPrefix];
}
// Loads environment variables from `.env` files and `process.env`
const fullEnv = loadEnv(
viteConfig.mode,
viteConfig.envDir ?? fileURLToPath(astroConfig.root),
'',
);
const privateEnv: Record<string, string> = {};
for (const key in fullEnv) {
// Ignore public env var
if (isValidIdentifierRe.test(key) && envPrefixes.every((prefix) => !key.startsWith(prefix))) {
if (typeof process.env[key] !== 'undefined') {
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') {
privateEnv[key] = value;
} else {
privateEnv[key] = `process.env.${key}`;
}
} else {
privateEnv[key] = JSON.stringify(fullEnv[key]);
}
}
}
return privateEnv;
}
function getReferencedPrivateKeys(source: string, privateEnv: Record<string, any>): Set<string> {
const references = new Set<string>();
@ -114,13 +65,12 @@ async function replaceDefine(
};
}
export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plugin {
export default function envVitePlugin({ envLoader }: EnvPluginOptions): vite.Plugin {
let privateEnv: Record<string, string>;
let defaultDefines: Record<string, string>;
let isDev: boolean;
let devImportMetaEnvPrepend: string;
let viteConfig: vite.ResolvedConfig;
const { config: astroConfig } = settings;
return {
name: 'astro:vite-plugin-env',
config(_, { command }) {
@ -152,7 +102,9 @@ export default function envVitePlugin({ settings }: EnvPluginOptions): vite.Plug
}
// Find matches for *private* env and do our own replacement.
privateEnv ??= getPrivateEnv(viteConfig, astroConfig);
// Env is retrieved before process.env is populated by astro:env
// so that import.meta.env is first replaced by values, not process.env
privateEnv ??= envLoader.getPrivateEnv();
// In dev, we can assign the private env vars to `import.meta.env` directly for performance
if (isDev) {

View file

@ -1,4 +1,9 @@
import { ActionError, deserializeActionResult, getActionQueryString } from 'astro:actions';
import {
ActionError,
appendForwardSlash,
deserializeActionResult,
getActionQueryString,
} from 'astro:actions';
const ENCODED_DOT = '%2E';
@ -83,7 +88,15 @@ async function handleAction(param, path, context) {
headers.set('Content-Length', '0');
}
}
const rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}`, {
const shouldAppendTrailingSlash = '/** @TRAILING_SLASH@ **/';
let actionPath = import.meta.env.BASE_URL.replace(/\/$/, '') + '/_actions/' + path;
if (shouldAppendTrailingSlash) {
actionPath = appendForwardSlash(actionPath);
}
const rawResult = await fetch(actionPath, {
method: 'POST',
body,
headers,

View file

@ -1,13 +1,23 @@
// @ts-check
import { schema } from 'virtual:astro:env/internal';
import {
// biome-ignore lint/correctness/noUnusedImports: `_getEnv` is used by the generated code
getEnv as _getEnv,
createInvalidVariablesError,
getEnv,
getEnvFieldType,
setOnSetGetEnv,
validateEnvVariable,
} from 'astro/env/runtime';
// @ts-expect-error
/** @returns {string} */
// used while generating the virtual module
// biome-ignore lint/correctness/noUnusedFunctionParameters: `key` is used by the generated code
// biome-ignore lint/correctness/noUnusedVariables: `key` is used by the generated code
const getEnv = (key) => {
// @@GET_ENV@@
};
export const getSecret = (key) => {
return getEnv(key);
};
@ -25,9 +35,6 @@ const _internalGetSecret = (key) => {
throw createInvalidVariablesError(key, type, result);
};
// used while generating the virtual module
// biome-ignore lint/correctness/noUnusedFunctionParameters: `reset` is used by the generated code
// biome-ignore lint/correctness/noUnusedVariables: `reset` is used by the generated code
setOnSetGetEnv((reset) => {
setOnSetGetEnv(() => {
// @@ON_SET_GET_ENV@@
});

View file

@ -564,6 +564,30 @@ it('Base path should be used', async () => {
await devServer.stop();
});
it('Should support trailing slash', async () => {
const fixture = await loadFixture({
root: './fixtures/actions/',
adapter: testAdapter(),
trailingSlash: 'always',
});
const devServer = await fixture.startDevServer();
const formData = new FormData();
formData.append('channel', 'bholmesdev');
formData.append('comment', 'Hello, World!');
const res = await fixture.fetch('/_actions/comment/', {
method: 'POST',
body: formData,
});
assert.equal(res.ok, true);
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
const data = devalue.parse(await res.text());
assert.equal(data.channel, 'bholmesdev');
assert.equal(data.comment, 'Hello, World!');
await devServer.stop();
});
/**
* Follow an expected redirect response.
*

View file

@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { fixLineEndings, loadFixture } from './test-utils.js';
@ -154,4 +154,24 @@ describe('Astro Markdown', () => {
assert.ok(title.includes('import.meta.env.TITLE'));
});
});
describe('dev', () => {
let devServer;
before(async () => {
devServer = await fixture.startDevServer();
});
it('ignores .md extensions on query params', async () => {
const res = await fixture.fetch('/false-positive?page=page.md');
assert.ok(res.ok);
const html = await res.text();
const $ = cheerio.load(html);
assert.equal($('p').text(), 'the page is not markdown');
});
after(async () => {
await devServer.stop();
});
});
});

View file

@ -0,0 +1,5 @@
---
let page = "not markdown"
---
<p>the page is {page}</p>

View file

@ -0,0 +1,16 @@
---
title: I'm back!
description: 'Introduction to Iguana.'
publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [cats, felines]
---
import H2 from "../src/components/H2.astro";
<H2>Iguana</H2>
### Iguana
This is a rendered entry
![file](./I'm%20back.jpg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -3,6 +3,8 @@ title: Endeavour
description: 'Learn about the Endeavour NASA space shuttle.'
publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 90s]
heroImage: "@images/I'm back.jpg"
---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)

View file

@ -1,18 +1,28 @@
---
// for SSR
const blogs = {
1: { content: "Hello world" },
2: { content: "Eat Something" },
3: { content: "How are you?" },
}
const id = Astro.params?.id;
const ssrContent = id && blogs[id]?.content;
// for SSG
export function getStaticPaths() {
return [
{params: {id: '1'}, props: { content: "Hello world" }},
{params: {id: '2'}, props: { content: "Eat Something" }},
{params: {id: '3'}, props: { content: "How are you?" }},
];
]
}
const { content } = Astro.props;
const { content } = Astro.props
---
<html>
<head>
<title>Astro</title>
</head>
<body>
{content}
{content || ssrContent}
</body>
</html>

View file

@ -1,4 +1,12 @@
---
const blogs = {
1: { content: "Hola mundo" },
2: { content: "Eat Something" },
3: { content: "How are you?" },
}
const id = Astro.params?.id;
const ssrContent = id && blogs[id]?.content;
export function getStaticPaths() {
return [
{params: {id: '1'}, props: { content: "Hola mundo" }},
@ -13,6 +21,6 @@ const { content } = Astro.props;
<title>Astro</title>
</head>
<body>
{content}
{content || ssrContent}
</body>
</html>

View file

@ -9,6 +9,9 @@ export default defineConfig({
codes: ["es", "es-ar"]
}
],
routing: "manual"
routing: "manual",
fallback: {
it: 'en'
}
}
})

View file

@ -18,5 +18,6 @@ export const onRequest = sequence(
customLogic,
middleware({
prefixDefaultLocale: true,
fallbackType: "rewrite"
})
);

View file

@ -0,0 +1,8 @@
{
"name": "@test/middleware-full-ssr",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1 @@
throw new Error("Shoud not error at build time")

View file

@ -0,0 +1,2 @@
import "./error.js"
export const onRequest = (_ , next) => next();

View file

@ -0,0 +1,14 @@
---
const data = Astro.locals;
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<span>Index</span>
<p>{data?.name}</p>
</body>
</html>

View file

@ -117,4 +117,13 @@ describe('SSR manual routing', () => {
const $ = cheerio.load(html);
assert.equal($('p').text(), '/en/blog/title/');
});
it('should use the fallback', async () => {
let request = new Request('http://example.com/it/start');
let response = await app.render(request);
assert.equal(response.status, 200);
const html = await response.text();
const $ = cheerio.load(html);
assert.equal($('p').text(), '/en/blog/title/');
});
});

View file

@ -2000,12 +2000,14 @@ describe('Fallback rewrite dev server', () => {
root: './fixtures/i18n-routing-fallback/',
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
locales: ['en', 'fr', 'es', 'it', 'pt'],
routing: {
prefixDefaultLocale: false,
},
fallback: {
fr: 'en',
it: 'en',
es: 'pt',
},
fallbackType: 'rewrite',
},
@ -2021,6 +2023,27 @@ describe('Fallback rewrite dev server', () => {
assert.match(html, /Hello/);
// assert.fail()
});
it('should render fallback locale paths with path parameters correctly (fr)', async () => {
let response = await fixture.fetch('/fr/blog/1');
assert.equal(response.status, 200);
const text = await response.text();
assert.match(text, /Hello world/);
});
it('should render fallback locale paths with path parameters correctly (es)', async () => {
let response = await fixture.fetch('/es/blog/1');
assert.equal(response.status, 200);
const text = await response.text();
assert.match(text, /Hola mundo/);
});
it('should render fallback locale paths with query parameters correctly (it)', async () => {
let response = await fixture.fetch('/it/blog/1');
assert.equal(response.status, 200);
const text = await response.text();
assert.match(text, /Hello world/);
});
});
describe('Fallback rewrite SSG', () => {
@ -2032,13 +2055,15 @@ describe('Fallback rewrite SSG', () => {
root: './fixtures/i18n-routing-fallback/',
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
locales: ['en', 'fr', 'es', 'it', 'pt'],
routing: {
prefixDefaultLocale: false,
fallbackType: 'rewrite',
},
fallback: {
fr: 'en',
it: 'en',
es: 'pt',
},
},
});
@ -2051,6 +2076,21 @@ describe('Fallback rewrite SSG', () => {
assert.match(html, /Hello/);
// assert.fail()
});
it('should render fallback locale paths with path parameters correctly (fr)', async () => {
const html = await fixture.readFile('/fr/blog/1/index.html');
assert.match(html, /Hello world/);
});
it('should render fallback locale paths with path parameters correctly (es)', async () => {
const html = await fixture.readFile('/es/blog/1/index.html');
assert.match(html, /Hola mundo/);
});
it('should render fallback locale paths with query parameters correctly (it)', async () => {
const html = await fixture.readFile('/it/blog/1/index.html');
assert.match(html, /Hello world/);
});
});
describe('Fallback rewrite SSR', () => {
@ -2066,13 +2106,15 @@ describe('Fallback rewrite SSR', () => {
adapter: testAdapter(),
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
locales: ['en', 'fr', 'es', 'it', 'pt'],
routing: {
prefixDefaultLocale: false,
fallbackType: 'rewrite',
},
fallback: {
fr: 'en',
it: 'en',
es: 'pt',
},
},
});
@ -2087,4 +2129,28 @@ describe('Fallback rewrite SSR', () => {
const html = await response.text();
assert.match(html, /Hello/);
});
it('should render fallback locale paths with path parameters correctly (fr)', async () => {
let request = new Request('http://example.com/new-site/fr/blog/1');
let response = await app.render(request);
assert.equal(response.status, 200);
const text = await response.text();
assert.match(text, /Hello world/);
});
it('should render fallback locale paths with path parameters correctly (es)', async () => {
let request = new Request('http://example.com/new-site/es/blog/1');
let response = await app.render(request);
assert.equal(response.status, 200);
const text = await response.text();
assert.match(text, /Hola mundo/);
});
it('should render fallback locale paths with query parameters correctly (it)', async () => {
let request = new Request('http://example.com/new-site/it/blog/1');
let response = await app.render(request);
assert.equal(response.status, 200);
const text = await response.text();
assert.match(text, /Hello world/);
});
});

View file

@ -171,6 +171,21 @@ describe('Middleware in PROD mode, SSG', () => {
});
});
describe('Middleware should not be executed or imported during', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
it('should build the project without errors', async () => {
fixture = await loadFixture({
root: './fixtures/middleware-full-ssr/',
output: 'server',
adapter: testAdapter({}),
});
await fixture.build();
assert.ok('Should build');
});
});
describe('Middleware API in PROD mode, SSR', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

View file

@ -1,6 +1,5 @@
import * as assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import * as cheerio from 'cheerio';
import { createContainer } from '../../../dist/core/dev/container.js';
import { createViteLoader } from '../../../dist/core/module-loader/vite.js';

View file

@ -1,6 +1,6 @@
{
"name": "@astrojs/db",
"version": "0.14.1",
"version": "0.14.3",
"description": "Add libSQL and Astro Studio support to your Astro site",
"license": "MIT",
"repository": {

View file

@ -1,8 +1,8 @@
import { existsSync } from 'node:fs';
import { LibsqlError } from '@libsql/client';
import type { AstroConfig } from 'astro';
import { green } from 'kleur/colors';
import type { Arguments } from 'yargs-parser';
import { isDbError } from '../../../../runtime/utils.js';
import {
EXEC_DEFAULT_EXPORT_ERROR,
EXEC_ERROR,
@ -64,9 +64,7 @@ export async function cmd({
await mod.default();
console.info(`${green('✔')} File run successfully.`);
} catch (e) {
if (e instanceof LibsqlError) {
throw new Error(EXEC_ERROR(e.message));
}
throw e;
if (isDbError(e)) throw new Error(EXEC_ERROR(e.message));
else throw e;
}
}

View file

@ -1,5 +1,4 @@
import { stripVTControlCharacters } from 'node:util';
import { LibsqlError } from '@libsql/client';
import deepDiff from 'deep-diff';
import { sql } from 'drizzle-orm';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
@ -8,7 +7,7 @@ import { customAlphabet } from 'nanoid';
import { hasPrimaryKey } from '../../runtime/index.js';
import { createRemoteDatabaseClient } from '../../runtime/index.js';
import { isSerializedSQL } from '../../runtime/types.js';
import { safeFetch } from '../../runtime/utils.js';
import { isDbError, safeFetch } from '../../runtime/utils.js';
import { MIGRATION_VERSION } from '../consts.js';
import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js';
import {
@ -454,7 +453,7 @@ async function getDbCurrentSnapshot(
} catch (error) {
// Don't handle errors that are not from libSQL
if (
error instanceof LibsqlError &&
isDbError(error) &&
// If the schema was never pushed to the database yet the table won't exist.
// Treat a missing snapshot table as an empty table.

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