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

Merge branch 'main' into next

This commit is contained in:
Matt Kane 2024-09-02 17:40:53 +01:00
commit a1d78b75aa
281 changed files with 679 additions and 10241 deletions

View file

@ -1,5 +0,0 @@
---
'astro': patch
---
Refactors content layer sync to use a queue

View file

@ -8,7 +8,7 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "workspace:*", "@astrojs/mdx": "workspace:*",
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"@benchmark/timer": "workspace:*", "@benchmark/timer": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"autocannon": "^7.15.0", "autocannon": "^7.15.0",

View file

@ -19,7 +19,7 @@
"vitest": "^2.0.5" "vitest": "^2.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0" "@types/react-dom": "^18.3.0"
} }
} }

View file

@ -12,7 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/react": "^3.6.2", "@astrojs/react": "^3.6.2",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -13,6 +13,6 @@
"dependencies": { "dependencies": {
"@astrojs/solid-js": "^4.4.1", "@astrojs/solid-js": "^4.4.1",
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"solid-js": "^1.8.21" "solid-js": "^1.8.22"
} }
} }

View file

@ -13,6 +13,6 @@
"dependencies": { "dependencies": {
"@astrojs/svelte": "^6.0.0-alpha.0", "@astrojs/svelte": "^6.0.0-alpha.0",
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"svelte": "^4.2.18" "svelte": "^4.2.19"
} }
} }

View file

@ -14,11 +14,11 @@
"@astrojs/react": "^3.6.2", "@astrojs/react": "^3.6.2",
"@astrojs/tailwind": "^6.0.0-alpha.0", "@astrojs/tailwind": "^6.0.0-alpha.0",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.8",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"tailwindcss": "^3.4.10" "tailwindcss": "^3.4.10"

View file

@ -15,6 +15,6 @@
"@astrojs/node": "^9.0.0-alpha.1", "@astrojs/node": "^9.0.0-alpha.1",
"@astrojs/svelte": "^6.0.0-alpha.0", "@astrojs/svelte": "^6.0.0-alpha.0",
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"svelte": "^4.2.18" "svelte": "^4.2.19"
} }
} }

View file

@ -14,7 +14,7 @@
"@astrojs/preact": "^3.5.2", "@astrojs/preact": "^3.5.2",
"@nanostores/preact": "^0.5.2", "@nanostores/preact": "^0.5.2",
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"nanostores": "^0.11.2", "nanostores": "^0.11.3",
"preact": "^10.23.2" "preact": "^10.23.2"
} }
} }

View file

@ -17,7 +17,7 @@
"astro": "^5.0.0-alpha.2", "astro": "^5.0.0-alpha.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"canvas-confetti": "^1.9.3", "canvas-confetti": "^1.9.3",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"tailwindcss": "^3.4.10" "tailwindcss": "^3.4.10"
} }
} }

View file

@ -65,7 +65,7 @@
"only-allow": "^1.2.1", "only-allow": "^1.2.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"turbo": "^2.1.0", "turbo": "^2.1.1",
"typescript": "~5.5.4", "typescript": "~5.5.4",
"typescript-eslint": "^8.3.0" "typescript-eslint": "^8.3.0"
}, },

View file

@ -88,6 +88,22 @@
As this is a potentially breaking change to your script behavior, please review your `<script>` tags and ensure that they behave as expected. As this is a potentially breaking change to your script behavior, please review your `<script>` tags and ensure that they behave as expected.
## 4.15.2
### Patch Changes
- [#11870](https://github.com/withastro/astro/pull/11870) [`8e5257a`](https://github.com/withastro/astro/commit/8e5257addaeff809ed6f0c47ac0ed4ded755320e) Thanks [@ArmandPhilippot](https://github.com/ArmandPhilippot)! - Fixes typo in documenting the `fallbackType` property in i18n routing
- [#11884](https://github.com/withastro/astro/pull/11884) [`e450704`](https://github.com/withastro/astro/commit/e45070459f18976400fc8939812e172781eba351) Thanks [@ascorbic](https://github.com/ascorbic)! - Correctly handles content layer data where the transformed value does not match the input schema
- [#11900](https://github.com/withastro/astro/pull/11900) [`80b4a18`](https://github.com/withastro/astro/commit/80b4a181a077266c44065a737e61cc7cff6bc6d7) Thanks [@delucis](https://github.com/delucis)! - Fixes the user-facing type of the new `i18n.routing.fallbackType` option to be optional
## 4.15.1
### Patch Changes
- [#11872](https://github.com/withastro/astro/pull/11872) [`9327d56`](https://github.com/withastro/astro/commit/9327d56755404b481993b058bbfc4aa7880b2304) Thanks [@bluwy](https://github.com/bluwy)! - Fixes `astro add` importing adapters and integrations
- [#11767](https://github.com/withastro/astro/pull/11767) [`d1bd1a1`](https://github.com/withastro/astro/commit/d1bd1a11f7aca4d2141d1c4665f2db0440393d03) Thanks [@ascorbic](https://github.com/ascorbic)! - Refactors content layer sync to use a queue - [#11767](https://github.com/withastro/astro/pull/11767) [`d1bd1a1`](https://github.com/withastro/astro/commit/d1bd1a11f7aca4d2141d1c4665f2db0440393d03) Thanks [@ascorbic](https://github.com/ascorbic)! - Refactors content layer sync to use a queue
## 4.15.0 ## 4.15.0

View file

@ -12,9 +12,9 @@
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.3", "@astrojs/check": "^0.9.3",
"@astrojs/db": "workspace:*", "@astrojs/db": "workspace:*",
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "workspace:*", "astro": "workspace:*",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.3", "@astrojs/check": "^0.9.3",
"@astrojs/db": "workspace:*", "@astrojs/db": "workspace:*",
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@types/react": "npm:types-react", "@types/react": "npm:types-react",
"@types/react-dom": "npm:types-react-dom", "@types/react-dom": "npm:types-react-dom",

View file

@ -9,7 +9,7 @@
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"@astrojs/mdx": "workspace:*", "@astrojs/mdx": "workspace:*",
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
} }

View file

@ -6,7 +6,7 @@
"@astrojs/tailwind": "workspace:*", "@astrojs/tailwind": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"tailwindcss": "^3.4.10" "tailwindcss": "^3.4.10"
} }
} }

View file

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@astrojs/svelte": "workspace:*", "@astrojs/svelte": "workspace:*",
"@astrojs/vue": "workspace:*", "@astrojs/vue": "workspace:*",

View file

@ -169,15 +169,15 @@
"prompts": "^2.4.2", "prompts": "^2.4.2",
"rehype": "^13.0.1", "rehype": "^13.0.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"shiki": "^1.14.1", "shiki": "^1.16.1",
"string-width": "^7.2.0", "string-width": "^7.2.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"tinyexec": "^0.3.0", "tinyexec": "^0.3.0",
"tsconfck": "^3.1.1", "tsconfck": "^3.1.3",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vfile": "^6.0.3", "vfile": "^6.0.3",
"vite": "^5.4.2", "vite": "^5.4.2",
"vitefu": "^0.2.5", "vitefu": "^1.0.2",
"which-pm": "^3.0.0", "which-pm": "^3.0.0",
"xxhash-wasm": "^1.0.2", "xxhash-wasm": "^1.0.2",
"yargs-parser": "^21.1.1", "yargs-parser": "^21.1.1",
@ -192,12 +192,10 @@
"@astrojs/check": "^0.9.3", "@astrojs/check": "^0.9.3",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.46.1",
"@types/aria-query": "^5.0.4", "@types/aria-query": "^5.0.4",
"@types/babel__generator": "^7.6.8",
"@types/babel__traverse": "^7.20.6",
"@types/common-ancestor-path": "^1.0.2", "@types/common-ancestor-path": "^1.0.2",
"@types/cssesc": "^3.0.2", "@types/cssesc": "^3.0.2",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/diff": "^5.2.1", "@types/diff": "^5.2.2",
"@types/dlv": "^1.1.4", "@types/dlv": "^1.1.4",
"@types/dom-view-transitions": "^1.0.5", "@types/dom-view-transitions": "^1.0.5",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
@ -210,7 +208,7 @@
"@types/yargs-parser": "^21.0.3", "@types/yargs-parser": "^21.0.3",
"astro-scripts": "workspace:*", "astro-scripts": "workspace:*",
"cheerio": "1.0.0", "cheerio": "1.0.0",
"eol": "^0.9.1", "eol": "^0.10.0",
"execa": "^8.0.1", "execa": "^8.0.1",
"expect-type": "^0.20.0", "expect-type": "^0.20.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
@ -222,7 +220,7 @@
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"rehype-toc": "^3.0.2", "rehype-toc": "^3.0.2",
"remark-code-titles": "^0.1.2", "remark-code-titles": "^0.1.2",
"rollup": "^4.21.1", "rollup": "^4.21.2",
"sass": "^1.77.8", "sass": "^1.77.8",
"undici": "^6.19.8", "undici": "^6.19.8",
"unified": "^11.0.5" "unified": "^11.0.5"

View file

@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@performance/utils": "workspace:*", "@performance/utils": "workspace:*",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "workspace:*", "astro": "workspace:*",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -17,7 +17,7 @@
"@astrojs/markdoc": "workspace:*", "@astrojs/markdoc": "workspace:*",
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@performance/utils": "workspace:*", "@performance/utils": "workspace:*",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "workspace:*", "astro": "workspace:*",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -17,7 +17,7 @@
"@astrojs/mdx": "workspace:*", "@astrojs/mdx": "workspace:*",
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@performance/utils": "workspace:*", "@performance/utils": "workspace:*",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "workspace:*", "astro": "workspace:*",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -427,7 +427,11 @@ function addIntegration(mod: ProxifiedModule<any>, integration: IntegrationInfo)
const integrationId = toIdent(integration.id); const integrationId = toIdent(integration.id);
if (!mod.imports.$items.some((imp) => imp.local === integrationId)) { if (!mod.imports.$items.some((imp) => imp.local === integrationId)) {
mod.imports.$append({ imported: integrationId, from: integration.packageName }); mod.imports.$append({
imported: 'default',
local: integrationId,
from: integration.packageName,
});
} }
config.integrations ??= []; config.integrations ??= [];
@ -448,7 +452,11 @@ export function setAdapter(mod: ProxifiedModule<any>, adapter: IntegrationInfo)
const adapterId = toIdent(adapter.id); const adapterId = toIdent(adapter.id);
if (!mod.imports.$items.some((imp) => imp.local === adapterId)) { if (!mod.imports.$items.some((imp) => imp.local === adapterId)) {
mod.imports.$append({ imported: adapterId, from: adapter.packageName }); mod.imports.$append({
imported: 'default',
local: adapterId,
from: adapter.packageName,
});
} }
if (!config.output) { if (!config.output) {

View file

@ -1,6 +1,4 @@
import { promises as fs, existsSync } from 'node:fs'; import { promises as fs, existsSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as fastq from 'fastq'; import * as fastq from 'fastq';
import type { FSWatcher } from 'vite'; import type { FSWatcher } from 'vite';
import xxhash from 'xxhash-wasm'; import xxhash from 'xxhash-wasm';
@ -20,7 +18,6 @@ import {
getEntryConfigByExtMap, getEntryConfigByExtMap,
getEntryDataAndImages, getEntryDataAndImages,
globalContentConfigObserver, globalContentConfigObserver,
posixRelative,
} from './utils.js'; } from './utils.js';
export interface ContentLayerOptions { export interface ContentLayerOptions {
@ -189,7 +186,7 @@ export class ContentLayer {
const collectionWithResolvedSchema = { ...collection, schema }; const collectionWithResolvedSchema = { ...collection, schema };
const parseData: LoaderContext['parseData'] = async ({ id, data, filePath = '' }) => { const parseData: LoaderContext['parseData'] = async ({ id, data, filePath = '' }) => {
const { imageImports, data: parsedData } = await getEntryDataAndImages( const { data: parsedData } = await getEntryDataAndImages(
{ {
id, id,
collection: name, collection: name,
@ -202,15 +199,6 @@ export class ContentLayer {
collectionWithResolvedSchema, collectionWithResolvedSchema,
false, false,
); );
if (imageImports?.length) {
this.#store.addAssetImports(
imageImports,
// This path may already be relative, if we're re-parsing an existing entry
isAbsolute(filePath)
? posixRelative(fileURLToPath(this.#settings.config.root), filePath)
: filePath,
);
}
return parsedData; return parsedData;
}; };

View file

@ -33,6 +33,7 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase when calling `renderEntry`. * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase when calling `renderEntry`.
*/ */
deferredRender?: boolean; deferredRender?: boolean;
assetImports?: Array<string>;
} }
/** /**

View file

@ -107,15 +107,11 @@ export function glob(globOptions: GlobOptions): Loader {
store.addModuleImport(existingEntry.filePath); store.addModuleImport(existingEntry.filePath);
} }
if (existingEntry.rendered?.metadata?.imagePaths?.length) { if (existingEntry.assetImports?.length) {
// Add asset imports for existing entries // Add asset imports for existing entries
store.addAssetImports( store.addAssetImports(existingEntry.assetImports, existingEntry.filePath);
existingEntry.rendered.metadata.imagePaths,
existingEntry.filePath,
);
} }
// Re-parsing to resolve images and other effects
await parseData(existingEntry);
return; return;
} }
@ -156,10 +152,9 @@ export function glob(globOptions: GlobOptions): Loader {
filePath: relativePath, filePath: relativePath,
digest, digest,
rendered, rendered,
assetImports: rendered?.metadata?.imagePaths,
}); });
if (rendered?.metadata?.imagePaths?.length) {
store.addAssetImports(rendered.metadata.imagePaths, relativePath);
}
// todo: add an explicit way to opt in to deferred rendering // todo: add an explicit way to opt in to deferred rendering
} else if ('contentModuleTypes' in entryType) { } else if ('contentModuleTypes' in entryType) {
store.set({ store.set({

View file

@ -1,7 +1,9 @@
import { promises as fs, type PathLike, existsSync } from 'node:fs'; import { promises as fs, type PathLike, existsSync } from 'node:fs';
import * as devalue from 'devalue'; import * as devalue from 'devalue';
import { Traverse } from 'neotraverse/modern';
import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js'; import { imageSrcToImportId, importIdToSymbolName } from '../assets/utils/resolveImports.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js'; import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { IMAGE_IMPORT_PREFIX } from './consts.js';
import { type DataEntry, DataStore, type RenderedContent } from './data-store.js'; import { type DataEntry, DataStore, type RenderedContent } from './data-store.js';
import { contentModuleToId } from './utils.js'; import { contentModuleToId } from './utils.js';
@ -53,7 +55,7 @@ export class MutableDataStore extends DataStore {
this.#saveToDiskDebounced(); this.#saveToDiskDebounced();
} }
addAssetImport(assetImport: string, filePath: string) { addAssetImport(assetImport: string, filePath?: string) {
const id = imageSrcToImportId(assetImport, filePath); const id = imageSrcToImportId(assetImport, filePath);
if (id) { if (id) {
this.#assetImports.add(id); this.#assetImports.add(id);
@ -64,7 +66,7 @@ export class MutableDataStore extends DataStore {
} }
} }
addAssetImports(assets: Array<string>, filePath: string) { addAssetImports(assets: Array<string>, filePath?: string) {
assets.forEach((asset) => this.addAssetImport(asset, filePath)); assets.forEach((asset) => this.addAssetImport(asset, filePath));
} }
@ -195,7 +197,7 @@ export default new Map([\n${lines.join(',\n')}]);
entries: () => this.entries(collectionName), entries: () => this.entries(collectionName),
values: () => this.values(collectionName), values: () => this.values(collectionName),
keys: () => this.keys(collectionName), keys: () => this.keys(collectionName),
set: ({ id: key, data, body, filePath, deferredRender, digest, rendered }) => { set: ({ id: key, data, body, filePath, deferredRender, digest, rendered, assetImports }) => {
if (!key) { if (!key) {
throw new Error(`ID must be a non-empty string`); throw new Error(`ID must be a non-empty string`);
} }
@ -206,6 +208,15 @@ export default new Map([\n${lines.join(',\n')}]);
return false; return false;
} }
} }
const foundAssets = new Set<string>(assetImports);
// Check for image imports in the data. These will have been prefixed during schema parsing
new Traverse(data).forEach((_, val) => {
if (typeof val === 'string' && val.startsWith(IMAGE_IMPORT_PREFIX)) {
const src = val.replace(IMAGE_IMPORT_PREFIX, '');
foundAssets.add(src);
}
});
const entry: DataEntry = { const entry: DataEntry = {
id, id,
data, data,
@ -221,6 +232,12 @@ export default new Map([\n${lines.join(',\n')}]);
} }
entry.filePath = filePath; entry.filePath = filePath;
} }
if (foundAssets.size) {
entry.assetImports = Array.from(foundAssets);
this.addAssetImports(entry.assetImports, filePath);
}
if (digest) { if (digest) {
entry.digest = digest; entry.digest = digest;
} }
@ -334,6 +351,12 @@ export interface ScopedDataStore {
* If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase. * If an entry is a deferred, its rendering phase is delegated to a virtual module during the runtime phase.
*/ */
deferredRender?: boolean; deferredRender?: boolean;
/**
* Assets such as images to process during the build. These should be files on disk, with a path relative to filePath.
* Any values that use image() in the schema will already be added automatically.
* @internal
*/
assetImports?: Array<string>;
}) => boolean; }) => boolean;
values: () => Array<DataEntry>; values: () => Array<DataEntry>;
keys: () => Array<string>; keys: () => Array<string>;

View file

@ -21,8 +21,8 @@ Message:
- Begin with **what happened** and **why**. (ex: `Could not use {feature} because Server-side Rendering is not enabled.`) - Begin with **what happened** and **why**. (ex: `Could not use {feature} because Server-side Rendering is not enabled.`)
- Then, **describe the action the user should take**. (ex: `Update your Astro config with `output: 'server'` to enable Server-side Rendering.`) - Then, **describe the action the user should take**. (ex: `Update your Astro config with `output: 'server'` to enable Server-side Rendering.`)
- Although this does not need to be as brief as the `title`, try to keep sentences short, clear and direct to give the reader all the necessary information quickly as possible. - Although this does not need to be as brief as the `title`, try to keep sentences short, clear and direct to give the reader all the necessary information quickly as possible. Users should be able to skim the message and understand the problem and solution.
- Instead of writing a longer message, consider using a `hint`. - If your message is too long, or the solution is not guaranteed to work, use the `hint` property to provide more information.
Hint: Hint:
@ -44,10 +44,10 @@ If you are unsure about anything, ask [Erika](https://github.com/Princesseuh)!
### Shape ### Shape
- **Names are permanent**, and should never be changed, nor deleted. Users should always be able to find an error by searching, and this ensures a matching result. When an error is no longer relevant, it should be deprecated, not removed. - **Names are permanent**, and should never be changed. Users should always be able to find an error by searching, and this ensures a matching result.
- Contextual information may be used to enhance the message or the hint. However, the code that caused the error or the position of the error should not be included in the message as they will already be shown as part of the error. - Contextual information may be used to enhance the message or the hint. However, the code that caused the error or the position of the error should not be included in the message as they will already be shown as part of the error.
- Do not prefix `title`, `message` and `hint` with descriptive words such as "Error:" or "Hint:" as it may lead to duplicated labels in the UI / CLI. - Do not prefix `title`, `message` and `hint` with descriptive words such as "Error:" or "Hint:" as it may lead to duplicated labels in the UI / CLI.
- Dynamic error messages must use the following shape: - Dynamic error messages **must** use the following shape:
```js ```js
message: (arguments) => `text ${substitute}`; message: (arguments) => `text ${substitute}`;
@ -69,8 +69,9 @@ Here's how to create and format the comments:
/** /**
* @docs <- Needed for the comment to be used for docs * @docs <- Needed for the comment to be used for docs
* @message <- (Optional) Clearer error message to show in cases where the original one is too complex (ex: because of conditional messages) * @message <- (Optional) Clearer error message to show in cases where the original one is too complex (ex: because of conditional messages)
* @see <- List of additional references users can look at * @see <- (Optional) List of additional references users can look at
* @description <- Description of the error * @description <- Description of the error
* @deprecated <- (Optional) If the error is no longer relevant, when it was removed and why (see "Removing errors" section below)
*/ */
``` ```
@ -89,6 +90,19 @@ Example:
The `@message` property is intended to provide slightly more context when it is helpful: a more descriptive error message or a collection of common messages if there are multiple possible error messages. Try to avoid making substantial changes to existing messages so that they are easy to find for users who copy and search the exact content of an error message. The `@message` property is intended to provide slightly more context when it is helpful: a more descriptive error message or a collection of common messages if there are multiple possible error messages. Try to avoid making substantial changes to existing messages so that they are easy to find for users who copy and search the exact content of an error message.
### Removing errors
If the error cannot be triggered at all anymore, it can deprecated by adding a `@deprecated` tag to the JSDoc comment with a message that will be shown in the docs. This message is useful for users on previous versions who might still encounter the error so that they can know that upgrading to a newer version of Astro would perhaps solve their issue.
```js
/**
* @docs
* @deprecated Removed in Astro v9.8.6 as it is no longer relevant due to...
*/
```
Alternatively, if no special deprecation message is needed, errors can be directly removed from the `errors-data.ts` file. A basic message will be shown in the docs stating that the error can no longer appear in the latest version of Astro.
### Always remember ### Always remember
Error are a reactive strategy. They are the last line of defense against a mistake. Error are a reactive strategy. They are the last line of defense against a mistake.
@ -99,5 +113,7 @@ While adding a new error message, ask yourself, "Was there a way this situation
## Additional resources on writing good error messages ## Additional resources on writing good error messages
- [Compiler errors for humans](https://elm-lang.org/news/compiler-errors-for-humans)
- [When life gives you lemons, write better error messages](https://wix-ux.com/when-life-gives-you-lemons-write-better-error-messages-46c5223e1a2f) - [When life gives you lemons, write better error messages](https://wix-ux.com/when-life-gives-you-lemons-write-better-error-messages-46c5223e1a2f)
- [RustConf 2020 - Bending the Curve: A Personal Tutor at Your Fingertips by Esteban Kuber](https://www.youtube.com/watch?v=Z6X7Ada0ugE) (part on error messages starts around 19:17) - [RustConf 2020 - Bending the Curve: A Personal Tutor at Your Fingertips by Esteban Kuber](https://www.youtube.com/watch?v=Z6X7Ada0ugE)
- [What's in a good error](https://erika.florist/articles/gooderrors) (by the person who wrote this document!)

View file

@ -33,25 +33,7 @@ export const UnknownCompilerError = {
title: 'Unknown compiler error.', title: 'Unknown compiler error.',
hint: 'This is almost always a problem with the Astro compiler, not your code. Please open an issue at https://astro.build/issues/compiler.', hint: 'This is almost always a problem with the Astro compiler, not your code. Please open an issue at https://astro.build/issues/compiler.',
} satisfies ErrorData; } satisfies ErrorData;
// 1xxx and 2xxx codes are reserved for compiler errors and warnings respectively
/**
* @docs
* @see
* - [Enabling SSR in Your Project](https://docs.astro.build/en/guides/server-side-rendering/)
* - [Astro.redirect](https://docs.astro.build/en/reference/api-reference/#astroredirect)
* @description
* The `Astro.redirect` function is only available when [Server-side rendering](/en/guides/server-side-rendering/) is enabled.
*
* To redirect on a static website, the [meta refresh attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) can be used. Certain hosts also provide config-based redirects (ex: [Netlify redirects](https://docs.netlify.com/routing/redirects/)).
* @deprecated Deprecated since version 2.6.
*/
export const StaticRedirectNotAvailable = {
name: 'StaticRedirectNotAvailable',
title: '`Astro.redirect` is not available in static mode.',
message:
"Redirects are only available when using `output: 'server'` or `output: 'hybrid'`. Update your Astro config if you need SSR features.",
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/ for more information on how to enable SSR.',
} satisfies ErrorData;
/** /**
* @docs * @docs
* @see * @see
@ -302,21 +284,6 @@ export const InvalidGetStaticPathsReturn = {
hint: 'See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths.', hint: 'See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths.',
} satisfies ErrorData; } satisfies ErrorData;
/**
* @docs
* @deprecated Deprecated since Astro 4.0. The RSS helper no longer exists with an error fallback.
* @see
* - [RSS Guide](https://docs.astro.build/en/guides/rss/)
* @description
* `getStaticPaths` no longer expose an helper for generating a RSS feed. We recommend migrating to the [@astrojs/rss](https://docs.astro.build/en/guides/rss/#setting-up-astrojsrss)integration instead.
*/
export const GetStaticPathsRemovedRSSHelper = {
name: 'GetStaticPathsRemovedRSSHelper',
title: 'getStaticPaths RSS helper is not available anymore.',
message:
'The RSS helper has been removed from `getStaticPaths`. Try the new @astrojs/rss package instead.',
hint: 'See https://docs.astro.build/en/guides/rss/ for more information.',
} satisfies ErrorData;
/** /**
* @docs * @docs
* @see * @see
@ -732,28 +699,6 @@ export const NoImageMetadata = {
hint: 'This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue.', hint: 'This is often caused by a corrupted or malformed image. Re-exporting the image from your image editor may fix this issue.',
} satisfies ErrorData; } satisfies ErrorData;
/**
* @docs
* @deprecated This error is no longer Markdown specific and as such, as been replaced by `ImageNotFound`
* @message
* Could not find requested image `IMAGE_PATH` at `FULL_IMAGE_PATH`.
* @see
* - [Images](https://docs.astro.build/en/guides/images/)
* @description
* Astro could not find an image you included in your Markdown content. Usually, this is simply caused by a typo in the path.
*
* Images in Markdown are relative to the current file. To refer to an image that is located in the same folder as the `.md` file, the path should start with `./`
*/
export const MarkdownImageNotFound = {
name: 'MarkdownImageNotFound',
title: 'Image not found.',
message: (imagePath: string, fullImagePath: string | undefined) =>
`Could not find requested image \`${imagePath}\`${
fullImagePath ? ` at \`${fullImagePath}\`.` : '.'
}`,
hint: 'This is often caused by a typo in the image path. Please make sure the file exists, and is spelled correctly.',
} satisfies ErrorData;
/** /**
* @docs * @docs
* @see * @see
@ -1140,22 +1085,6 @@ export const MissingMiddlewareForInternationalization = {
"Your configuration setting `i18n.routing: 'manual'` requires you to provide your own i18n `middleware` file.", "Your configuration setting `i18n.routing: 'manual'` requires you to provide your own i18n `middleware` file.",
} satisfies ErrorData; } satisfies ErrorData;
/**
* @deprecated
* @docs
* @description
* The user tried to rewrite using a route that doesn't exist, or it emitted a runtime error during its rendering phase.
*/
export const RewriteEncounteredAnError = {
name: 'RewriteEncounteredAnError',
title:
"Astro couldn't find the route to rewrite, or if was found but it emitted an error during the rendering phase.",
message: (route: string, stack?: string) =>
`The route ${route} that you tried to render doesn't exist, or it emitted an error during the rendering phase. ${
stack ? stack : ''
}.`,
} satisfies ErrorData;
/** /**
* @docs * @docs
* @description * @description
@ -1553,20 +1482,6 @@ export const ContentSchemaContainsSlugError = {
hint: 'See https://docs.astro.build/en/guides/content-collections/ for more on the `slug` field.', hint: 'See https://docs.astro.build/en/guides/content-collections/ for more on the `slug` field.',
} satisfies ErrorData; } satisfies ErrorData;
/**
* @docs
* @message A collection queried via `getCollection()` does not exist.
* @deprecated Collections that do not exist no longer result in an error. A warning is given instead.
* @description
* When querying a collection, ensure a collection directory with the requested name exists under `src/content/`.
*/
export const CollectionDoesNotExistError = {
name: 'CollectionDoesNotExistError',
title: 'Collection does not exist',
message: (collectionName: string) =>
`The collection **${collectionName}** does not exist. Ensure a collection directory with this name exists.`,
hint: 'See https://docs.astro.build/en/guides/content-collections/ for more on creating collections.',
} satisfies ErrorData;
/** /**
* @docs * @docs
* @see * @see

View file

@ -1313,7 +1313,7 @@ export interface AstroUserConfig {
* *
* When `i18n.routing.fallback: "rewrite"` is configured, Astro will create pages that render the contents of the fallback page on the original, requested URL. * When `i18n.routing.fallback: "rewrite"` is configured, Astro will create pages that render the contents of the fallback page on the original, requested URL.
* *
* With the following configuration, if you have the file `src/pages/en/about.astro` but not `src/pages/fr/about.astro`, the `astro build` command will generate `dist/fr/about.html` with the same content as the `dist/en/index.html` page. * With the following configuration, if you have the file `src/pages/en/about.astro` but not `src/pages/fr/about.astro`, the `astro build` command will generate `dist/fr/about.html` with the same content as the `dist/en/about.html` page.
* Your site visitor will see the English version of the page at `https://example.com/fr/about/` and will not be redirected. * Your site visitor will see the English version of the page at `https://example.com/fr/about/` and will not be redirected.
* *
* ```js * ```js
@ -1333,7 +1333,7 @@ export interface AstroUserConfig {
* }) * })
* ``` * ```
*/ */
fallbackType: 'redirect' | 'rewrite'; fallbackType?: 'redirect' | 'rewrite';
/** /**
* @name i18n.routing.strategy * @name i18n.routing.strategy

View file

@ -162,17 +162,21 @@ describe('Content Layer', () => {
it('updates the store on new builds', async () => { it('updates the store on new builds', async () => {
assert.equal(json.increment.data.lastValue, 1); assert.equal(json.increment.data.lastValue, 1);
assert.equal(json.entryWithReference.data.something?.content, 'transform me');
await fixture.build(); await fixture.build();
const newJson = devalue.parse(await fixture.readFile('/collections.json')); const newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 2); assert.equal(newJson.increment.data.lastValue, 2);
assert.equal(newJson.entryWithReference.data.something?.content, 'transform me');
}); });
it('clears the store on new build with force flag', async () => { it('clears the store on new build with force flag', async () => {
let newJson = devalue.parse(await fixture.readFile('/collections.json')); let newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 2); assert.equal(newJson.increment.data.lastValue, 2);
assert.equal(newJson.entryWithReference.data.something?.content, 'transform me');
await fixture.build({ force: true }, {}); await fixture.build({ force: true }, {});
newJson = devalue.parse(await fixture.readFile('/collections.json')); newJson = devalue.parse(await fixture.readFile('/collections.json'));
assert.equal(newJson.increment.data.lastValue, 1); assert.equal(newJson.increment.data.lastValue, 1);
assert.equal(newJson.entryWithReference.data.something?.content, 'transform me');
}); });
it('clears the store on new build if the config has changed', async () => { it('clears the store on new build if the config has changed', async () => {

View file

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"astro": "workspace:*" "astro": "workspace:*"
} }
} }

View file

@ -5,6 +5,7 @@ publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
tags: [space, 90s] tags: [space, 90s]
cat: tabby cat: tabby
heroImage: "./shuttle.jpg" heroImage: "./shuttle.jpg"
something: "transform me"
--- ---
**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour) **Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)

View file

@ -78,6 +78,7 @@ const spacecraft = defineCollection({
tags: z.array(z.string()), tags: z.array(z.string()),
heroImage: image().optional(), heroImage: image().optional(),
cat: reference('cats').optional(), cat: reference('cats').optional(),
something: z.string().optional().transform(str => ({ type: 'test', content: str }))
}), }),
}); });
@ -120,9 +121,9 @@ const increment = defineCollection({
schema: async () => z.object({ schema: async () => z.object({
lastValue: z.number(), lastValue: z.number(),
lastUpdated: z.date(), lastUpdated: z.date(),
}), }),
}, },
}); });
export const collections = { blog, dogs, cats, numbers, spacecraft, increment, images }; export const collections = { blog, dogs, cats, numbers, spacecraft, increment, images };

View file

@ -17,7 +17,6 @@ export async function GET() {
const increment = await getEntry('increment', 'value'); const increment = await getEntry('increment', 'value');
const images = await getCollection('images'); const images = await getCollection('images');
return new Response( return new Response(
devalue.stringify({ devalue.stringify({
customLoader, customLoader,

View file

@ -4,6 +4,6 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"astro": "workspace:*", "astro": "workspace:*",
"@astrojs/node": "workspace:*" "@astrojs/node": "^8.3.3"
} }
} }

View file

@ -8,7 +8,7 @@
"@astrojs/vue": "workspace:*", "@astrojs/vue": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"solid-js": "^1.8.22", "solid-js": "^1.8.22",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"vue": "^3.4.38" "vue": "^3.4.38"

View file

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"astro": "workspace:*" "astro": "workspace:*"
} }
} }

View file

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@astrojs/react": "workspace:*", "@astrojs/react": "workspace:*",
"@test/ssr-prerender-chunks-test-adapter": "link:./deps/test-adapter", "@test/ssr-prerender-chunks-test-adapter": "link:./deps/test-adapter",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "workspace:*", "astro": "workspace:*",
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"@test/static-build-pkg": "workspace:*", "@test/static-build-pkg": "workspace:*",
"astro": "workspace:*" "astro": "workspace:*"
} }

View file

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@astrojs/tailwind": "workspace:*", "@astrojs/tailwind": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"tailwindcss": "^3.4.10" "tailwindcss": "^3.4.10"
} }
} }

View file

@ -7,7 +7,7 @@
"@astrojs/tailwind": "workspace:*", "@astrojs/tailwind": "workspace:*",
"astro": "workspace:*", "astro": "workspace:*",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.41", "postcss": "^8.4.43",
"tailwindcss": "^3.4.10" "tailwindcss": "^3.4.10"
} }
} }

View file

@ -12,12 +12,12 @@
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.3", "@astrojs/check": "^0.9.3",
"@astrojs/db": "workspace:*", "@astrojs/db": "workspace:*",
"@astrojs/node": "workspace:*", "@astrojs/node": "^8.3.3",
"@astrojs/react": "^3.6.2", "@astrojs/react": "^3.6.2",
"@types/react": "^18.3.4", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "workspace:*", "astro": "workspace:*",
"open-props": "^1.7.5", "open-props": "^1.7.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"simple-stack-form": "^0.1.12", "simple-stack-form": "^0.1.12",

View file

@ -64,12 +64,12 @@
"mdast-util-mdx-jsx": "^3.1.3", "mdast-util-mdx-jsx": "^3.1.3",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^6.0.0",
"rehype-pretty-code": "^0.13.2", "rehype-pretty-code": "^0.14.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-rehype": "^11.1.0", "remark-rehype": "^11.1.0",
"remark-shiki-twoslash": "^3.1.3", "remark-shiki-twoslash": "^3.1.3",
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"shiki": "^1.14.1", "shiki": "^1.16.1",
"unified": "^11.0.5", "unified": "^11.0.5",
"vite": "^5.4.2" "vite": "^5.4.2"
}, },

File diff suppressed because one or more lines are too long

View file

@ -1,38 +1,3 @@
# @astrojs/node # @astrojs/node
This adapter allows Astro to deploy your SSR site to Node targets. The Node adapter package has moved. Please see [the new repository for the Node adapter](https://github.com/withastro/adapters/tree/main/packages/node).
## Documentation
Read the [`@astrojs/node` docs][docs]
## Support
- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more!
- Check our [Astro Integration Documentation][astro-integration] for more on integrations.
- Submit bug reports and feature requests as [GitHub issues][issues].
## Contributing
This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started:
- [Contributor Manual][contributing]
- [Code of Conduct][coc]
- [Community Guide][community]
## License
MIT
Copyright (c) 2023present [Astro][astro]
[astro]: https://astro.build/
[docs]: https://docs.astro.build/en/guides/integrations-guide/node/
[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md
[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md
[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md
[discord]: https://astro.build/chat/
[issues]: https://github.com/withastro/astro/issues
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/

View file

@ -1,55 +1,7 @@
{ {
"name": "@astrojs/node", "name": "@astrojs/node",
"description": "Deploy your site to a Node.js server", "version": "0.0.0",
"version": "9.0.0-alpha.1", "private": true,
"type": "module", "keywords": [],
"types": "./dist/index.d.ts", "dont_remove": "This is a placeholder for the sake of the docs smoke test"
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/integrations/node"
},
"keywords": [
"withastro",
"astro-adapter"
],
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://docs.astro.build/en/guides/integrations-guide/node/",
"exports": {
".": "./dist/index.js",
"./server.js": "./dist/server.js",
"./preview.js": "./dist/preview.js",
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
"send": "^0.18.0",
"server-destroy": "^1.0.1"
},
"peerDependencies": {
"astro": "^5.0.0-alpha.0"
},
"devDependencies": {
"@types/node": "^18.17.8",
"@types/send": "^0.17.4",
"@types/server-destroy": "^1.0.4",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"cheerio": "1.0.0",
"express": "^4.19.2",
"node-mocks-http": "^1.15.1"
},
"publishConfig": {
"provenance": true
}
} }

View file

@ -1,81 +0,0 @@
import type { AstroAdapter, AstroIntegration } from 'astro';
import { AstroError } from 'astro/errors';
import type { Options, UserOptions } from './types.js';
export function getAdapter(options: Options): AstroAdapter {
return {
name: '@astrojs/node',
serverEntrypoint: '@astrojs/node/server.js',
previewEntrypoint: '@astrojs/node/preview.js',
exports: ['handler', 'startServer', 'options'],
args: options,
supportedAstroFeatures: {
hybridOutput: 'stable',
staticOutput: 'stable',
serverOutput: 'stable',
assets: {
supportKind: 'stable',
isSharpCompatible: true,
},
i18nDomains: 'experimental',
envGetSecret: 'stable',
},
};
}
// TODO: remove once we don't use a TLA anymore
async function shouldExternalizeAstroEnvSetup() {
try {
await import('astro/env/setup');
return false;
} catch {
return true;
}
}
export default function createIntegration(userOptions: UserOptions): AstroIntegration {
if (!userOptions?.mode) {
throw new AstroError(`Setting the 'mode' option is required.`);
}
let _options: Options;
return {
name: '@astrojs/node',
hooks: {
'astro:config:setup': async ({ updateConfig, config }) => {
updateConfig({
image: {
endpoint: config.image.endpoint ?? 'astro/assets/endpoint/node',
},
vite: {
ssr: {
noExternal: ['@astrojs/node'],
...((await shouldExternalizeAstroEnvSetup())
? {
external: ['astro/env/setup'],
}
: {}),
},
},
});
},
'astro:config:done': ({ setAdapter, config, logger }) => {
_options = {
...userOptions,
client: config.build.client?.toString(),
server: config.build.server?.toString(),
host: config.server.host,
port: config.server.port,
assets: config.build.assets,
};
setAdapter(getAdapter(_options));
if (config.output === 'static') {
logger.warn(
`\`output: "server"\` or \`output: "hybrid"\` is required to use this adapter.`,
);
}
},
},
};
}

View file

@ -1,88 +0,0 @@
import type http from 'node:http';
import https from 'node:https';
import type { AddressInfo } from 'node:net';
import os from 'node:os';
import type { AstroIntegrationLogger } from 'astro';
import type { Options } from './types.js';
export async function logListeningOn(
logger: AstroIntegrationLogger,
server: http.Server | https.Server,
options: Pick<Options, 'host'>,
) {
await new Promise<void>((resolve) => server.once('listening', resolve));
const protocol = server instanceof https.Server ? 'https' : 'http';
// Allow to provide host value at runtime
const host = getResolvedHostForHttpServer(
process.env.HOST !== undefined && process.env.HOST !== '' ? process.env.HOST : options.host,
);
const { port } = server.address() as AddressInfo;
const address = getNetworkAddress(protocol, host, port);
if (host === undefined) {
logger.info(
`Server listening on \n local: ${address.local[0]} \t\n network: ${address.network[0]}\n`,
);
} else {
logger.info(`Server listening on ${address.local[0]}`);
}
}
function getResolvedHostForHttpServer(host: string | boolean) {
if (host === false) {
// Use a secure default
return 'localhost';
} else if (host === true) {
// If passed --host in the CLI without arguments
return undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs)
} else {
return host;
}
}
interface NetworkAddressOpt {
local: string[];
network: string[];
}
const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']);
// this code from vite https://github.com/vitejs/vite/blob/d09bbd093a4b893e78f0bbff5b17c7cf7821f403/packages/vite/src/node/utils.ts#L892-L914
export function getNetworkAddress(
protocol: 'http' | 'https' = 'http',
hostname: string | undefined,
port: number,
base?: string,
) {
const NetworkAddress: NetworkAddressOpt = {
local: [],
network: [],
};
Object.values(os.networkInterfaces())
.flatMap((nInterface) => nInterface ?? [])
.filter(
(detail) =>
detail &&
detail.address &&
(detail.family === 'IPv4' ||
// @ts-expect-error Node 18.0 - 18.3 returns number
detail.family === 4),
)
.forEach((detail) => {
let host = detail.address.replace(
'127.0.0.1',
hostname === undefined || wildcardHosts.has(hostname) ? 'localhost' : hostname,
);
// ipv6 host
if (host.includes(':')) {
host = `[${host}]`;
}
const url = `${protocol}://${host}:${port}${base ? base : ''}`;
if (detail.address.includes('127.0.0.1')) {
NetworkAddress.local.push(url);
} else {
NetworkAddress.network.push(url);
}
});
return NetworkAddress;
}

View file

@ -1,41 +0,0 @@
import type { NodeApp } from 'astro/app/node';
import { createAppHandler } from './serve-app.js';
import type { RequestHandler } from './types.js';
/**
* Creates a middleware that can be used with Express, Connect, etc.
*
* Similar to `createAppHandler` but can additionally be placed in the express
* chain as an error middleware.
*
* https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling
*/
export default function createMiddleware(app: NodeApp): RequestHandler {
const handler = createAppHandler(app);
const logger = app.getAdapterLogger();
// using spread args because express trips up if the function's
// stringified body includes req, res, next, locals directly
return async function (...args) {
// assume normal invocation at first
const [req, res, next, locals] = args;
// short circuit if it is an error invocation
if (req instanceof Error) {
const error = req;
if (next) {
return next(error);
} else {
throw error;
}
}
try {
await handler(req, res, next, locals);
} catch (err) {
logger.error(`Could not render ${req.url}`);
console.error(err);
if (!res.headersSent) {
res.writeHead(500, `Server error`);
res.end();
}
}
};
}

View file

@ -1,61 +0,0 @@
import { fileURLToPath } from 'node:url';
import type { CreatePreviewServer } from 'astro';
import { AstroError } from 'astro/errors';
import { logListeningOn } from './log-listening-on.js';
import type { createExports } from './server.js';
import { createServer } from './standalone.js';
type ServerModule = ReturnType<typeof createExports>;
type MaybeServerModule = Partial<ServerModule>;
const createPreviewServer: CreatePreviewServer = async function (preview) {
let ssrHandler: ServerModule['handler'];
let options: ServerModule['options'];
try {
process.env.ASTRO_NODE_AUTOSTART = 'disabled';
const ssrModule: MaybeServerModule = await import(preview.serverEntrypoint.toString());
if (typeof ssrModule.handler === 'function') {
ssrHandler = ssrModule.handler;
options = ssrModule.options!;
} else {
throw new AstroError(
`The server entrypoint doesn't have a handler. Are you sure this is the right file?`,
);
}
} catch (err) {
if ((err as any).code === 'ERR_MODULE_NOT_FOUND') {
throw new AstroError(
`The server entrypoint ${fileURLToPath(
preview.serverEntrypoint,
)} does not exist. Have you ran a build yet?`,
);
} else {
throw err;
}
}
const host = preview.host ?? 'localhost';
const port = preview.port ?? 4321;
const server = createServer(ssrHandler, host, port);
// If user specified custom headers append a listener
// to the server to add those headers to response
if (preview.headers) {
server.server.addListener('request', (_, res) => {
if (res.statusCode === 200) {
for (const [name, value] of Object.entries(preview.headers ?? {})) {
if (value) res.setHeader(name, value);
}
}
});
}
logListeningOn(preview.logger, server.server, options);
await new Promise<void>((resolve, reject) => {
server.server.once('listening', resolve);
server.server.once('error', reject);
server.server.listen(port, host);
});
return server;
};
export { createPreviewServer as default };

View file

@ -1,52 +0,0 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import { NodeApp } from 'astro/app/node';
import type { RequestHandler } from './types.js';
/**
* Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware.
* If the next callback is provided, it will be called if the request does not have a matching route.
* Intended to be used in both standalone and middleware mode.
*/
export function createAppHandler(app: NodeApp): RequestHandler {
/**
* Keep track of the current request path using AsyncLocalStorage.
* Used to log unhandled rejections with a helpful message.
*/
const als = new AsyncLocalStorage<string>();
const logger = app.getAdapterLogger();
process.on('unhandledRejection', (reason) => {
const requestUrl = als.getStore();
logger.error(`Unhandled rejection while rendering ${requestUrl}`);
console.error(reason);
});
return async (req, res, next, locals) => {
let request: Request;
try {
request = NodeApp.createRequest(req);
} catch (err) {
logger.error(`Could not render ${req.url}`);
console.error(err);
res.statusCode = 500;
res.end('Internal Server Error');
return;
}
const routeData = app.match(request);
if (routeData) {
const response = await als.run(request.url, () =>
app.render(request, {
addCookieHeader: true,
locals,
routeData,
}),
);
await NodeApp.writeResponse(response, res);
} else if (next) {
return next();
} else {
const response = await app.render(req);
await NodeApp.writeResponse(response, res);
}
};
}

View file

@ -1,125 +0,0 @@
import fs from 'node:fs';
import type { IncomingMessage, ServerResponse } from 'node:http';
import path from 'node:path';
import url from 'node:url';
import type { NodeApp } from 'astro/app/node';
import send from 'send';
import type { Options } from './types.js';
// check for a dot followed by a extension made up of lowercase characters
const isSubresourceRegex = /.+\.[a-z]+$/i;
/**
* Creates a Node.js http listener for static files and prerendered pages.
* In standalone mode, the static handler is queried first for the static files.
* If one matching the request path is not found, it relegates to the SSR handler.
* Intended to be used only in the standalone mode.
*/
export function createStaticHandler(app: NodeApp, options: Options) {
const client = resolveClientDir(options);
/**
* @param ssr The SSR handler to be called if the static handler does not find a matching file.
*/
return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => {
if (req.url) {
const [urlPath, urlQuery] = req.url.split('?');
const filePath = path.join(client, app.removeBase(urlPath));
let pathname: string;
let isDirectory = false;
try {
isDirectory = fs.lstatSync(filePath).isDirectory();
} catch {}
const { trailingSlash = 'ignore' } = options;
const hasSlash = urlPath.endsWith('/');
switch (trailingSlash) {
case 'never':
if (isDirectory && urlPath != '/' && hasSlash) {
pathname = urlPath.slice(0, -1) + (urlQuery ? '?' + urlQuery : '');
res.statusCode = 301;
res.setHeader('Location', pathname);
return res.end();
} else pathname = urlPath;
// intentionally fall through
case 'ignore':
{
if (isDirectory && !hasSlash) {
pathname = urlPath + '/index.html';
} else pathname = urlPath;
}
break;
case 'always':
// trailing slash is not added to "subresources"
if (!hasSlash && !isSubresourceRegex.test(urlPath)) {
pathname = urlPath + '/' + (urlQuery ? '?' + urlQuery : '');
res.statusCode = 301;
res.setHeader('Location', pathname);
return res.end();
} else pathname = urlPath;
break;
}
// app.removeBase sometimes returns a path without a leading slash
pathname = prependForwardSlash(app.removeBase(pathname));
const stream = send(req, pathname, {
root: client,
dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny',
});
let forwardError = false;
stream.on('error', (err) => {
if (forwardError) {
console.error(err.toString());
res.writeHead(500);
res.end('Internal server error');
return;
}
// File not found, forward to the SSR handler
ssr();
});
stream.on('headers', (_res: ServerResponse) => {
// assets in dist/_astro are hashed and should get the immutable header
if (pathname.startsWith(`/${options.assets}/`)) {
// This is the "far future" cache header, used for static files whose name includes their digest hash.
// 1 year (31,536,000 seconds) is convention.
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
_res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
});
stream.on('file', () => {
forwardError = true;
});
stream.pipe(res);
} else {
ssr();
}
};
}
function resolveClientDir(options: Options) {
const clientURLRaw = new URL(options.client);
const serverURLRaw = new URL(options.server);
const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw));
// walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively.
const serverFolder = path.basename(options.server);
let serverEntryFolderURL = path.dirname(import.meta.url);
while (!serverEntryFolderURL.endsWith(serverFolder)) {
serverEntryFolderURL = path.dirname(serverEntryFolderURL);
}
const serverEntryURL = serverEntryFolderURL + '/entry.mjs';
const clientURL = new URL(appendForwardSlash(rel), serverEntryURL);
const client = url.fileURLToPath(clientURL);
return client;
}
function prependForwardSlash(pth: string) {
return pth.startsWith('/') ? pth : '/' + pth;
}
function appendForwardSlash(pth: string) {
return pth.endsWith('/') ? pth : pth + '/';
}

View file

@ -1,31 +0,0 @@
import type { SSRManifest } from 'astro';
import { NodeApp, applyPolyfills } from 'astro/app/node';
import { setGetEnv } from 'astro/env/setup';
import createMiddleware from './middleware.js';
import { createStandaloneHandler } from './standalone.js';
import startServer from './standalone.js';
import type { Options } from './types.js';
// This needs to run first because some internals depend on `crypto`
applyPolyfills();
setGetEnv((key) => process.env[key]);
export function createExports(manifest: SSRManifest, options: Options) {
const app = new NodeApp(manifest);
options.trailingSlash = manifest.trailingSlash;
return {
options: options,
handler:
options.mode === 'middleware' ? createMiddleware(app) : createStandaloneHandler(app, options),
startServer: () => startServer(app, options),
};
}
export function start(manifest: SSRManifest, options: Options) {
if (options.mode !== 'standalone' || process.env.ASTRO_NODE_AUTOSTART === 'disabled') {
return;
}
const app = new NodeApp(manifest);
startServer(app, options);
}

View file

@ -1,92 +0,0 @@
import fs from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import type { PreviewServer } from 'astro';
import type { NodeApp } from 'astro/app/node';
import enableDestroy from 'server-destroy';
import { logListeningOn } from './log-listening-on.js';
import { createAppHandler } from './serve-app.js';
import { createStaticHandler } from './serve-static.js';
import type { Options } from './types.js';
// Used to get Host Value at Runtime
export const hostOptions = (host: Options['host']): string => {
if (typeof host === 'boolean') {
return host ? '0.0.0.0' : 'localhost';
}
return host;
};
export default function standalone(app: NodeApp, options: Options) {
const port = process.env.PORT ? Number(process.env.PORT) : options.port ?? 8080;
const host = process.env.HOST ?? hostOptions(options.host);
const handler = createStandaloneHandler(app, options);
const server = createServer(handler, host, port);
server.server.listen(port, host);
if (process.env.ASTRO_NODE_LOGGING !== 'disabled') {
logListeningOn(app.getAdapterLogger(), server.server, options);
}
return {
server,
done: server.closed(),
};
}
// also used by server entrypoint
export function createStandaloneHandler(app: NodeApp, options: Options) {
const appHandler = createAppHandler(app);
const staticHandler = createStaticHandler(app, options);
return (req: http.IncomingMessage, res: http.ServerResponse) => {
try {
// validate request path
decodeURI(req.url!);
} catch {
res.writeHead(400);
res.end('Bad request.');
return;
}
staticHandler(req, res, () => appHandler(req, res));
};
}
// also used by preview entrypoint
export function createServer(listener: http.RequestListener, host: string, port: number) {
let httpServer: http.Server | https.Server;
if (process.env.SERVER_CERT_PATH && process.env.SERVER_KEY_PATH) {
httpServer = https.createServer(
{
key: fs.readFileSync(process.env.SERVER_KEY_PATH),
cert: fs.readFileSync(process.env.SERVER_CERT_PATH),
},
listener,
);
} else {
httpServer = http.createServer(listener);
}
enableDestroy(httpServer);
// Resolves once the server is closed
const closed = new Promise<void>((resolve, reject) => {
httpServer.addListener('close', resolve);
httpServer.addListener('error', reject);
});
const previewable = {
host,
port,
closed() {
return closed;
},
async stop() {
await new Promise((resolve, reject) => {
httpServer.destroy((err) => (err ? reject(err) : resolve(undefined)));
});
},
} satisfies PreviewServer;
return {
server: httpServer,
...previewable,
};
}

View file

@ -1,39 +0,0 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { SSRManifest } from 'astro';
import type { NodeApp } from 'astro/app/node';
export interface UserOptions {
/**
* Specifies the mode that the adapter builds to.
*
* - 'middleware' - Build to middleware, to be used within another Node.js server, such as Express.
* - 'standalone' - Build to a standalone server. The server starts up just by running the built script.
*/
mode: 'middleware' | 'standalone';
}
export interface Options extends UserOptions {
host: string | boolean;
port: number;
server: string;
client: string;
assets: string;
trailingSlash?: SSRManifest['trailingSlash'];
}
export interface CreateServerOptions {
app: NodeApp;
assets: string;
client: URL;
port: number;
host: string | undefined;
removeBase: (pathname: string) => string;
}
export type RequestHandler = (...args: RequestHandlerParams) => void | Promise<void>;
export type RequestHandlerParams = [
req: IncomingMessage,
res: ServerResponse,
next?: (err?: unknown) => void,
locals?: object,
];

View file

@ -1,153 +0,0 @@
import * as assert from 'node:assert/strict';
import crypto from 'node:crypto';
import { after, before, describe, it } from 'node:test';
import nodejs from '../dist/index.js';
import { createRequestAndResponse, loadFixture } from './test-utils.js';
describe('API routes', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
/** @type {import('../../../astro/src/types/public/preview.js').PreviewServer} */
let previewServer;
/** @type {URL} */
let baseUri;
before(async () => {
fixture = await loadFixture({
root: './fixtures/api-route/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
});
await fixture.build();
previewServer = await fixture.preview();
baseUri = new URL(`http://${previewServer.host ?? 'localhost'}:${previewServer.port}/`);
});
after(() => previewServer.stop());
it('Can get the request body', async () => {
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
let { req, res, done } = createRequestAndResponse({
method: 'POST',
url: '/recipes',
});
req.once('async_iterator', () => {
req.send(JSON.stringify({ id: 2 }));
});
handler(req, res);
let [buffer] = await done;
let json = JSON.parse(buffer.toString('utf-8'));
assert.equal(json.length, 1);
assert.equal(json[0].name, 'Broccoli Soup');
});
it('Can get binary data', async () => {
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
let { req, res, done } = createRequestAndResponse({
method: 'POST',
url: '/binary',
});
req.once('async_iterator', () => {
req.send(Buffer.from(new Uint8Array([1, 2, 3, 4, 5])));
});
handler(req, res);
let [out] = await done;
let arr = Array.from(new Uint8Array(out.buffer));
assert.deepEqual(arr, [5, 4, 3, 2, 1]);
});
it('Can post large binary data', async () => {
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
let { req, res, done } = createRequestAndResponse({
method: 'POST',
url: '/hash',
});
handler(req, res);
let expectedDigest = null;
req.once('async_iterator', () => {
// Send 256MB of garbage data in 256KB chunks. This should be fast (< 1sec).
let remainingBytes = 256 * 1024 * 1024;
const chunkSize = 256 * 1024;
const hash = crypto.createHash('sha256');
while (remainingBytes > 0) {
const size = Math.min(remainingBytes, chunkSize);
const chunk = Buffer.alloc(size, Math.floor(Math.random() * 256));
hash.update(chunk);
req.emit('data', chunk);
remainingBytes -= size;
}
req.emit('end');
expectedDigest = hash.digest();
});
let [out] = await done;
assert.deepEqual(new Uint8Array(out.buffer), new Uint8Array(expectedDigest));
});
it('Can bail on streaming', async () => {
const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs');
let { req, res, done } = createRequestAndResponse({
url: '/streaming',
});
let locals = { cancelledByTheServer: false };
handler(req, res, () => {}, locals);
req.send();
await new Promise((resolve) => setTimeout(resolve, 500));
res.emit('close');
await done;
assert.deepEqual(locals, { cancelledByTheServer: true });
});
it('Can respond with SSR redirect', async () => {
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
const response = await fetch(new URL('/redirect', baseUri), {
redirect: 'manual',
signal: controller.signal,
});
assert.equal(response.status, 302);
assert.equal(response.headers.get('location'), '/destination');
});
it('Can respond with Astro.redirect', async () => {
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
const response = await fetch(new URL('/astro-redirect', baseUri), {
redirect: 'manual',
signal: controller.signal,
});
assert.equal(response.status, 303);
assert.equal(response.headers.get('location'), '/destination');
});
it('Can respond with Response.redirect', async () => {
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
const response = await fetch(new URL('/response-redirect', baseUri), {
redirect: 'manual',
signal: controller.signal,
});
assert.equal(response.status, 307);
assert.equal(response.headers.get('location'), String(new URL('/destination', baseUri)));
});
});

View file

@ -1,44 +0,0 @@
import * as assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import nodejs from '../dist/index.js';
import { loadFixture } from './test-utils.js';
describe('Assets', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let devPreview;
before(async () => {
fixture = await loadFixture({
root: './fixtures/image/',
output: 'server',
adapter: nodejs({ mode: 'standalone' }),
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
await fixture.build();
devPreview = await fixture.preview();
});
after(async () => {
await devPreview.stop();
});
it('Assets within the _astro folder should be given immutable headers', async () => {
let response = await fixture.fetch('/text-file');
let cacheControl = response.headers.get('cache-control');
assert.equal(cacheControl, null);
const html = await response.text();
const $ = cheerio.load(html);
// Fetch the asset
const fileURL = $('a').attr('href');
response = await fixture.fetch(fileURL);
cacheControl = response.headers.get('cache-control');
assert.equal(cacheControl, 'public, max-age=31536000, immutable');
});
});

View file

@ -1,49 +0,0 @@
import * as assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import nodejs from '../dist/index.js';
import { loadFixture } from './test-utils.js';
describe('Bad URLs', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let devPreview;
before(async () => {
fixture = await loadFixture({
root: './fixtures/bad-urls/',
output: 'server',
adapter: nodejs({ mode: 'standalone' }),
});
await fixture.build();
devPreview = await fixture.preview();
});
after(async () => {
await devPreview.stop();
});
it('Does not crash on bad urls', async () => {
const weirdURLs = [
'/\\xfs.bxss.me%3Fastrojs.com/hello-world',
'/asdasdasd@ax_zX=.zxczas🐥%/úadasd000%/',
'%',
'%80',
'%c',
'%c0%80',
'%20foobar%',
];
const statusCodes = [400, 404, 500];
for (const weirdUrl of weirdURLs) {
const fetchResult = await fixture.fetch(weirdUrl);
assert.equal(
statusCodes.includes(fetchResult.status),
true,
`${weirdUrl} returned something else than 400, 404, or 500`,
);
}
const stillWork = await fixture.fetch('/');
const text = await stillWork.text();
assert.equal(text, '<!DOCTYPE html>Hello!');
});
});

View file

@ -1,45 +0,0 @@
import * as assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import nodejs from '../dist/index.js';
import { createRequestAndResponse, loadFixture } from './test-utils.js';
describe('Encoded Pathname', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/encoded/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
});
await fixture.build();
});
it('Can get an Astro file', async () => {
const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs');
let { req, res, text } = createRequestAndResponse({
url: '/什么',
});
handler(req, res);
req.send();
const html = await text();
assert.equal(html.includes('什么</h1>'), true);
});
it('Can get a Markdown file', async () => {
const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs');
let { req, res, text } = createRequestAndResponse({
url: '/blog/什么',
});
handler(req, res);
req.send();
const html = await text();
assert.equal(html.includes('什么</h1>'), true);
});
});

View file

@ -1,91 +0,0 @@
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import { Worker } from 'node:worker_threads';
import * as cheerio from 'cheerio';
import nodejs from '../dist/index.js';
import { loadFixture } from './test-utils.js';
describe('Errors', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/errors/',
output: 'server',
adapter: nodejs({ mode: 'standalone' }),
});
await fixture.build();
});
let devPreview;
before(async () => {
// The two tests that need the server to run are skipped
// devPreview = await fixture.preview();
});
after(async () => {
await devPreview?.stop();
});
it('stays alive after offshoot promise rejections', async () => {
// this test needs to happen in a worker because node test runner adds a listener for unhandled rejections in the main thread
const url = new URL('./fixtures/errors/dist/server/entry.mjs', import.meta.url);
const worker = new Worker(fileURLToPath(url), {
type: 'module',
env: { ASTRO_NODE_LOGGING: 'enabled' },
});
await new Promise((resolve, reject) => {
worker.stdout.on('data', (data) => {
setTimeout(() => reject('Server took too long to start'), 1000);
if (data.toString().includes('Server listening on http://localhost:4321')) resolve();
});
});
await fetch('http://localhost:4321/offshoot-promise-rejection');
// if there was a crash, it becomes an error here
await worker.terminate();
});
it(
'rejected promise in template',
{ skip: true, todo: 'Review the response from the in-stream' },
async () => {
const res = await fixture.fetch('/in-stream');
const html = await res.text();
const $ = cheerio.load(html);
assert.equal($('p').text().trim(), 'Internal server error');
},
);
it(
'generator that throws called in template',
{ skip: true, todo: 'Review the response from the generator' },
async () => {
const result = ['<!DOCTYPE html><h1>Astro</h1> 1', 'Internal server error'];
/** @type {Response} */
const res = await fixture.fetch('/generator');
const reader = res.body.getReader();
const decoder = new TextDecoder();
const chunk1 = await reader.read();
const chunk2 = await reader.read();
const chunk3 = await reader.read();
assert.equal(chunk1.done, false);
console.log(chunk1);
console.log(chunk2);
console.log(chunk3);
if (chunk2.done) {
assert.equal(decoder.decode(chunk1.value), result.join(''));
} else if (chunk3.done) {
assert.equal(decoder.decode(chunk1.value), result[0]);
assert.equal(decoder.decode(chunk2.value), result[1]);
} else {
throw new Error('The response should take at most 2 chunks.');
}
},
);
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-api-route",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,3 +0,0 @@
---
return Astro.redirect('/destination', 303);
---

View file

@ -1,11 +0,0 @@
export async function POST({ request }: { request: Request }) {
let body = await request.arrayBuffer();
let data = new Uint8Array(body);
let r = data.reverse();
return new Response(r, {
headers: {
'Content-Type': 'application/octet-stream'
}
});
}

View file

@ -1,16 +0,0 @@
import crypto from 'node:crypto';
export async function POST({ request }: { request: Request }) {
const hash = crypto.createHash('sha256');
const iterable = request.body as unknown as AsyncIterable<Uint8Array>;
for await (const chunk of iterable) {
hash.update(chunk);
}
return new Response(hash.digest(), {
headers: {
'Content-Type': 'application/octet-stream'
}
});
}

View file

@ -1,24 +0,0 @@
export async function POST({ request }) {
let body = await request.json();
const recipes = [
{
id: 1,
name: 'Potato Soup'
},
{
id: 2,
name: 'Broccoli Soup'
}
];
let out = recipes.filter(r => {
return r.id === body.id;
});
return new Response(JSON.stringify(out), {
headers: {
'Content-Type': 'application/json'
}
});
}

View file

@ -1,5 +0,0 @@
import { APIContext } from 'astro';
export async function GET({ redirect }: APIContext) {
return redirect('/destination');
}

View file

@ -1,5 +0,0 @@
import { APIContext } from 'astro';
export async function GET({ url: requestUrl }: APIContext) {
return Response.redirect(new URL('/destination', requestUrl), 307);
}

View file

@ -1,22 +0,0 @@
export const GET = ({ locals }) => {
let sentChunks = 0;
const readableStream = new ReadableStream({
async pull(controller) {
if (sentChunks === 3) return controller.close();
else sentChunks++;
await new Promise(resolve => setTimeout(resolve, 1000));
controller.enqueue(new TextEncoder().encode('hello\n'));
},
cancel() {
locals.cancelledByTheServer = true;
}
});
return new Response(readableStream, {
headers: {
"Content-Type": "text/event-stream"
}
});
}

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-badurls",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-encoded",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1 +0,0 @@
<h1>什么</h1>

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-errors",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,11 +0,0 @@
---
function * generator () {
yield 1
throw Error('ohnoes')
}
---
<h1>Astro</h1>
{generator()}
<footer>
Footer
</footer>

View file

@ -1,13 +0,0 @@
---
---
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
<p>
{Promise.reject('Error in the stream')}
</p>
</body>
</html>

View file

@ -1,2 +0,0 @@
{new Promise(async _ => (await {}, Astro.props.undefined.alsoAPropertyOfUndefined))}
{Astro.props.undefined.propertyOfUndefined}

View file

@ -1,9 +0,0 @@
{
"name": "@test/nodejs-headers",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

View file

@ -1,5 +0,0 @@
---
Astro.cookies.set('from1', 'astro1');
Astro.cookies.set('from2', 'astro2');
---
<p>hello world</p>

View file

@ -1,4 +0,0 @@
---
Astro.cookies.set('from1', 'astro1');
---
<p>hello world</p>

View file

@ -1,7 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=response1');
Astro.response.headers.append('set-cookie', 'from2=response2');
Astro.cookies.set('from3', 'astro1');
Astro.cookies.set('from4', 'astro2');
---
<p>hello world</p>

View file

@ -1,5 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=response1');
Astro.cookies.set('from1', 'astro1');
---
<p>hello world</p>

View file

@ -1,5 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=value1');
Astro.response.headers.append('set-cookie', 'from2=value2');
---
<p>hello world</p>

View file

@ -1,4 +0,0 @@
---
Astro.response.headers.append('set-cookie', 'from1=value1');
---
<p>hello world</p>

View file

@ -1,9 +0,0 @@
import type { APIContext } from 'astro';
export async function GET({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
cookies.set('from1', 'astro1');
cookies.set('from2', 'astro2');
return new Response('hello world', { headers });
}

View file

@ -1,8 +0,0 @@
import type { APIContext } from 'astro';
export async function GET({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
cookies.set('from1', 'astro1');
return new Response('hello world', { headers });
}

View file

@ -1,11 +0,0 @@
import type { APIContext } from 'astro';
export async function GET({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('set-cookie', 'from1=response1');
headers.append('set-cookie', 'from2=response2');
cookies.set('from3', 'astro1');
cookies.set('from4', 'astro2');
return new Response('hello world', { headers });
}

View file

@ -1,9 +0,0 @@
import type { APIContext } from 'astro';
export async function GET({ request, cookies }: APIContext) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('set-cookie', 'from1=response1');
cookies.set('from1', 'astro1');
return new Response('hello world', { headers });
}

View file

@ -1,11 +0,0 @@
export async function GET({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('x-SINGLE', 'single');
headers.append('X-triple', 'one');
headers.append('x-Triple', 'two');
headers.append('x-TRIPLE', 'three');
headers.append('SET-cookie', 'hello1=world1');
headers.append('Set-Cookie', 'hello2=world2');
return new Response('hello world', { headers });
}

View file

@ -1,7 +0,0 @@
export async function GET({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('Set-Cookie', 'hello1=world1');
headers.append('SET-COOKIE', 'hello2=world2');
return new Response('hello world', { headers });
}

View file

@ -1,6 +0,0 @@
export async function GET({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('Set-Cookie', 'hello1=world1');
return new Response('hello world', { headers });
}

View file

@ -1,4 +0,0 @@
export async function GET({ request }: { request: Request }) {
const headers = new Headers();
return new Response('hello world', { headers });
}

View file

@ -1,3 +0,0 @@
export async function GET({ request }: { request: Request }) {
return new Response('hello world', { headers: undefined });
}

View file

@ -1,6 +0,0 @@
export async function GET({ request }: { request: Request }) {
const headers = new Headers();
headers.append('content-type', 'text/plain;charset=utf-8');
headers.append('X-HELLO', 'world');
return new Response('hello world', { headers });
}

View file

@ -1,13 +0,0 @@
{
"name": "@test/nodejs-image",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
},
"scripts": {
"build": "astro build",
"preview": "astro preview"
}
}

View file

@ -1 +0,0 @@
this is a text file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

View file

@ -1,6 +0,0 @@
---
import { Image } from "astro:assets";
import penguin from "../assets/some_penguin.png";
---
<Image src={penguin} alt="Penguins" width={50} />

View file

@ -1,14 +0,0 @@
---
import txt from '../assets/file.txt?url';
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>
<main>
<a href={txt} download>Download text file</a>
</main>
</body>
</html>

View file

@ -1,9 +0,0 @@
{
"name": "@test/locals",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}

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