Migrate error messages to new format (and misc error improvements) (#5316)
* Migrate messages to errors data file * Move errors to new 'error database' for easier improvements * Fix tests * Add changeset * Remove unnecessary console.log * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update packages/astro/src/core/errors/errors-data.ts Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Misc punctuations fixes and additions to README Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
faa01cec73
commit
a780f2595d
38 changed files with 797 additions and 382 deletions
5
.changeset/curly-experts-think.md
Normal file
5
.changeset/curly-experts-think.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improved error messages descriptions and hints to be more informative
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { testFactory, getErrorOverlayMessage } from './test-utils.js';
|
import { getErrorOverlayMessage, testFactory } from './test-utils.js';
|
||||||
|
|
||||||
const test = testFactory({ root: './fixtures/errors/' });
|
const test = testFactory({ root: './fixtures/errors/' });
|
||||||
|
|
||||||
|
@ -38,7 +38,9 @@ test.describe('Error display', () => {
|
||||||
await page.goto(astro.resolveUrl('/import-not-found'));
|
await page.goto(astro.resolveUrl('/import-not-found'));
|
||||||
|
|
||||||
const message = await getErrorOverlayMessage(page);
|
const message = await getErrorOverlayMessage(page);
|
||||||
expect(message).toMatch('Could not import "../abc.astro"');
|
expect(message).toMatch(
|
||||||
|
'Could not import "../abc.astro".\n\nThis is often caused by a typo in the import path. Please make sure the file exists.'
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Wait for page reload
|
// Wait for page reload
|
||||||
|
|
|
@ -123,7 +123,6 @@
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepmerge-ts": "^4.2.2",
|
"deepmerge-ts": "^4.2.2",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"eol": "^0.9.1",
|
|
||||||
"es-module-lexer": "^0.10.5",
|
"es-module-lexer": "^0.10.5",
|
||||||
"esbuild": "^0.14.43",
|
"esbuild": "^0.14.43",
|
||||||
"execa": "^6.1.0",
|
"execa": "^6.1.0",
|
||||||
|
@ -199,7 +198,8 @@
|
||||||
"remark-code-titles": "^0.1.2",
|
"remark-code-titles": "^0.1.2",
|
||||||
"sass": "^1.52.2",
|
"sass": "^1.52.2",
|
||||||
"srcset-parse": "^1.1.0",
|
"srcset-parse": "^1.1.0",
|
||||||
"unified": "^10.1.2"
|
"unified": "^10.1.2",
|
||||||
|
"eol": "^0.9.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.12.0",
|
"node": "^14.18.0 || >=16.12.0",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import * as colors from 'kleur/colors';
|
import * as colors from 'kleur/colors';
|
||||||
import { pathToFileURL } from 'url';
|
|
||||||
import type { Arguments as Flags } from 'yargs-parser';
|
import type { Arguments as Flags } from 'yargs-parser';
|
||||||
import yargs from 'yargs-parser';
|
import yargs from 'yargs-parser';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -94,9 +93,7 @@ async function handleConfigError(
|
||||||
if (path) {
|
if (path) {
|
||||||
error(logging, 'astro', `Unable to load ${colors.bold(path)}\n`);
|
error(logging, 'astro', `Unable to load ${colors.bold(path)}\n`);
|
||||||
}
|
}
|
||||||
console.error(
|
console.error(formatErrorMessage(collectErrorMetadata(e)) + '\n');
|
||||||
formatErrorMessage(collectErrorMetadata(e, path ? pathToFileURL(path) : undefined)) + '\n'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
runHookConfigSetup,
|
runHookConfigSetup,
|
||||||
} from '../../integrations/index.js';
|
} from '../../integrations/index.js';
|
||||||
import { createVite } from '../create-vite.js';
|
import { createVite } from '../create-vite.js';
|
||||||
import { enhanceViteSSRError } from '../errors/dev/index.js';
|
|
||||||
import { debug, info, levels, timerMessage } from '../logger/core.js';
|
import { debug, info, levels, timerMessage } from '../logger/core.js';
|
||||||
import { apply as applyPolyfill } from '../polyfill.js';
|
import { apply as applyPolyfill } from '../polyfill.js';
|
||||||
import { RouteCache } from '../render/route-cache.js';
|
import { RouteCache } from '../render/route-cache.js';
|
||||||
|
@ -169,7 +168,7 @@ class AstroBuilder {
|
||||||
try {
|
try {
|
||||||
await this.build(setupData);
|
await this.build(setupData);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
throw enhanceViteSSRError(_err as Error);
|
throw _err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { prependForwardSlash } from '../../core/path.js';
|
||||||
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
import { isModeServerWithNoAdapter } from '../../core/util.js';
|
||||||
import { runHookBuildSetup } from '../../integrations/index.js';
|
import { runHookBuildSetup } from '../../integrations/index.js';
|
||||||
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { info } from '../logger/core.js';
|
import { info } from '../logger/core.js';
|
||||||
import { getOutDirWithinCwd } from './common.js';
|
import { getOutDirWithinCwd } from './common.js';
|
||||||
import { generatePages } from './generate.js';
|
import { generatePages } from './generate.js';
|
||||||
|
@ -26,19 +27,9 @@ import { injectManifest, vitePluginSSR } from './vite-plugin-ssr.js';
|
||||||
export async function staticBuild(opts: StaticBuildOptions) {
|
export async function staticBuild(opts: StaticBuildOptions) {
|
||||||
const { allPages, settings } = opts;
|
const { allPages, settings } = opts;
|
||||||
|
|
||||||
// Verify this app is buildable.
|
// Make sure we have an adapter before building
|
||||||
if (isModeServerWithNoAdapter(opts.settings)) {
|
if (isModeServerWithNoAdapter(opts.settings)) {
|
||||||
throw new Error(`Cannot use \`output: 'server'\` without an adapter.
|
throw new AstroError(AstroErrorData.NoAdapterInstalled);
|
||||||
Install and configure the appropriate server adapter for your final deployment.
|
|
||||||
Learn more: https://docs.astro.build/en/guides/server-side-rendering/
|
|
||||||
|
|
||||||
// Example: astro.config.js
|
|
||||||
import netlify from '@astrojs/netlify';
|
|
||||||
export default {
|
|
||||||
output: 'server',
|
|
||||||
adapter: netlify(),
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pages to be built for rendering purposes.
|
// The pages to be built for rendering purposes.
|
||||||
|
|
|
@ -3,8 +3,8 @@ import type { ResolvedConfig } from 'vite';
|
||||||
import type { AstroConfig } from '../../@types/astro';
|
import type { AstroConfig } from '../../@types/astro';
|
||||||
|
|
||||||
import { transform } from '@astrojs/compiler';
|
import { transform } from '@astrojs/compiler';
|
||||||
import { AstroErrorCodes } from '../errors/codes.js';
|
|
||||||
import { AggregateError, AstroError, CompilerError } from '../errors/errors.js';
|
import { AggregateError, AstroError, CompilerError } from '../errors/errors.js';
|
||||||
|
import { AstroErrorData } from '../errors/index.js';
|
||||||
import { prependForwardSlash } from '../path.js';
|
import { prependForwardSlash } from '../path.js';
|
||||||
import { resolvePath, viteID } from '../util.js';
|
import { resolvePath, viteID } from '../util.js';
|
||||||
import { createStylePreprocessor } from './style.js';
|
import { createStylePreprocessor } from './style.js';
|
||||||
|
@ -61,7 +61,7 @@ async function compile({
|
||||||
// The compiler should be able to handle errors by itself, however
|
// The compiler should be able to handle errors by itself, however
|
||||||
// for the rare cases where it can't let's directly throw here with as much info as possible
|
// for the rare cases where it can't let's directly throw here with as much info as possible
|
||||||
throw new CompilerError({
|
throw new CompilerError({
|
||||||
errorCode: AstroErrorCodes.UnknownCompilerError,
|
...AstroErrorData.UnknownCompilerError,
|
||||||
message: err.message ?? 'Unknown compiler error',
|
message: err.message ?? 'Unknown compiler error',
|
||||||
stack: err.stack,
|
stack: err.stack,
|
||||||
location: {
|
location: {
|
||||||
|
@ -70,22 +70,18 @@ async function compile({
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const compilerError = result.diagnostics.find(
|
const compilerError = result.diagnostics.find((diag) => diag.severity === 1);
|
||||||
// HACK: The compiler currently mistakenly returns the wrong severity for warnings, so we'll also filter by code
|
|
||||||
// https://github.com/withastro/compiler/issues/595
|
|
||||||
(diag) => diag.severity === 1 && diag.code < 2000
|
|
||||||
);
|
|
||||||
|
|
||||||
if (compilerError) {
|
if (compilerError) {
|
||||||
throw new CompilerError({
|
throw new CompilerError({
|
||||||
errorCode: compilerError.code,
|
code: compilerError.code,
|
||||||
message: compilerError.text,
|
message: compilerError.text,
|
||||||
location: {
|
location: {
|
||||||
line: compilerError.location.line,
|
line: compilerError.location.line,
|
||||||
column: compilerError.location.column,
|
column: compilerError.location.column,
|
||||||
file: compilerError.location.file,
|
file: compilerError.location.file,
|
||||||
},
|
},
|
||||||
hint: compilerError.hint ? compilerError.hint : undefined,
|
hint: compilerError.hint,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +90,8 @@ async function compile({
|
||||||
return result;
|
return result;
|
||||||
case 1: {
|
case 1: {
|
||||||
let error = cssTransformErrors[0];
|
let error = cssTransformErrors[0];
|
||||||
if (!error.errorCode) {
|
if (!error.code) {
|
||||||
error.errorCode = AstroErrorCodes.UnknownCompilerCSSError;
|
error.code = AstroErrorData.UnknownCSSError.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw cssTransformErrors[0];
|
throw cssTransformErrors[0];
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import type { TransformOptions } from '@astrojs/compiler';
|
import type { TransformOptions } from '@astrojs/compiler';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { preprocessCSS, ResolvedConfig } from 'vite';
|
import { preprocessCSS, ResolvedConfig } from 'vite';
|
||||||
import { AstroErrorCodes } from '../errors/codes.js';
|
import { AstroErrorData, CSSError, positionAt } from '../errors/index.js';
|
||||||
import { CSSError } from '../errors/errors.js';
|
|
||||||
import { positionAt } from '../errors/index.js';
|
|
||||||
|
|
||||||
export function createStylePreprocessor({
|
export function createStylePreprocessor({
|
||||||
filename,
|
filename,
|
||||||
|
@ -57,7 +55,7 @@ function enhanceCSSError(err: any, filename: string) {
|
||||||
// Vite will handle creating the frame for us with proper line numbers, no need to create one
|
// Vite will handle creating the frame for us with proper line numbers, no need to create one
|
||||||
|
|
||||||
return new CSSError({
|
return new CSSError({
|
||||||
errorCode: AstroErrorCodes.CssSyntaxError,
|
...AstroErrorData.CSSSyntaxError,
|
||||||
message: err.reason,
|
message: err.reason,
|
||||||
location: {
|
location: {
|
||||||
file: filename,
|
file: filename,
|
||||||
|
@ -72,7 +70,7 @@ function enhanceCSSError(err: any, filename: string) {
|
||||||
const errorLine = positionAt(styleTagBeginning, fileContent).line + (err.line ?? 0);
|
const errorLine = positionAt(styleTagBeginning, fileContent).line + (err.line ?? 0);
|
||||||
|
|
||||||
return new CSSError({
|
return new CSSError({
|
||||||
errorCode: AstroErrorCodes.CssUnknownError,
|
...AstroErrorData.UnknownCSSError,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
location: {
|
location: {
|
||||||
file: filename,
|
file: filename,
|
||||||
|
@ -88,7 +86,7 @@ function enhanceCSSError(err: any, filename: string) {
|
||||||
errorPosition.line += 1;
|
errorPosition.line += 1;
|
||||||
|
|
||||||
return new CSSError({
|
return new CSSError({
|
||||||
errorCode: AstroErrorCodes.CssUnknownError,
|
code: AstroErrorData.UnknownCSSError.code,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
location: {
|
location: {
|
||||||
file: filename,
|
file: filename,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { mergeConfig as mergeViteConfig } from 'vite';
|
||||||
import { LogOptions } from '../logger/core.js';
|
import { LogOptions } from '../logger/core.js';
|
||||||
import { arraify, isObject, isURL } from '../util.js';
|
import { arraify, isObject, isURL } from '../util.js';
|
||||||
import { createRelativeSchema } from './schema.js';
|
import { createRelativeSchema } from './schema.js';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
|
|
||||||
load.use([loadTypeScript]);
|
load.use([loadTypeScript]);
|
||||||
|
|
||||||
|
@ -76,9 +77,10 @@ export async function validateConfig(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (legacyConfigKey) {
|
if (legacyConfigKey) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`Legacy configuration detected: "${legacyConfigKey}".\nPlease update your configuration to the new format!\nSee https://astro.build/config for more information.`
|
...AstroErrorData.ConfigLegacyKey,
|
||||||
);
|
message: AstroErrorData.ConfigLegacyKey.message(legacyConfigKey),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
|
|
||||||
|
@ -171,7 +173,10 @@ export async function resolveConfigPath(
|
||||||
return configPath;
|
return configPath;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ProloadError && flags.config) {
|
if (e instanceof ProloadError && flags.config) {
|
||||||
throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`);
|
throw new AstroError({
|
||||||
|
...AstroErrorData.ConfigNotFound,
|
||||||
|
message: AstroErrorData.ConfigNotFound.message(flags.config),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -251,7 +256,10 @@ async function tryLoadConfig(
|
||||||
return config as TryLoadConfigResult;
|
return config as TryLoadConfigResult;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ProloadError && flags.config) {
|
if (e instanceof ProloadError && flags.config) {
|
||||||
throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`);
|
throw new AstroError({
|
||||||
|
...AstroErrorData.ConfigNotFound,
|
||||||
|
message: AstroErrorData.ConfigNotFound.message(flags.config),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = await resolveConfigPath(configOptions);
|
const configPath = await resolveConfigPath(configOptions);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { renderEndpoint } from '../../runtime/server/index.js';
|
||||||
import { ASTRO_VERSION } from '../constants.js';
|
import { ASTRO_VERSION } from '../constants.js';
|
||||||
import { AstroCookies, attachToResponse } from '../cookies/index.js';
|
import { AstroCookies, attachToResponse } from '../cookies/index.js';
|
||||||
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
|
import { getParamsAndProps, GetParamsAndPropsError } from '../render/core.js';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
|
@ -52,13 +53,12 @@ function createAPIContext({
|
||||||
get clientAddress() {
|
get clientAddress() {
|
||||||
if (!(clientAddressSymbol in request)) {
|
if (!(clientAddressSymbol in request)) {
|
||||||
if (adapterName) {
|
if (adapterName) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`clientAddress is not available in the ${adapterName} adapter. File an issue with the adapter to add support.`
|
...AstroErrorData.SSRClientAddressNotAvailableInAdapter,
|
||||||
);
|
message: AstroErrorData.SSRClientAddressNotAvailableInAdapter.message(adapterName),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new AstroError(AstroErrorData.StaticClientAddressNotAvailable);
|
||||||
`clientAddress is not available in your environment. Ensure that you are using an SSR adapter that supports this feature.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,9 +82,13 @@ export async function call(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
if (paramsAndPropsResp === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`[getStaticPath] route pattern matched, but no matching static path found. (${ctx.pathname})`
|
...AstroErrorData.NoMatchingStaticPathFound,
|
||||||
);
|
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
|
||||||
|
hint: ctx.route?.component
|
||||||
|
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
|
||||||
|
: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const [params, props] = paramsAndPropsResp;
|
const [params, props] = paramsAndPropsResp;
|
||||||
|
|
||||||
|
@ -120,8 +124,6 @@ function isRedirect(statusCode: number) {
|
||||||
|
|
||||||
export function throwIfRedirectNotAllowed(response: Response, config: AstroConfig) {
|
export function throwIfRedirectNotAllowed(response: Response, config: AstroConfig) {
|
||||||
if (config.output !== 'server' && isRedirect(response.status)) {
|
if (config.output !== 'server' && isRedirect(response.status)) {
|
||||||
throw new Error(
|
throw new AstroError(AstroErrorData.StaticRedirectNotAllowed);
|
||||||
`Redirects are only available when using output: 'server'. Update your Astro config if you need SSR features.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
57
packages/astro/src/core/errors/README.md
Normal file
57
packages/astro/src/core/errors/README.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# Errors
|
||||||
|
|
||||||
|
> Interested in the technical details? See the comments in [errors-data.ts.](./errors-data.ts)
|
||||||
|
|
||||||
|
## Writing error messages for Astro
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
**Choosing an Error Code**
|
||||||
|
|
||||||
|
Choose any available error code in the appropriate range:
|
||||||
|
- 01xxx and 02xxx are reserved for compiler errors and warnings respectively
|
||||||
|
- 03xxx: Astro errors (your error most likely goes here!)
|
||||||
|
- 04xxx: CSS errors
|
||||||
|
- 05xxx: Vite errors
|
||||||
|
- 06xxx: Markdown errors
|
||||||
|
- 07xxx: Configuration errors
|
||||||
|
- 07xxx-98xxx <- Need to add a category? Add it here!
|
||||||
|
- 99xxx: Catch-alls for unknown errors
|
||||||
|
|
||||||
|
As long as it is unique, the exact error code used is unimportant. For example, error 5005 and error 5006 don't necessarily have to be related, or follow any logical pattern.
|
||||||
|
|
||||||
|
Users are not reading codes sequentially. They're much more likely to directly land on the error or search for a specific code.
|
||||||
|
|
||||||
|
If you are unsure about which error code to choose, ask [Erika](https://github.com/Princesseuh)!
|
||||||
|
|
||||||
|
**Error Code Format**
|
||||||
|
- 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.`)
|
||||||
|
- A `hint` can be used for any additional info that might help the user (ex: a link to the documentation, or a common cause)
|
||||||
|
|
||||||
|
**Error Code Writing Style**
|
||||||
|
- Technical jargon is mostly okay! But, most abbreviations should be avoided. If a developer is unfamiliar with a technical term, spelling it out in full allows them to look it up on the web more easily.
|
||||||
|
- Describe the what, why and action to take from the user's perspective. Assume they don't know Astro internals, and care only about how Astro is _used_ (ex: `You are missing...` vs `Astro/file cannot find...`)
|
||||||
|
- Avoid using cutesy language (ex: Oops!). This tone minimizes the significance of the error, which _is_ important to the developer. The developer may be frustrated and your error message shouldn't be making jokes about their struggles. Only include words and phrases that help the developer **interpret the error** and **fix the problem**.
|
||||||
|
|
||||||
|
### CLI specifics:
|
||||||
|
- If the error happened **during an action that changes the state of the project** (ex: editing configuration, creating files), the error should **reassure the user** about the state of their project (ex: "Failed to update configuration. Your project has been restored to its previous state.")
|
||||||
|
- If an "error" happened because of a conscious user action (ex: pressing CTRL+C during a choice), it is okay to add more personality (ex: "Operation cancelled. See you later, astronaut!"). Do keep in mind the previous point however (ex: "Operation cancelled. No worries, your project folder has already been created")
|
||||||
|
|
||||||
|
### Shape
|
||||||
|
- **Error codes and 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.
|
||||||
|
- Contextual information may be used to enhance the message or the hint. However, the error code itself should not be included in the message as it will already be shown as part of the the error.
|
||||||
|
- Do not prefix `message` and `hint` with descriptive words such as "Error:" or "Hint:" as it may lead to duplicated labels in the UI / CLI.
|
||||||
|
|
||||||
|
### Always remember
|
||||||
|
|
||||||
|
Error are a reactive strategy. They are the last line of defense against a mistake.
|
||||||
|
|
||||||
|
Before adding a new error message, ask yourself, "Was there a way this situation could've been avoided in the first place?" (docs, editor tooling etc).
|
||||||
|
|
||||||
|
**If you can prevent the error, you don't need an error message!**
|
||||||
|
|
||||||
|
## Additional resources on writing good error messages
|
||||||
|
|
||||||
|
- [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)
|
|
@ -1,24 +0,0 @@
|
||||||
export enum AstroErrorCodes {
|
|
||||||
// 1xxx are reserved for compiler errors
|
|
||||||
StaticRedirectNotAllowed = 2005,
|
|
||||||
UnavailableInSSR = 2006,
|
|
||||||
// Runtime errors
|
|
||||||
GenericRuntimeError = 3000,
|
|
||||||
// PostCSS errors
|
|
||||||
CssSyntaxError = 4000,
|
|
||||||
CssUnknownError = 4001,
|
|
||||||
// Vite SSR errors
|
|
||||||
FailedToLoadModuleSSR = 5000,
|
|
||||||
// Config Errors
|
|
||||||
ConfigError = 6000,
|
|
||||||
|
|
||||||
// Markdown Errors
|
|
||||||
GenericMarkdownError = 7000,
|
|
||||||
MarkdownFrontmatterParseError = 7001,
|
|
||||||
|
|
||||||
// General catch-alls for cases where we have zero information
|
|
||||||
UnknownCompilerError = 9000,
|
|
||||||
UnknownCompilerCSSError = 9001,
|
|
||||||
UnknownViteSSRError = 9002,
|
|
||||||
UnknownError = 9999,
|
|
||||||
}
|
|
|
@ -1,9 +1,12 @@
|
||||||
import type { BuildResult } from 'esbuild';
|
import type { BuildResult } from 'esbuild';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
import type { SSRError } from '../../../@types/astro.js';
|
import type { SSRError } from '../../../@types/astro.js';
|
||||||
import { AggregateError, ErrorWithMetadata } from '../errors.js';
|
import { AggregateError, ErrorWithMetadata } from '../errors.js';
|
||||||
import { codeFrame } from '../printer.js';
|
import { codeFrame } from '../printer.js';
|
||||||
import { collectInfoFromStacktrace } from '../utils.js';
|
import { normalizeLF } from '../utils.js';
|
||||||
|
|
||||||
export const incompatiblePackages = {
|
export const incompatiblePackages = {
|
||||||
'react-spectrum': `@adobe/react-spectrum is not compatible with Vite's server-side rendering mode at the moment. You can still use React Spectrum from the client. Create an island React component and use the client:only directive. From there you can use React Spectrum.`,
|
'react-spectrum': `@adobe/react-spectrum is not compatible with Vite's server-side rendering mode at the moment. You can still use React Spectrum from the client. Create an island React component and use the client:only directive. From there you can use React Spectrum.`,
|
||||||
|
@ -14,7 +17,7 @@ export const incompatPackageExp = new RegExp(`(${Object.keys(incompatiblePackage
|
||||||
* Takes any error-like object and returns a standardized Error + metadata object.
|
* Takes any error-like object and returns a standardized Error + metadata object.
|
||||||
* Useful for consistent reporting regardless of where the error surfaced from.
|
* Useful for consistent reporting regardless of where the error surfaced from.
|
||||||
*/
|
*/
|
||||||
export function collectErrorMetadata(e: any, filePath?: URL): ErrorWithMetadata {
|
export function collectErrorMetadata(e: any, rootFolder?: URL | undefined): ErrorWithMetadata {
|
||||||
const err = AggregateError.is(e) ? (e.errors as SSRError[]) : [e as SSRError];
|
const err = AggregateError.is(e) ? (e.errors as SSRError[]) : [e as SSRError];
|
||||||
|
|
||||||
err.forEach((error) => {
|
err.forEach((error) => {
|
||||||
|
@ -22,6 +25,11 @@ export function collectErrorMetadata(e: any, filePath?: URL): ErrorWithMetadata
|
||||||
error = collectInfoFromStacktrace(e);
|
error = collectInfoFromStacktrace(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (error.loc?.file && rootFolder && !error.loc.file.startsWith('/')) {
|
||||||
|
error.loc.file = join(fileURLToPath(rootFolder), error.loc.file);
|
||||||
|
}
|
||||||
|
|
||||||
// If we don't have a frame, but we have a location let's try making up a frame for it
|
// If we don't have a frame, but we have a location let's try making up a frame for it
|
||||||
if (!error.frame && error.loc) {
|
if (!error.frame && error.loc) {
|
||||||
try {
|
try {
|
||||||
|
@ -32,21 +40,33 @@ export function collectErrorMetadata(e: any, filePath?: URL): ErrorWithMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic error (probably from Vite, and already formatted)
|
// Generic error (probably from Vite, and already formatted)
|
||||||
if (!error.hint) {
|
error.hint = generateHint(e);
|
||||||
error.hint = generateHint(e, filePath);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we received an array of errors and it's not from us, it should be from ESBuild, try to extract info for Vite to display
|
// If we received an array of errors and it's not from us, it should be from ESBuild, try to extract info for Vite to display
|
||||||
if (!AggregateError.is(e) && Array.isArray((e as any).errors)) {
|
if (!AggregateError.is(e) && Array.isArray((e as any).errors)) {
|
||||||
(e as BuildResult).errors.forEach((buildError, i) => {
|
(e as BuildResult).errors.forEach((buildError, i) => {
|
||||||
const { location, pluginName } = buildError;
|
const { location, pluginName, text } = buildError;
|
||||||
|
|
||||||
|
// ESBuild can give us a slightly better error message than the one in the error, so let's use it
|
||||||
|
err[i].message = text;
|
||||||
|
|
||||||
if (location) {
|
if (location) {
|
||||||
err[i].loc = { file: location.file, line: location.line, column: location.column };
|
err[i].loc = { file: location.file, line: location.line, column: location.column };
|
||||||
err[i].id = err[0].id || location?.file;
|
err[i].id = err[0].id || location?.file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vite adds the error message to the frame for ESBuild errors, we don't want that
|
||||||
|
if (err[i].frame) {
|
||||||
|
const errorLines = err[i].frame?.trim().split('\n');
|
||||||
|
|
||||||
|
if (errorLines) {
|
||||||
|
err[i].frame = !/^\d/.test(errorLines[0])
|
||||||
|
? errorLines?.slice(1).join('\n')
|
||||||
|
: err[i].frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const possibleFilePath = err[i].pluginCode || err[i].id || location?.file;
|
const possibleFilePath = err[i].pluginCode || err[i].id || location?.file;
|
||||||
if (possibleFilePath && !err[i].frame) {
|
if (possibleFilePath && !err[i].frame) {
|
||||||
try {
|
try {
|
||||||
|
@ -61,7 +81,7 @@ export function collectErrorMetadata(e: any, filePath?: URL): ErrorWithMetadata
|
||||||
err[i].plugin = pluginName;
|
err[i].plugin = pluginName;
|
||||||
}
|
}
|
||||||
|
|
||||||
err[i].hint = generateHint(err[0], filePath);
|
err[i].hint = generateHint(err[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,14 +89,21 @@ export function collectErrorMetadata(e: any, filePath?: URL): ErrorWithMetadata
|
||||||
return err[0];
|
return err[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateHint(err: ErrorWithMetadata, filePath?: URL): string | undefined {
|
function generateHint(err: ErrorWithMetadata): string | undefined {
|
||||||
if (/Unknown file extension \"\.(jsx|vue|svelte|astro|css)\" for /.test(err.message)) {
|
if (/Unknown file extension \"\.(jsx|vue|svelte|astro|css)\" for /.test(err.message)) {
|
||||||
return 'You likely need to add this package to `vite.ssr.noExternal` in your astro config file.';
|
return 'You likely need to add this package to `vite.ssr.noExternal` in your astro config file.';
|
||||||
} else if (
|
} else if (err.toString().includes('document')) {
|
||||||
err.toString().startsWith('ReferenceError') &&
|
const hint = `Browser APIs are not available on the server.
|
||||||
(err.loc?.file ?? filePath?.pathname)?.endsWith('.astro')
|
|
||||||
) {
|
${
|
||||||
return 'export statements in `.astro` files do not have access to local variable declarations, only imported values.';
|
err.loc?.file?.endsWith('.astro')
|
||||||
|
? 'Move your code to a <script> tag outside of the frontmatter, so the code runs on the client'
|
||||||
|
: 'If the code is in a framework component, try to access these objects after rendering using lifecycle methods or use a `client:only` directive to make the component exclusively run on the client'
|
||||||
|
}
|
||||||
|
|
||||||
|
See https://docs.astro.build/en/guides/troubleshooting/#document-or-window-is-not-defined for more information.
|
||||||
|
`;
|
||||||
|
return hint;
|
||||||
} else {
|
} else {
|
||||||
const res = incompatPackageExp.exec(err.stack);
|
const res = incompatPackageExp.exec(err.stack);
|
||||||
if (res) {
|
if (res) {
|
||||||
|
@ -84,5 +111,61 @@ function generateHint(err: ErrorWithMetadata, filePath?: URL): string | undefine
|
||||||
return incompatiblePackages[key];
|
return incompatiblePackages[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return err.hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectInfoFromStacktrace(error: SSRError): SSRError {
|
||||||
|
if (!error.stack) return error;
|
||||||
|
|
||||||
|
// normalize error stack line-endings to \n
|
||||||
|
error.stack = normalizeLF(error.stack);
|
||||||
|
const stackText = stripAnsi(error.stack);
|
||||||
|
|
||||||
|
// Try to find possible location from stack if we don't have one
|
||||||
|
if (!error.loc || (!error.loc.column && !error.loc.line)) {
|
||||||
|
const possibleFilePath =
|
||||||
|
error.loc?.file ||
|
||||||
|
error.pluginCode ||
|
||||||
|
error.id ||
|
||||||
|
// TODO: this could be better, `src` might be something else
|
||||||
|
stackText.split('\n').find((ln) => ln.includes('src') || ln.includes('node_modules'));
|
||||||
|
const source = possibleFilePath?.replace(/^[^(]+\(([^)]+).*$/, '$1').replace(/^\s+at\s+/, '');
|
||||||
|
|
||||||
|
let file = source?.replace(/(:[0-9]+)/g, '');
|
||||||
|
const location = /:([0-9]+):([0-9]+)/g.exec(source!) ?? [];
|
||||||
|
const line = location[1];
|
||||||
|
const column = location[2];
|
||||||
|
|
||||||
|
if (file && line && column) {
|
||||||
|
try {
|
||||||
|
file = fileURLToPath(file);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
error.loc = {
|
||||||
|
file,
|
||||||
|
line: Number.parseInt(line),
|
||||||
|
column: Number.parseInt(column),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive plugin from stack (if possible)
|
||||||
|
if (!error.plugin) {
|
||||||
|
error.plugin =
|
||||||
|
/withastro\/astro\/packages\/integrations\/([\w-]+)/gim.exec(stackText)?.at(1) ||
|
||||||
|
/(@astrojs\/[\w-]+)\/(server|client|index)/gim.exec(stackText)?.at(1) ||
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize stack (remove `/@fs/` urls, etc)
|
||||||
|
error.stack = cleanErrorStack(error.stack);
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanErrorStack(stack: string) {
|
||||||
|
return stack
|
||||||
|
.split(/\n/g)
|
||||||
|
.map((l) => l.replace(/\/@fs\//g, '/'))
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,9 @@ import * as fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { createLogger, type ErrorPayload, type Logger, type LogLevel } from 'vite';
|
import { createLogger, type ErrorPayload, type Logger, type LogLevel } from 'vite';
|
||||||
import type { ModuleLoader } from '../../module-loader/index.js';
|
import type { ModuleLoader } from '../../module-loader/index.js';
|
||||||
import { AstroErrorCodes } from '../codes.js';
|
import { AstroErrorData } from '../errors-data.js';
|
||||||
import { AstroError, type ErrorWithMetadata } from '../errors.js';
|
import { type ErrorWithMetadata } from '../errors.js';
|
||||||
|
import { createSafeError } from '../utils.js';
|
||||||
import { incompatPackageExp } from './utils.js';
|
import { incompatPackageExp } from './utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,55 +23,71 @@ export function createCustomViteLogger(logLevel: LogLevel): Logger {
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enhanceViteSSRError(
|
export function enhanceViteSSRError(error: unknown, filePath?: URL, loader?: ModuleLoader): Error {
|
||||||
error: Error,
|
// NOTE: We don't know where the error that's coming here comes from, so we need to be defensive regarding what we do
|
||||||
filePath?: URL,
|
// to it to make sure we keep as much information as possible. It's very possible that we receive an error that does not
|
||||||
loader?: ModuleLoader
|
// follow any kind of standard formats (ex: a number, a string etc)
|
||||||
): AstroError {
|
const safeError = createSafeError(error) as ErrorWithMetadata;
|
||||||
|
|
||||||
// Vite will give you better stacktraces, using sourcemaps.
|
// Vite will give you better stacktraces, using sourcemaps.
|
||||||
if (loader) {
|
if (loader) {
|
||||||
try {
|
try {
|
||||||
loader.fixStacktrace(error);
|
loader.fixStacktrace(safeError as Error);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newError = new AstroError({
|
if (filePath) {
|
||||||
name: error.name,
|
|
||||||
message: error.message,
|
|
||||||
location: (error as any).loc,
|
|
||||||
stack: error.stack,
|
|
||||||
errorCode: (error as AstroError).errorCode
|
|
||||||
? (error as AstroError).errorCode
|
|
||||||
: AstroErrorCodes.UnknownViteSSRError,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Vite has a fairly generic error message when it fails to load a module, let's try to enhance it a bit
|
|
||||||
// https://github.com/vitejs/vite/blob/ee7c28a46a6563d54b828af42570c55f16b15d2c/packages/vite/src/node/ssr/ssrModuleLoader.ts#L91
|
|
||||||
if (filePath && /failed to load module for ssr:/.test(error.message)) {
|
|
||||||
const importName = error.message.split('for ssr:').at(1)?.trim();
|
|
||||||
if (importName) {
|
|
||||||
newError.setMessage(`Could not import "${importName}"`);
|
|
||||||
newError.setHint('Make sure the file exists');
|
|
||||||
newError.setErrorCode(AstroErrorCodes.FailedToLoadModuleSSR);
|
|
||||||
|
|
||||||
const path = fileURLToPath(filePath);
|
const path = fileURLToPath(filePath);
|
||||||
const content = fs.readFileSync(path).toString();
|
const content = fs.readFileSync(path).toString();
|
||||||
const lns = content.split('\n');
|
const lns = content.split('\n');
|
||||||
|
|
||||||
|
// Vite has a fairly generic error message when it fails to load a module, let's try to enhance it a bit
|
||||||
|
// https://github.com/vitejs/vite/blob/ee7c28a46a6563d54b828af42570c55f16b15d2c/packages/vite/src/node/ssr/ssrModuleLoader.ts#L91
|
||||||
|
if (/failed to load module for ssr:/.test(safeError.message)) {
|
||||||
|
const importName = safeError.message.split('for ssr:').at(1)?.trim();
|
||||||
|
if (importName) {
|
||||||
|
safeError.message = AstroErrorData.FailedToLoadModuleSSR.message(importName);
|
||||||
|
safeError.hint = AstroErrorData.FailedToLoadModuleSSR.hint;
|
||||||
|
safeError.code = AstroErrorData.FailedToLoadModuleSSR.code;
|
||||||
const line = lns.findIndex((ln) => ln.includes(importName));
|
const line = lns.findIndex((ln) => ln.includes(importName));
|
||||||
|
|
||||||
if (line !== -1) {
|
if (line !== -1) {
|
||||||
const column = lns[line]?.indexOf(importName);
|
const column = lns[line]?.indexOf(importName);
|
||||||
|
|
||||||
newError.setLocation({
|
safeError.loc = {
|
||||||
file: path,
|
file: path,
|
||||||
line: line + 1,
|
line: line + 1,
|
||||||
column,
|
column,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newError;
|
// Since Astro.glob is a wrapper around Vite's import.meta.glob, errors don't show accurate information, let's fix that
|
||||||
|
if (/Invalid glob/.test(safeError.message)) {
|
||||||
|
const globPattern = safeError.message.match(/glob: "(.+)" \(/)?.[1];
|
||||||
|
|
||||||
|
if (globPattern) {
|
||||||
|
safeError.message = AstroErrorData.InvalidGlob.message(globPattern);
|
||||||
|
safeError.hint = AstroErrorData.InvalidGlob.hint;
|
||||||
|
safeError.code = AstroErrorData.InvalidGlob.code;
|
||||||
|
|
||||||
|
const line = lns.findIndex((ln) => ln.includes(globPattern));
|
||||||
|
|
||||||
|
if (line !== -1) {
|
||||||
|
const column = lns[line]?.indexOf(globPattern);
|
||||||
|
|
||||||
|
safeError.loc = {
|
||||||
|
file: path,
|
||||||
|
line: line + 1,
|
||||||
|
column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeError;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
199
packages/astro/src/core/errors/errors-data.ts
Normal file
199
packages/astro/src/core/errors/errors-data.ts
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
// BEFORE ADDING AN ERROR: Please look at the README.md in this folder for general guidelines on writing error messages
|
||||||
|
// Additionally, this code, much like `@types/astro.ts`, is used to generate documentation, so make sure to pass
|
||||||
|
// your changes by our wonderful docs team before merging!
|
||||||
|
|
||||||
|
interface ErrorData {
|
||||||
|
code: number;
|
||||||
|
message?: string | ((...params: any) => string);
|
||||||
|
hint?: string | ((...params: any) => string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace with `satisfies` once TS 4.9 is out
|
||||||
|
const defineErrors = <T extends Record<string, ErrorData>>(errs: T) => errs;
|
||||||
|
export const AstroErrorData = defineErrors({
|
||||||
|
UnknownCompilerError: {
|
||||||
|
code: 1000,
|
||||||
|
},
|
||||||
|
// 1xxx and 2xxx codes are reserved for compiler errors and warnings respectively
|
||||||
|
StaticRedirectNotAllowed: {
|
||||||
|
code: 3001,
|
||||||
|
message:
|
||||||
|
"Redirects are only available when using output: 'server'. Update your Astro config if you need SSR features.",
|
||||||
|
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project for more information on how to enable SSR.',
|
||||||
|
},
|
||||||
|
SSRClientAddressNotAvailableInAdapter: {
|
||||||
|
code: 3002,
|
||||||
|
message: (adapterName: string) =>
|
||||||
|
`Astro.clientAddress is not available in the ${adapterName} adapter. File an issue with the adapter to add support.`,
|
||||||
|
},
|
||||||
|
StaticClientAddressNotAvailable: {
|
||||||
|
code: 3003,
|
||||||
|
message:
|
||||||
|
"Astro.clientAddress is only available when using output: 'server'. Update your Astro config if you need SSR features.",
|
||||||
|
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project for more information on how to enable SSR.',
|
||||||
|
},
|
||||||
|
NoMatchingStaticPathFound: {
|
||||||
|
code: 3004,
|
||||||
|
message: (pathName: string) =>
|
||||||
|
`A getStaticPaths route pattern was matched, but no matching static path was found for requested path ${pathName}.`,
|
||||||
|
hint: (possibleRoutes: string[]) =>
|
||||||
|
`Possible dynamic routes being matched: ${possibleRoutes.join(', ')}.`,
|
||||||
|
},
|
||||||
|
OnlyResponseCanBeReturned: {
|
||||||
|
code: 3005,
|
||||||
|
message: (route: string | undefined, returnedValue: string) =>
|
||||||
|
`Route ${
|
||||||
|
route ? route : ''
|
||||||
|
} returned a ${returnedValue}. Only a Response can be returned from Astro files.`,
|
||||||
|
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/#response for more information.',
|
||||||
|
},
|
||||||
|
MissingMediaQueryDirective: {
|
||||||
|
code: 3006,
|
||||||
|
message: (componentName: string) =>
|
||||||
|
`Media query not provided for "client:media" directive. A media query similar to <${componentName} client:media="(max-width: 600px)" /> must be provided`,
|
||||||
|
},
|
||||||
|
NoMatchingRenderer: {
|
||||||
|
code: 3007,
|
||||||
|
message: (
|
||||||
|
componentName: string,
|
||||||
|
componentExtension: string | undefined,
|
||||||
|
plural: boolean,
|
||||||
|
validRenderersCount: number
|
||||||
|
) =>
|
||||||
|
`Unable to render ${componentName}!
|
||||||
|
|
||||||
|
${
|
||||||
|
validRenderersCount > 0
|
||||||
|
? `There ${plural ? 'are' : 'is'} ${validRenderersCount} renderer${
|
||||||
|
plural ? 's' : ''
|
||||||
|
} configured in your \`astro.config.mjs\` file,
|
||||||
|
but ${plural ? 'none were' : 'it was not'} able to server-side render ${componentName}.`
|
||||||
|
: `No valid renderer was found ${
|
||||||
|
componentExtension
|
||||||
|
? `for the .${componentExtension} file extension.`
|
||||||
|
: `for this file extension.`
|
||||||
|
}`
|
||||||
|
}`,
|
||||||
|
hint: (probableRenderers: string) =>
|
||||||
|
`Did you mean to enable the ${probableRenderers} integration?\n\nSee https://docs.astro.build/en/core-concepts/framework-components/ for more information on how to install and configure integrations.`,
|
||||||
|
},
|
||||||
|
NoClientEntrypoint: {
|
||||||
|
code: 3008,
|
||||||
|
message: (componentName: string, clientDirective: string, rendererName: string) =>
|
||||||
|
`${componentName} component has a \`client:${clientDirective}\` directive, but no client entrypoint was provided by ${rendererName}!`,
|
||||||
|
hint: 'See https://docs.astro.build/en/reference/integrations-reference/#addrenderer-option for more information on how to configure your renderer.',
|
||||||
|
},
|
||||||
|
NoClientOnlyHint: {
|
||||||
|
code: 3009,
|
||||||
|
message: (componentName: string) =>
|
||||||
|
`Unable to render ${componentName}! When using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.`,
|
||||||
|
hint: (probableRenderers: string) =>
|
||||||
|
`Did you mean to pass client:only="${probableRenderers}"? See https://docs.astro.build/en/reference/directives-reference/#clientonly for more information on client:only`,
|
||||||
|
},
|
||||||
|
InvalidStaticPathParam: {
|
||||||
|
code: 3010,
|
||||||
|
message: (paramType) =>
|
||||||
|
`Invalid params given to getStaticPaths path. Expected an object, got ${paramType}`,
|
||||||
|
hint: 'See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths.',
|
||||||
|
},
|
||||||
|
InvalidGetStaticPathsReturn: {
|
||||||
|
code: 3011,
|
||||||
|
message: (returnType) =>
|
||||||
|
`Invalid type returned by getStaticPaths. Expected an array, got ${returnType}`,
|
||||||
|
hint: 'See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths.',
|
||||||
|
},
|
||||||
|
GetStaticPathsDeprecatedRSS: {
|
||||||
|
code: 3012,
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
GetStaticPathsExpectedParams: {
|
||||||
|
code: 3013,
|
||||||
|
message: 'Missing or empty required params property on getStaticPaths route',
|
||||||
|
hint: 'See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths.',
|
||||||
|
},
|
||||||
|
GetStaticPathsInvalidRouteParam: {
|
||||||
|
code: 3014,
|
||||||
|
message: (key: string, value: any) =>
|
||||||
|
`Invalid getStaticPaths route parameter for \`${key}\`. Expected a string or number, received \`${typeof value}\` ("${value}")`,
|
||||||
|
hint: 'See https://docs.astro.build/en/reference/api-reference/#getstaticpaths for more information on getStaticPaths.',
|
||||||
|
},
|
||||||
|
GetStaticPathsRequired: {
|
||||||
|
code: 3015,
|
||||||
|
message:
|
||||||
|
'getStaticPaths() function is required for dynamic routes. Make sure that you `export` a `getStaticPaths` function from your dynamic route.',
|
||||||
|
hint: `See https://docs.astro.build/en/core-concepts/routing/#dynamic-routes for more information on dynamic routes.
|
||||||
|
|
||||||
|
Alternatively, set \`output: "server"\` in your Astro config file to switch to a non-static server build.
|
||||||
|
See https://docs.astro.build/en/guides/server-side-rendering/ for more information on non-static rendering.`,
|
||||||
|
},
|
||||||
|
ReservedSlotName: {
|
||||||
|
code: 3016,
|
||||||
|
message: (slotName: string) =>
|
||||||
|
`Unable to create a slot named "${slotName}". ${slotName}" is a reserved slot name! Please update the name of this slot.`,
|
||||||
|
},
|
||||||
|
NoAdapterInstalled: {
|
||||||
|
code: 3017,
|
||||||
|
message: `Cannot use \`output: 'server'\` without an adapter. Please install and configure the appropriate server adapter for your final deployment.`,
|
||||||
|
hint: 'See https://docs.astro.build/en/guides/server-side-rendering/ for more information.',
|
||||||
|
},
|
||||||
|
NoMatchingImport: {
|
||||||
|
code: 3018,
|
||||||
|
message: (componentName: string) =>
|
||||||
|
`Could not render ${componentName}. No matching import has been found for ${componentName}.`,
|
||||||
|
hint: 'Please make sure the component is properly imported.',
|
||||||
|
},
|
||||||
|
// CSS Errors - 4xxx
|
||||||
|
UnknownCSSError: {
|
||||||
|
code: 4000,
|
||||||
|
},
|
||||||
|
CSSSyntaxError: {
|
||||||
|
code: 4001,
|
||||||
|
},
|
||||||
|
// Vite Errors - 5xxx
|
||||||
|
UnknownViteError: {
|
||||||
|
code: 5000,
|
||||||
|
},
|
||||||
|
FailedToLoadModuleSSR: {
|
||||||
|
code: 5001,
|
||||||
|
message: (importName: string) => `Could not import "${importName}".`,
|
||||||
|
hint: 'This is often caused by a typo in the import path. Please make sure the file exists.',
|
||||||
|
},
|
||||||
|
InvalidGlob: {
|
||||||
|
code: 5002,
|
||||||
|
message: (globPattern: string) =>
|
||||||
|
`Invalid glob pattern: "${globPattern}". Glob patterns must start with './', '../' or '/'.`,
|
||||||
|
hint: 'See https://docs.astro.build/en/guides/imports/#glob-patterns for more information on supported glob patterns.',
|
||||||
|
},
|
||||||
|
// Markdown Errors - 6xxx
|
||||||
|
UnknownMarkdownError: {
|
||||||
|
code: 6000,
|
||||||
|
},
|
||||||
|
MarkdownFrontmatterParseError: {
|
||||||
|
code: 6001,
|
||||||
|
},
|
||||||
|
// Config Errors - 7xxx
|
||||||
|
UnknownConfigError: {
|
||||||
|
code: 7000,
|
||||||
|
},
|
||||||
|
ConfigNotFound: {
|
||||||
|
code: 7001,
|
||||||
|
message: (configFile: string) =>
|
||||||
|
`Unable to resolve --config "${configFile}"! Does the file exist?`,
|
||||||
|
},
|
||||||
|
ConfigLegacyKey: {
|
||||||
|
code: 7002,
|
||||||
|
message: (legacyConfigKey: string) => `Legacy configuration detected: "${legacyConfigKey}".`,
|
||||||
|
hint: 'Please update your configuration to the new format!\nSee https://astro.build/config for more information.',
|
||||||
|
},
|
||||||
|
// Generic catch-all
|
||||||
|
UnknownError: {
|
||||||
|
code: 99999,
|
||||||
|
},
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
type ValueOf<T> = T[keyof T];
|
||||||
|
export type AstroErrorCodes = ValueOf<{
|
||||||
|
[T in keyof typeof AstroErrorData]: typeof AstroErrorData[T]['code'];
|
||||||
|
}>;
|
|
@ -1,9 +1,10 @@
|
||||||
import type { DiagnosticCode } from '@astrojs/compiler/shared/diagnostics.js';
|
import type { DiagnosticCode } from '@astrojs/compiler/shared/diagnostics.js';
|
||||||
import { AstroErrorCodes } from './codes.js';
|
import { AstroErrorCodes } from './errors-data.js';
|
||||||
import { codeFrame } from './printer.js';
|
import { codeFrame } from './printer.js';
|
||||||
|
import { getErrorDataByCode } from './utils.js';
|
||||||
|
|
||||||
interface ErrorProperties {
|
interface ErrorProperties {
|
||||||
errorCode: AstroErrorCodes | DiagnosticCode;
|
code: AstroErrorCodes | DiagnosticCode;
|
||||||
name?: string;
|
name?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
location?: ErrorLocation;
|
location?: ErrorLocation;
|
||||||
|
@ -19,31 +20,32 @@ export interface ErrorLocation {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorTypes =
|
type ErrorTypes =
|
||||||
| 'CSSError'
|
| 'AstroError'
|
||||||
| 'CompilerError'
|
| 'CompilerError'
|
||||||
| 'RuntimeError'
|
| 'CSSError'
|
||||||
| 'MarkdownError'
|
| 'MarkdownError'
|
||||||
| 'AstroAggregateError';
|
| 'InternalError'
|
||||||
|
| 'AggregateError';
|
||||||
|
|
||||||
export class AstroError extends Error {
|
export class AstroError extends Error {
|
||||||
public errorCode: AstroErrorCodes | DiagnosticCode;
|
public code: AstroErrorCodes | DiagnosticCode;
|
||||||
public loc: ErrorLocation | undefined;
|
public loc: ErrorLocation | undefined;
|
||||||
public hint: string | undefined;
|
public hint: string | undefined;
|
||||||
public frame: string | undefined;
|
public frame: string | undefined;
|
||||||
|
|
||||||
type: ErrorTypes | undefined;
|
type: ErrorTypes = 'AstroError';
|
||||||
|
|
||||||
constructor(props: ErrorProperties, ...params: any) {
|
constructor(props: ErrorProperties, ...params: any) {
|
||||||
super(...params);
|
super(...params);
|
||||||
|
|
||||||
const { errorCode, name, message, stack, location, hint, frame } = props;
|
const { code, name, message, stack, location, hint, frame } = props;
|
||||||
|
|
||||||
this.errorCode = errorCode;
|
this.code = code;
|
||||||
if (name) {
|
if (name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
} else {
|
} else {
|
||||||
// If we don't have a name, let's generate one from the code
|
// If we don't have a name, let's generate one from the code
|
||||||
this.name = AstroErrorCodes[errorCode];
|
this.name = getErrorDataByCode(this.code)?.name ?? 'UnknownError';
|
||||||
}
|
}
|
||||||
if (message) this.message = message;
|
if (message) this.message = message;
|
||||||
// Only set this if we actually have a stack passed, otherwise uses Error's
|
// Only set this if we actually have a stack passed, otherwise uses Error's
|
||||||
|
@ -53,9 +55,10 @@ export class AstroError extends Error {
|
||||||
this.frame = frame;
|
this.frame = frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setErrorCode(errorCode: AstroErrorCodes | DiagnosticCode) {
|
public setErrorCode(errorCode: AstroErrorCodes) {
|
||||||
this.errorCode = errorCode;
|
this.code = errorCode;
|
||||||
this.name = AstroErrorCodes[errorCode];
|
|
||||||
|
this.name = getErrorDataByCode(this.code)?.name ?? 'UnknownError';
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLocation(location: ErrorLocation): void {
|
public setLocation(location: ErrorLocation): void {
|
||||||
|
@ -77,51 +80,52 @@ export class AstroError extends Error {
|
||||||
public setFrame(source: string, location: ErrorLocation): void {
|
public setFrame(source: string, location: ErrorLocation): void {
|
||||||
this.frame = codeFrame(source, location);
|
this.frame = codeFrame(source, location);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class CSSError extends AstroError {
|
static is(err: Error | unknown): err is AstroError {
|
||||||
type: ErrorTypes = 'CSSError';
|
return (err as AstroError).type === 'AstroError';
|
||||||
|
|
||||||
static is(err: Error | unknown): boolean {
|
|
||||||
return (err as CSSError).type === 'CSSError';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CompilerError extends AstroError {
|
export class CompilerError extends AstroError {
|
||||||
type: ErrorTypes = 'CompilerError';
|
type: ErrorTypes = 'CompilerError';
|
||||||
|
|
||||||
constructor(
|
constructor(props: Omit<ErrorProperties, 'code'> & { code: DiagnosticCode }, ...params: any) {
|
||||||
props: ErrorProperties & { errorCode: DiagnosticCode | AstroErrorCodes.UnknownCompilerError },
|
|
||||||
...params: any
|
|
||||||
) {
|
|
||||||
super(props, ...params);
|
super(props, ...params);
|
||||||
|
|
||||||
this.name = 'CompilerError';
|
this.name = 'CompilerError';
|
||||||
}
|
}
|
||||||
|
|
||||||
static is(err: Error | unknown): boolean {
|
static is(err: Error | unknown): err is CompilerError {
|
||||||
return (err as CompilerError).type === 'CompilerError';
|
return (err as CompilerError).type === 'CompilerError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RuntimeError extends AstroError {
|
export class CSSError extends AstroError {
|
||||||
type: ErrorTypes = 'RuntimeError';
|
type: ErrorTypes = 'CSSError';
|
||||||
|
|
||||||
static is(err: Error | unknown): boolean {
|
static is(err: Error | unknown): err is CSSError {
|
||||||
return (err as RuntimeError).type === 'RuntimeError';
|
return (err as CSSError).type === 'CSSError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MarkdownError extends AstroError {
|
export class MarkdownError extends AstroError {
|
||||||
type: ErrorTypes = 'MarkdownError';
|
type: ErrorTypes = 'MarkdownError';
|
||||||
|
|
||||||
static is(err: Error | unknown): boolean {
|
static is(err: Error | unknown): err is MarkdownError {
|
||||||
return (err as MarkdownError).type === 'MarkdownError';
|
return (err as MarkdownError).type === 'MarkdownError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InternalError extends AstroError {
|
||||||
|
type: ErrorTypes = 'InternalError';
|
||||||
|
|
||||||
|
static is(err: Error | unknown): err is InternalError {
|
||||||
|
return (err as InternalError).type === 'InternalError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AggregateError extends AstroError {
|
export class AggregateError extends AstroError {
|
||||||
type: ErrorTypes = 'AstroAggregateError';
|
type: ErrorTypes = 'AggregateError';
|
||||||
errors: AstroError[];
|
errors: AstroError[];
|
||||||
|
|
||||||
// Despite being a collection of errors, AggregateError still needs to have a main error attached to it
|
// Despite being a collection of errors, AggregateError still needs to have a main error attached to it
|
||||||
|
@ -132,8 +136,8 @@ export class AggregateError extends AstroError {
|
||||||
this.errors = props.errors;
|
this.errors = props.errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
static is(err: Error | unknown): boolean {
|
static is(err: Error | unknown): err is AggregateError {
|
||||||
return (err as AggregateError).type === 'AstroAggregateError';
|
return (err as AggregateError).type === 'AggregateError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +147,7 @@ export class AggregateError extends AstroError {
|
||||||
*/
|
*/
|
||||||
export interface ErrorWithMetadata {
|
export interface ErrorWithMetadata {
|
||||||
[name: string]: any;
|
[name: string]: any;
|
||||||
|
name: string;
|
||||||
type?: ErrorTypes;
|
type?: ErrorTypes;
|
||||||
message: string;
|
message: string;
|
||||||
stack: string;
|
stack: string;
|
||||||
|
@ -157,4 +162,5 @@ export interface ErrorWithMetadata {
|
||||||
line?: number;
|
line?: number;
|
||||||
column?: number;
|
column?: number;
|
||||||
};
|
};
|
||||||
|
cause?: any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
export { AstroErrorCodes } from './codes.js';
|
|
||||||
export type { ErrorLocation, ErrorWithMetadata } from './errors';
|
export type { ErrorLocation, ErrorWithMetadata } from './errors';
|
||||||
export {
|
export { AstroErrorData, type AstroErrorCodes } from './errors-data.js';
|
||||||
AggregateError,
|
export { AggregateError, AstroError, CompilerError, CSSError, MarkdownError } from './errors.js';
|
||||||
AstroError,
|
|
||||||
CompilerError,
|
|
||||||
CSSError,
|
|
||||||
MarkdownError,
|
|
||||||
RuntimeError,
|
|
||||||
} from './errors.js';
|
|
||||||
export { codeFrame } from './printer.js';
|
export { codeFrame } from './printer.js';
|
||||||
export { collectInfoFromStacktrace, createSafeError, positionAt } from './utils.js';
|
export { createSafeError, positionAt } from './utils.js';
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import eol from 'eol';
|
|
||||||
import type { ErrorLocation } from './errors.js';
|
import type { ErrorLocation } from './errors.js';
|
||||||
|
import { normalizeLF } from './utils.js';
|
||||||
|
|
||||||
/** Generate a code frame from string and an error location */
|
/** Generate a code frame from string and an error location */
|
||||||
export function codeFrame(src: string, loc: ErrorLocation): string {
|
export function codeFrame(src: string, loc: ErrorLocation): string {
|
||||||
if (!loc || loc.line === undefined || loc.column === undefined) {
|
if (!loc || loc.line === undefined || loc.column === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const lines = eol
|
const lines = normalizeLF(src)
|
||||||
.lf(src)
|
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((ln) => ln.replace(/\t/g, ' '));
|
.map((ln) => ln.replace(/\t/g, ' '));
|
||||||
// grab 2 lines before, and 3 lines after focused line
|
// grab 2 lines before, and 3 lines after focused line
|
||||||
|
|
|
@ -1,54 +1,5 @@
|
||||||
import eol from 'eol';
|
import { DiagnosticCode } from '@astrojs/compiler/shared/diagnostics.js';
|
||||||
import stripAnsi from 'strip-ansi';
|
import { AstroErrorCodes, AstroErrorData } from './errors-data.js';
|
||||||
import type { SSRError } from '../../@types/astro.js';
|
|
||||||
|
|
||||||
export function collectInfoFromStacktrace(error: SSRError): SSRError {
|
|
||||||
if (!error.stack) return error;
|
|
||||||
|
|
||||||
// normalize error stack line-endings to \n
|
|
||||||
error.stack = eol.lf(error.stack);
|
|
||||||
const stackText = stripAnsi(error.stack);
|
|
||||||
|
|
||||||
// Try to find possible location from stack if we don't have one
|
|
||||||
if (!error.loc || (!error.loc.column && !error.loc.line)) {
|
|
||||||
const possibleFilePath =
|
|
||||||
error.loc?.file ||
|
|
||||||
error.pluginCode ||
|
|
||||||
error.id ||
|
|
||||||
// TODO: this could be better, `src` might be something else
|
|
||||||
stackText.split('\n').find((ln) => ln.includes('src') || ln.includes('node_modules'));
|
|
||||||
const source = possibleFilePath?.replace(/^[^(]+\(([^)]+).*$/, '$1').replace(/^\s+at\s+/, '');
|
|
||||||
|
|
||||||
const [file, line, column] = source?.split(':') ?? [];
|
|
||||||
if (line && column) {
|
|
||||||
error.loc = {
|
|
||||||
file,
|
|
||||||
line: Number.parseInt(line),
|
|
||||||
column: Number.parseInt(column),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive plugin from stack (if possible)
|
|
||||||
if (!error.plugin) {
|
|
||||||
error.plugin =
|
|
||||||
/withastro\/astro\/packages\/integrations\/([\w-]+)/gim.exec(stackText)?.at(1) ||
|
|
||||||
/(@astrojs\/[\w-]+)\/(server|client|index)/gim.exec(stackText)?.at(1) ||
|
|
||||||
undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize stack (remove `/@fs/` urls, etc)
|
|
||||||
error.stack = cleanErrorStack(error.stack);
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanErrorStack(stack: string) {
|
|
||||||
return stack
|
|
||||||
.split(/\n/g)
|
|
||||||
.map((l) => l.replace(/\/@fs\//g, '/'))
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the line and character based on the offset
|
* Get the line and character based on the offset
|
||||||
|
@ -125,3 +76,18 @@ export function createSafeError(err: any): Error {
|
||||||
? err
|
? err
|
||||||
: new Error(JSON.stringify(err));
|
: new Error(JSON.stringify(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeLF(code: string) {
|
||||||
|
return code.replace(/\r\n|\r(?!\n)|\n/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorDataByCode(code: AstroErrorCodes | DiagnosticCode) {
|
||||||
|
const entry = Object.entries(AstroErrorData).find((data) => data[1].code === code);
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
return {
|
||||||
|
name: entry[0],
|
||||||
|
data: entry[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -278,6 +278,17 @@ export function formatErrorMessage(err: ErrorWithMetadata, args: string[] = []):
|
||||||
args.push(dim(err.stack));
|
args.push(dim(err.stack));
|
||||||
args.push(``);
|
args.push(``);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.cause) {
|
||||||
|
args.push(` ${bold('Cause:')}`);
|
||||||
|
if (err.cause instanceof Error) {
|
||||||
|
args.push(dim(err.cause.stack ?? err.cause.toString()));
|
||||||
|
} else {
|
||||||
|
args.push(JSON.stringify(err.cause));
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(``);
|
||||||
|
}
|
||||||
return args.join('\n');
|
return args.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { attachToResponse } from '../cookies/index.js';
|
||||||
import { getParams } from '../routing/params.js';
|
import { getParams } from '../routing/params.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
|
|
||||||
interface GetParamsAndPropsOptions {
|
interface GetParamsAndPropsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
|
@ -45,7 +46,7 @@ export async function getParamsAndProps(
|
||||||
routeCacheEntry = await callGetStaticPaths({ mod, route, isValidate: true, logging, ssr });
|
routeCacheEntry = await callGetStaticPaths({ mod, route, isValidate: true, logging, ssr });
|
||||||
routeCache.set(route, routeCacheEntry);
|
routeCache.set(route, routeCacheEntry);
|
||||||
}
|
}
|
||||||
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params);
|
const matchedStaticPath = findPathItemByKey(routeCacheEntry.staticPaths, params, route);
|
||||||
if (!matchedStaticPath && !ssr) {
|
if (!matchedStaticPath && !ssr) {
|
||||||
return GetParamsAndPropsError.NoMatchingStaticPath;
|
return GetParamsAndPropsError.NoMatchingStaticPath;
|
||||||
}
|
}
|
||||||
|
@ -71,9 +72,13 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
|
||||||
});
|
});
|
||||||
|
|
||||||
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
if (paramsAndPropsRes === GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`[getStaticPath] route pattern matched, but no matching static path found. (${ctx.pathname})`
|
...AstroErrorData.NoMatchingStaticPathFound,
|
||||||
);
|
message: AstroErrorData.NoMatchingStaticPathFound.message(ctx.pathname),
|
||||||
|
hint: ctx.route?.component
|
||||||
|
? AstroErrorData.NoMatchingStaticPathFound.hint([ctx.route?.component])
|
||||||
|
: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const [params, pageProps] = paramsAndPropsRes;
|
const [params, pageProps] = paramsAndPropsRes;
|
||||||
|
|
||||||
|
@ -114,7 +119,14 @@ export async function renderPage(mod: ComponentInstance, ctx: RenderContext, env
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await runtimeRenderPage(result, Component, pageProps, null, env.streaming);
|
const response = await runtimeRenderPage(
|
||||||
|
result,
|
||||||
|
Component,
|
||||||
|
pageProps,
|
||||||
|
null,
|
||||||
|
env.streaming,
|
||||||
|
ctx.route
|
||||||
|
);
|
||||||
|
|
||||||
// If there is an Astro.cookies instance, attach it to the response so that
|
// If there is an Astro.cookies instance, attach it to the response so that
|
||||||
// adapters can grab the Set-Cookie headers.
|
// adapters can grab the Set-Cookie headers.
|
||||||
|
|
|
@ -13,16 +13,19 @@ import type {
|
||||||
import { renderSlot, stringifyChunk } from '../../runtime/server/index.js';
|
import { renderSlot, stringifyChunk } from '../../runtime/server/index.js';
|
||||||
import { renderJSX } from '../../runtime/server/jsx.js';
|
import { renderJSX } from '../../runtime/server/jsx.js';
|
||||||
import { AstroCookies } from '../cookies/index.js';
|
import { AstroCookies } from '../cookies/index.js';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { LogOptions, warn } from '../logger/core.js';
|
import { LogOptions, warn } from '../logger/core.js';
|
||||||
import { isScriptRequest } from './script.js';
|
import { isScriptRequest } from './script.js';
|
||||||
import { isCSSRequest } from './util.js';
|
import { isCSSRequest } from './util.js';
|
||||||
|
|
||||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||||
|
|
||||||
function onlyAvailableInSSR(name: string) {
|
function onlyAvailableInSSR(name: 'Astro.redirect') {
|
||||||
return function _onlyAvailableInSSR() {
|
return function _onlyAvailableInSSR() {
|
||||||
// TODO add more guidance when we have docs and adapters.
|
switch (name) {
|
||||||
throw new Error(`Oops, you are trying to use ${name}, which is only available with SSR.`);
|
case 'Astro.redirect':
|
||||||
|
throw new AstroError(AstroErrorData.StaticRedirectNotAllowed);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,9 +69,10 @@ class Slots {
|
||||||
if (slots) {
|
if (slots) {
|
||||||
for (const key of Object.keys(slots)) {
|
for (const key of Object.keys(slots)) {
|
||||||
if ((this as any)[key] !== undefined) {
|
if ((this as any)[key] !== undefined) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`Unable to create a slot named "${key}". "${key}" is a reserved slot name!\nPlease update the name of this slot.`
|
...AstroErrorData.ReservedSlotName,
|
||||||
);
|
message: AstroErrorData.ReservedSlotName.message(key),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Object.defineProperty(this, key, {
|
Object.defineProperty(this, key, {
|
||||||
get() {
|
get() {
|
||||||
|
@ -172,13 +176,14 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
||||||
get clientAddress() {
|
get clientAddress() {
|
||||||
if (!(clientAddressSymbol in request)) {
|
if (!(clientAddressSymbol in request)) {
|
||||||
if (args.adapterName) {
|
if (args.adapterName) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`Astro.clientAddress is not available in the ${args.adapterName} adapter. File an issue with the adapter to add support.`
|
...AstroErrorData.SSRClientAddressNotAvailableInAdapter,
|
||||||
);
|
message: AstroErrorData.SSRClientAddressNotAvailableInAdapter.message(
|
||||||
|
args.adapterName
|
||||||
|
),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new AstroError(AstroErrorData.StaticClientAddressNotAvailable);
|
||||||
`Astro.clientAddress is not available in your environment. Ensure that you are using an SSR adapter that supports this feature.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
RouteData,
|
RouteData,
|
||||||
RuntimeMode,
|
RuntimeMode,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import { debug, LogOptions, warn } from '../logger/core.js';
|
import { debug, LogOptions, warn } from '../logger/core.js';
|
||||||
|
|
||||||
import { stringifyParams } from '../routing/params.js';
|
import { stringifyParams } from '../routing/params.js';
|
||||||
|
@ -28,39 +29,39 @@ export async function callGetStaticPaths({
|
||||||
route,
|
route,
|
||||||
ssr,
|
ssr,
|
||||||
}: CallGetStaticPathsOptions): Promise<RouteCacheEntry> {
|
}: CallGetStaticPathsOptions): Promise<RouteCacheEntry> {
|
||||||
validateDynamicRouteModule(mod, { ssr, logging });
|
validateDynamicRouteModule(mod, { ssr, logging, route });
|
||||||
// No static paths in SSR mode. Return an empty RouteCacheEntry.
|
// No static paths in SSR mode. Return an empty RouteCacheEntry.
|
||||||
if (ssr) {
|
if (ssr) {
|
||||||
return { staticPaths: Object.assign([], { keyed: new Map() }) };
|
return { staticPaths: Object.assign([], { keyed: new Map() }) };
|
||||||
}
|
}
|
||||||
// Add a check here to my TypeScript happy.
|
// Add a check here to make TypeScript happy.
|
||||||
// This is already checked in validateDynamicRouteModule().
|
// This is already checked in validateDynamicRouteModule().
|
||||||
if (!mod.getStaticPaths) {
|
if (!mod.getStaticPaths) {
|
||||||
throw new Error('Unexpected Error.');
|
throw new Error('Unexpected Error.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate your static paths.
|
// Calculate your static paths.
|
||||||
let staticPaths: GetStaticPathsResult = [];
|
let staticPaths: GetStaticPathsResult = [];
|
||||||
staticPaths = (
|
staticPaths = await mod.getStaticPaths({
|
||||||
await mod.getStaticPaths({
|
|
||||||
paginate: generatePaginateFunction(route),
|
paginate: generatePaginateFunction(route),
|
||||||
rss() {
|
rss() {
|
||||||
throw new Error(
|
throw new AstroError(AstroErrorData.GetStaticPathsDeprecatedRSS);
|
||||||
'The RSS helper has been removed from getStaticPaths! Try the new @astrojs/rss package instead. See https://docs.astro.build/en/guides/rss/'
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
).flat();
|
|
||||||
|
|
||||||
|
if (isValidate) {
|
||||||
|
validateGetStaticPathsResult(staticPaths, logging, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
staticPaths = staticPaths.flat();
|
||||||
const keyedStaticPaths = staticPaths as GetStaticPathsResultKeyed;
|
const keyedStaticPaths = staticPaths as GetStaticPathsResultKeyed;
|
||||||
keyedStaticPaths.keyed = new Map<string, GetStaticPathsItem>();
|
keyedStaticPaths.keyed = new Map<string, GetStaticPathsItem>();
|
||||||
|
|
||||||
for (const sp of keyedStaticPaths) {
|
for (const sp of keyedStaticPaths) {
|
||||||
const paramsKey = stringifyParams(sp.params);
|
const paramsKey = stringifyParams(sp.params, route.component);
|
||||||
keyedStaticPaths.keyed.set(paramsKey, sp);
|
keyedStaticPaths.keyed.set(paramsKey, sp);
|
||||||
}
|
}
|
||||||
if (isValidate) {
|
|
||||||
validateGetStaticPathsResult(keyedStaticPaths, logging);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
staticPaths: keyedStaticPaths,
|
staticPaths: keyedStaticPaths,
|
||||||
};
|
};
|
||||||
|
@ -109,8 +110,12 @@ export class RouteCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findPathItemByKey(staticPaths: GetStaticPathsResultKeyed, params: Params) {
|
export function findPathItemByKey(
|
||||||
const paramsKey = stringifyParams(params);
|
staticPaths: GetStaticPathsResultKeyed,
|
||||||
|
params: Params,
|
||||||
|
route: RouteData
|
||||||
|
) {
|
||||||
|
const paramsKey = stringifyParams(params, route.component);
|
||||||
const matchedStaticPath = staticPaths.keyed.get(paramsKey);
|
const matchedStaticPath = staticPaths.keyed.get(paramsKey);
|
||||||
if (matchedStaticPath) {
|
if (matchedStaticPath) {
|
||||||
return matchedStaticPath;
|
return matchedStaticPath;
|
||||||
|
|
|
@ -114,11 +114,6 @@ function isSpread(str: string) {
|
||||||
function validateSegment(segment: string, file = '') {
|
function validateSegment(segment: string, file = '') {
|
||||||
if (!file) file = segment;
|
if (!file) file = segment;
|
||||||
|
|
||||||
if (/^\$/.test(segment)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid route ${file} \u2014 Astro's Collections API has been replaced by dynamic route params.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (/\]\[/.test(segment)) {
|
if (/\]\[/.test(segment)) {
|
||||||
throw new Error(`Invalid route ${file} \u2014 parameters must be separated`);
|
throw new Error(`Invalid route ${file} \u2014 parameters must be separated`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,10 @@ export function getParams(array: string[]) {
|
||||||
* values and create a stringified key for the route
|
* values and create a stringified key for the route
|
||||||
* that can be used to match request routes
|
* that can be used to match request routes
|
||||||
*/
|
*/
|
||||||
export function stringifyParams(params: Params) {
|
export function stringifyParams(params: Params, routeComponent: string) {
|
||||||
// validate parameter values then stringify each value
|
// validate parameter values then stringify each value
|
||||||
const validatedParams = Object.entries(params).reduce((acc, next) => {
|
const validatedParams = Object.entries(params).reduce((acc, next) => {
|
||||||
validateGetStaticPathsParameter(next);
|
validateGetStaticPathsParameter(next, routeComponent);
|
||||||
const [key, value] = next;
|
const [key, value] = next;
|
||||||
acc[key] = typeof value === 'undefined' ? undefined : `${value}`;
|
acc[key] = typeof value === 'undefined' ? undefined : `${value}`;
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
import type { ComponentInstance, GetStaticPathsResult } from '../../@types/astro';
|
import type { ComponentInstance, GetStaticPathsResult, RouteData } from '../../@types/astro';
|
||||||
|
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||||
import type { LogOptions } from '../logger/core';
|
import type { LogOptions } from '../logger/core';
|
||||||
import { warn } from '../logger/core.js';
|
import { warn } from '../logger/core.js';
|
||||||
|
|
||||||
const VALID_PARAM_TYPES = ['string', 'number', 'undefined'];
|
const VALID_PARAM_TYPES = ['string', 'number', 'undefined'];
|
||||||
|
|
||||||
/** Throws error for invalid parameter in getStaticPaths() response */
|
/** Throws error for invalid parameter in getStaticPaths() response */
|
||||||
export function validateGetStaticPathsParameter([key, value]: [string, any]) {
|
export function validateGetStaticPathsParameter([key, value]: [string, any], route: string) {
|
||||||
if (!VALID_PARAM_TYPES.includes(typeof value)) {
|
if (!VALID_PARAM_TYPES.includes(typeof value)) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`[getStaticPaths] invalid route parameter for "${key}". Expected a string or number, received \`${value}\` ("${typeof value}")`
|
...AstroErrorData.GetStaticPathsInvalidRouteParam,
|
||||||
);
|
message: AstroErrorData.GetStaticPathsInvalidRouteParam.message(key, value),
|
||||||
|
location: {
|
||||||
|
file: route,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,57 +24,76 @@ export function validateDynamicRouteModule(
|
||||||
{
|
{
|
||||||
ssr,
|
ssr,
|
||||||
logging,
|
logging,
|
||||||
|
route,
|
||||||
}: {
|
}: {
|
||||||
ssr: boolean;
|
ssr: boolean;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
|
route: RouteData;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if ((mod as any).createCollection) {
|
|
||||||
throw new Error(`[createCollection] deprecated. Please use getStaticPaths() instead.`);
|
|
||||||
}
|
|
||||||
if (ssr && mod.getStaticPaths) {
|
if (ssr && mod.getStaticPaths) {
|
||||||
warn(logging, 'getStaticPaths', 'getStaticPaths() is ignored when "output: server" is set.');
|
warn(logging, 'getStaticPaths', 'getStaticPaths() is ignored when "output: server" is set.');
|
||||||
}
|
}
|
||||||
if (!ssr && !mod.getStaticPaths) {
|
if (!ssr && !mod.getStaticPaths) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`[getStaticPaths] getStaticPaths() function is required.
|
...AstroErrorData.GetStaticPathsRequired,
|
||||||
Make sure that you \`export\` a \`getStaticPaths\` function from your dynamic route.
|
location: { file: route.component },
|
||||||
Alternatively, set \`output: "server"\` in your Astro config file to switch to a non-static server build. `
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Throw error for malformed getStaticPaths() response */
|
/** Throw error and log warnings for malformed getStaticPaths() response */
|
||||||
export function validateGetStaticPathsResult(result: GetStaticPathsResult, logging: LogOptions) {
|
export function validateGetStaticPathsResult(
|
||||||
|
result: GetStaticPathsResult,
|
||||||
|
logging: LogOptions,
|
||||||
|
route: RouteData
|
||||||
|
) {
|
||||||
if (!Array.isArray(result)) {
|
if (!Array.isArray(result)) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`[getStaticPaths] invalid return value. Expected an array of path objects, but got \`${JSON.stringify(
|
...AstroErrorData.InvalidGetStaticPathsReturn,
|
||||||
result
|
message: AstroErrorData.InvalidGetStaticPathsReturn.message(typeof result),
|
||||||
)}\`.`
|
location: {
|
||||||
);
|
file: route.component,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
result.forEach((pathObject) => {
|
result.forEach((pathObject) => {
|
||||||
if (!pathObject.params) {
|
if (
|
||||||
warn(
|
pathObject.params === undefined ||
|
||||||
logging,
|
pathObject.params === null ||
|
||||||
'getStaticPaths',
|
(pathObject.params && Object.keys(pathObject.params).length === 0)
|
||||||
`invalid path object. Expected an object with key \`params\`, but got \`${JSON.stringify(
|
) {
|
||||||
pathObject
|
throw new AstroError({
|
||||||
)}\`. Skipped.`
|
...AstroErrorData.GetStaticPathsExpectedParams,
|
||||||
);
|
location: {
|
||||||
return;
|
file: route.component,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof pathObject.params !== 'object') {
|
||||||
|
throw new AstroError({
|
||||||
|
...AstroErrorData.InvalidStaticPathParam,
|
||||||
|
message: AstroErrorData.InvalidStaticPathParam.message(typeof pathObject.params),
|
||||||
|
location: {
|
||||||
|
file: route.component,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace those with errors? They technically don't crash the build, but users might miss the warning. - erika, 2022-11-07
|
||||||
for (const [key, val] of Object.entries(pathObject.params)) {
|
for (const [key, val] of Object.entries(pathObject.params)) {
|
||||||
if (!(typeof val === 'undefined' || typeof val === 'string')) {
|
if (!(typeof val === 'undefined' || typeof val === 'string' || typeof val === 'number')) {
|
||||||
warn(
|
warn(
|
||||||
logging,
|
logging,
|
||||||
'getStaticPaths',
|
'getStaticPaths',
|
||||||
`invalid path param: ${key}. A string value was expected, but got \`${JSON.stringify(
|
`invalid path param: ${key}. A string, number or undefined value was expected, but got \`${JSON.stringify(
|
||||||
val
|
val
|
||||||
)}\`.`
|
)}\`.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (val === '') {
|
if (typeof val === 'string' && val === '') {
|
||||||
warn(
|
warn(
|
||||||
logging,
|
logging,
|
||||||
'getStaticPaths',
|
'getStaticPaths',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { AstroErrorCodes, ErrorWithMetadata } from '../core/errors/index.js';
|
import { AstroErrorData, ErrorWithMetadata } from '../core/errors/index.js';
|
||||||
|
|
||||||
const EVENT_ERROR = 'ASTRO_CLI_ERROR';
|
const EVENT_ERROR = 'ASTRO_CLI_ERROR';
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export function eventConfigError({
|
||||||
isFatal: boolean;
|
isFatal: boolean;
|
||||||
}): { eventName: string; payload: ConfigErrorEventPayload }[] {
|
}): { eventName: string; payload: ConfigErrorEventPayload }[] {
|
||||||
const payload: ConfigErrorEventPayload = {
|
const payload: ConfigErrorEventPayload = {
|
||||||
code: AstroErrorCodes.ConfigError,
|
code: AstroErrorData.UnknownConfigError.code,
|
||||||
isFatal,
|
isFatal,
|
||||||
isConfig: true,
|
isConfig: true,
|
||||||
cliCommand: cmd,
|
cliCommand: cmd,
|
||||||
|
@ -65,7 +65,7 @@ export function eventError({
|
||||||
isFatal: boolean;
|
isFatal: boolean;
|
||||||
}): { eventName: string; payload: ErrorEventPayload }[] {
|
}): { eventName: string; payload: ErrorEventPayload }[] {
|
||||||
const payload: ErrorEventPayload = {
|
const payload: ErrorEventPayload = {
|
||||||
code: err.code || AstroErrorCodes.UnknownError,
|
code: err.code || AstroErrorData.UnknownError.code,
|
||||||
plugin: err.plugin,
|
plugin: err.plugin,
|
||||||
cliCommand: cmd,
|
cliCommand: cmd,
|
||||||
isFatal: isFatal,
|
isFatal: isFatal,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { PluginObj } from '@babel/core';
|
import type { PluginObj } from '@babel/core';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
|
import { AstroErrorData } from '../core/errors/errors-data.js';
|
||||||
|
import { AstroError } from '../core/errors/errors.js';
|
||||||
import { resolvePath } from '../core/util.js';
|
import { resolvePath } from '../core/util.js';
|
||||||
import { HydrationDirectiveProps } from '../runtime/server/hydration.js';
|
import { HydrationDirectiveProps } from '../runtime/server/hydration.js';
|
||||||
import type { PluginMetadata } from '../vite-plugin-astro/types';
|
import type { PluginMetadata } from '../vite-plugin-astro/types';
|
||||||
|
@ -310,11 +312,10 @@ export default function astroJSX(): PluginObj {
|
||||||
addClientMetadata(parentNode, meta);
|
addClientMetadata(parentNode, meta);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`Unable to match <${getTagName(
|
...AstroErrorData.NoMatchingImport,
|
||||||
parentNode
|
message: AstroErrorData.NoMatchingImport.message(getTagName(parentNode)),
|
||||||
)}> with client:* directive to an import statement!`
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
||||||
import { escapeHTML } from './escape.js';
|
import { escapeHTML } from './escape.js';
|
||||||
import { serializeProps } from './serialize.js';
|
import { serializeProps } from './serialize.js';
|
||||||
import { serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
|
@ -27,7 +28,10 @@ interface ExtractedProps {
|
||||||
|
|
||||||
// Used to extract the directives, aka `client:load` information about a component.
|
// Used to extract the directives, aka `client:load` information about a component.
|
||||||
// Finds these special props and removes them from what gets passed into the component.
|
// Finds these special props and removes them from what gets passed into the component.
|
||||||
export function extractDirectives(inputProps: Record<string | number, any>): ExtractedProps {
|
export function extractDirectives(
|
||||||
|
displayName: string,
|
||||||
|
inputProps: Record<string | number, any>
|
||||||
|
): ExtractedProps {
|
||||||
let extracted: ExtractedProps = {
|
let extracted: ExtractedProps = {
|
||||||
isPage: false,
|
isPage: false,
|
||||||
hydration: null,
|
hydration: null,
|
||||||
|
@ -83,9 +87,10 @@ export function extractDirectives(inputProps: Record<string | number, any>): Ext
|
||||||
extracted.hydration.directive === 'media' &&
|
extracted.hydration.directive === 'media' &&
|
||||||
typeof extracted.hydration.value !== 'string'
|
typeof extracted.hydration.value !== 'string'
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
'Error: Media query must be provided for "client:media", similar to client:media="(max-width: 600px)"'
|
...AstroErrorData.MissingMediaQueryDirective,
|
||||||
);
|
message: AstroErrorData.MissingMediaQueryDirective.message(displayName),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import type { AstroComponentMetadata, SSRLoadedRenderer, SSRResult } from '../../../@types/astro';
|
import type {
|
||||||
|
AstroComponentMetadata,
|
||||||
|
RouteData,
|
||||||
|
SSRLoadedRenderer,
|
||||||
|
SSRResult,
|
||||||
|
} from '../../../@types/astro';
|
||||||
import type { RenderInstruction } from './types.js';
|
import type { RenderInstruction } from './types.js';
|
||||||
|
|
||||||
|
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
||||||
import { HTMLBytes, markHTMLString } from '../escape.js';
|
import { HTMLBytes, markHTMLString } from '../escape.js';
|
||||||
import { extractDirectives, generateHydrateScript } from '../hydration.js';
|
import { extractDirectives, generateHydrateScript } from '../hydration.js';
|
||||||
import { serializeProps } from '../serialize.js';
|
import { serializeProps } from '../serialize.js';
|
||||||
|
@ -27,9 +33,15 @@ function guessRenderers(componentUrl?: string): string[] {
|
||||||
return ['@astrojs/vue'];
|
return ['@astrojs/vue'];
|
||||||
case 'jsx':
|
case 'jsx':
|
||||||
case 'tsx':
|
case 'tsx':
|
||||||
return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue (jsx)'];
|
return ['@astrojs/react', '@astrojs/preact', '@astrojs/solid', '@astrojs/vue (jsx)'];
|
||||||
default:
|
default:
|
||||||
return ['@astrojs/react', '@astrojs/preact', '@astrojs/vue', '@astrojs/svelte'];
|
return [
|
||||||
|
'@astrojs/react',
|
||||||
|
'@astrojs/preact',
|
||||||
|
'@astrojs/solid',
|
||||||
|
'@astrojs/vue',
|
||||||
|
'@astrojs/svelte',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +66,8 @@ export async function renderComponent(
|
||||||
displayName: string,
|
displayName: string,
|
||||||
Component: unknown,
|
Component: unknown,
|
||||||
_props: Record<string | number, any>,
|
_props: Record<string | number, any>,
|
||||||
slots: any = {}
|
slots: any = {},
|
||||||
|
route?: RouteData | undefined
|
||||||
): Promise<ComponentIterable> {
|
): Promise<ComponentIterable> {
|
||||||
Component = (await Component) ?? Component;
|
Component = (await Component) ?? Component;
|
||||||
|
|
||||||
|
@ -100,7 +113,7 @@ export async function renderComponent(
|
||||||
const { renderers } = result._metadata;
|
const { renderers } = result._metadata;
|
||||||
const metadata: AstroComponentMetadata = { displayName };
|
const metadata: AstroComponentMetadata = { displayName };
|
||||||
|
|
||||||
const { hydration, isPage, props } = extractDirectives(_props);
|
const { hydration, isPage, props } = extractDirectives(displayName, _props);
|
||||||
let html = '';
|
let html = '';
|
||||||
let attrs: Record<string, string> | undefined = undefined;
|
let attrs: Record<string, string> | undefined = undefined;
|
||||||
|
|
||||||
|
@ -110,21 +123,9 @@ export async function renderComponent(
|
||||||
metadata.componentExport = hydration.componentExport;
|
metadata.componentExport = hydration.componentExport;
|
||||||
metadata.componentUrl = hydration.componentUrl;
|
metadata.componentUrl = hydration.componentUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const probableRendererNames = guessRenderers(metadata.componentUrl);
|
const probableRendererNames = guessRenderers(metadata.componentUrl);
|
||||||
|
const validRenderers = renderers.filter((r) => r.name !== 'astro:jsx');
|
||||||
if (
|
|
||||||
Array.isArray(renderers) &&
|
|
||||||
renderers.length === 0 &&
|
|
||||||
typeof Component !== 'string' &&
|
|
||||||
!componentIsHTMLElement(Component)
|
|
||||||
) {
|
|
||||||
const message = `Unable to render ${metadata.displayName}!
|
|
||||||
|
|
||||||
There are no \`integrations\` set in your \`astro.config.mjs\` file.
|
|
||||||
Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`;
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { children, slotInstructions } = await renderSlots(result, slots);
|
const { children, slotInstructions } = await renderSlots(result, slots);
|
||||||
|
|
||||||
// Call the renderers `check` hook to see if any claim this component.
|
// Call the renderers `check` hook to see if any claim this component.
|
||||||
|
@ -182,8 +183,8 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Attempt: user only has a single renderer, default to that
|
// Attempt: user only has a single renderer, default to that
|
||||||
if (!renderer && renderers.length === 1) {
|
if (!renderer && validRenderers.length === 1) {
|
||||||
renderer = renderers[0];
|
renderer = validRenderers[0];
|
||||||
}
|
}
|
||||||
// Attempt: can we guess the renderer from the export extension?
|
// Attempt: can we guess the renderer from the export extension?
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
|
@ -197,26 +198,31 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
|
||||||
// If no one claimed the renderer
|
// If no one claimed the renderer
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
if (metadata.hydrate === 'only') {
|
if (metadata.hydrate === 'only') {
|
||||||
// TODO: improve error message
|
throw new AstroError({
|
||||||
throw new Error(`Unable to render ${metadata.displayName}!
|
...AstroErrorData.NoClientOnlyHint,
|
||||||
|
message: AstroErrorData.NoClientOnlyHint.message(metadata.displayName),
|
||||||
Using the \`client:only\` hydration strategy, Astro needs a hint to use the correct renderer.
|
hint: AstroErrorData.NoClientOnlyHint.hint(
|
||||||
Did you mean to pass <${metadata.displayName} client:only="${probableRendererNames
|
probableRendererNames.map((r) => r.replace('@astrojs/', '')).join('|')
|
||||||
.map((r) => r.replace('@astrojs/', ''))
|
),
|
||||||
.join('|')}" />
|
});
|
||||||
`);
|
|
||||||
} else if (typeof Component !== 'string') {
|
} else if (typeof Component !== 'string') {
|
||||||
const matchingRenderers = renderers.filter((r) => probableRendererNames.includes(r.name));
|
const matchingRenderers = validRenderers.filter((r) =>
|
||||||
const plural = renderers.length > 1;
|
probableRendererNames.includes(r.name)
|
||||||
|
);
|
||||||
|
const plural = validRenderers.length > 1;
|
||||||
if (matchingRenderers.length === 0) {
|
if (matchingRenderers.length === 0) {
|
||||||
throw new Error(`Unable to render ${metadata.displayName}!
|
throw new AstroError({
|
||||||
|
...AstroErrorData.NoMatchingRenderer,
|
||||||
There ${plural ? 'are' : 'is'} ${renderers.length} renderer${
|
message: AstroErrorData.NoMatchingRenderer.message(
|
||||||
plural ? 's' : ''
|
metadata.displayName,
|
||||||
} configured in your \`astro.config.mjs\` file,
|
metadata?.componentUrl?.split('.').pop(),
|
||||||
but ${plural ? 'none were' : 'it was not'} able to server-side render ${metadata.displayName}.
|
plural,
|
||||||
|
validRenderers.length
|
||||||
Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '`'))}?`);
|
),
|
||||||
|
hint: AstroErrorData.NoMatchingRenderer.hint(
|
||||||
|
formatList(probableRendererNames.map((r) => '`' + r + '`'))
|
||||||
|
),
|
||||||
|
});
|
||||||
} else if (matchingRenderers.length === 1) {
|
} else if (matchingRenderers.length === 1) {
|
||||||
// We already know that renderer.ssr.check() has failed
|
// We already know that renderer.ssr.check() has failed
|
||||||
// but this will throw a much more descriptive error!
|
// but this will throw a much more descriptive error!
|
||||||
|
@ -264,9 +270,14 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
renderer.name !== '@astrojs/lit' &&
|
renderer.name !== '@astrojs/lit' &&
|
||||||
metadata.hydrate
|
metadata.hydrate
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new AstroError({
|
||||||
`${metadata.displayName} component has a \`client:${metadata.hydrate}\` directive, but no client entrypoint was provided by ${renderer.name}!`
|
...AstroErrorData.NoClientEntrypoint,
|
||||||
);
|
message: AstroErrorData.NoClientEntrypoint.message(
|
||||||
|
displayName,
|
||||||
|
metadata.hydrate,
|
||||||
|
renderer.name
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a custom element without a renderer. Because of that, render it
|
// This is a custom element without a renderer. Because of that, render it
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { SSRResult } from '../../../@types/astro';
|
import type { RouteData, SSRResult } from '../../../@types/astro';
|
||||||
import type { ComponentIterable } from './component';
|
import type { ComponentIterable } from './component';
|
||||||
import type { AstroComponentFactory } from './index';
|
import type { AstroComponentFactory } from './index';
|
||||||
|
|
||||||
|
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
||||||
import { isHTMLString } from '../escape.js';
|
import { isHTMLString } from '../escape.js';
|
||||||
import { createResponse } from '../response.js';
|
import { createResponse } from '../response.js';
|
||||||
import { isAstroComponent, isAstroComponentFactory, renderAstroComponent } from './astro.js';
|
import { isAstroComponent, isAstroComponentFactory, renderAstroComponent } from './astro.js';
|
||||||
|
@ -49,17 +50,32 @@ export async function renderPage(
|
||||||
componentFactory: AstroComponentFactory | NonAstroPageComponent,
|
componentFactory: AstroComponentFactory | NonAstroPageComponent,
|
||||||
props: any,
|
props: any,
|
||||||
children: any,
|
children: any,
|
||||||
streaming: boolean
|
streaming: boolean,
|
||||||
|
route?: RouteData | undefined
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
if (!isAstroComponentFactory(componentFactory)) {
|
if (!isAstroComponentFactory(componentFactory)) {
|
||||||
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
const pageProps: Record<string, any> = { ...(props ?? {}), 'server:root': true };
|
||||||
const output = await renderComponent(
|
|
||||||
|
let output: ComponentIterable;
|
||||||
|
|
||||||
|
try {
|
||||||
|
output = await renderComponent(
|
||||||
result,
|
result,
|
||||||
componentFactory.name,
|
componentFactory.name,
|
||||||
componentFactory,
|
componentFactory,
|
||||||
pageProps,
|
pageProps,
|
||||||
null
|
null,
|
||||||
|
route
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (AstroError.is(e) && !e.loc) {
|
||||||
|
e.setLocation({
|
||||||
|
file: route?.component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
// Accumulate the HTML string and append the head if necessary.
|
// Accumulate the HTML string and append the head if necessary.
|
||||||
const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
|
const bytes = await iterableToHTMLBytes(result, output, async (parts) => {
|
||||||
|
@ -106,6 +122,14 @@ export async function renderPage(
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// We don't have a lot of information downstream, and upstream we can't catch the error properly
|
||||||
|
// So let's add the location here
|
||||||
|
if (AstroError.is(e) && !e.loc) {
|
||||||
|
e.setLocation({
|
||||||
|
file: route?.component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
controller.error(e);
|
controller.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +147,16 @@ export async function renderPage(
|
||||||
|
|
||||||
// We double check if the file return a Response
|
// We double check if the file return a Response
|
||||||
if (!(factoryReturnValue instanceof Response)) {
|
if (!(factoryReturnValue instanceof Response)) {
|
||||||
throw new Error('Only instance of Response can be returned from an Astro file');
|
throw new AstroError({
|
||||||
|
...AstroErrorData.OnlyResponseCanBeReturned,
|
||||||
|
message: AstroErrorData.OnlyResponseCanBeReturned.message(
|
||||||
|
route?.route,
|
||||||
|
typeof factoryReturnValue
|
||||||
|
),
|
||||||
|
location: {
|
||||||
|
file: route?.component,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return factoryReturnValue;
|
return factoryReturnValue;
|
||||||
|
|
|
@ -57,13 +57,27 @@ export default function createVitePluginAstroServer({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// HACK: hide `.tip` in Vite's ErrorOverlay and replace [vite] messages with [astro]
|
// HACK: Manually replace code in Vite's overlay to fit it to our needs
|
||||||
|
// In the future, we'll instead take over the overlay entirely, which should be safer and cleaner
|
||||||
transform(code, id, opts = {}) {
|
transform(code, id, opts = {}) {
|
||||||
if (opts.ssr) return;
|
if (opts.ssr) return;
|
||||||
if (!id.includes('vite/dist/client/client.mjs')) return;
|
if (!id.includes('vite/dist/client/client.mjs')) return;
|
||||||
return code
|
return (
|
||||||
|
code
|
||||||
|
// Transform links in the message to clickable links
|
||||||
|
.replace(
|
||||||
|
"this.text('.message-body', message.trim());",
|
||||||
|
`const urlPattern = /(\\b(https?|ftp):\\/\\/[-A-Z0-9+&@#\\/%?=~_|!:,.;]*[-A-Z0-9+&@#\\/%=~_|])/gim;
|
||||||
|
function escapeHtml(unsafe){return unsafe.replace(/</g, "<").replace(/>/g, ">");}
|
||||||
|
const escapedMessage = escapeHtml(message);
|
||||||
|
this.root.querySelector(".message-body").innerHTML = escapedMessage.trim().replace(urlPattern, '<a href="$1" target="_blank">$1</a>');`
|
||||||
|
)
|
||||||
|
.replace('</style>', '.message-body a {\n color: #ededed;\n}\n</style>')
|
||||||
|
// Hide `.tip` in Vite's ErrorOverlay
|
||||||
.replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
|
.replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
|
||||||
.replace(/\[vite\]/g, '[astro]');
|
// Replace [vite] messages with [astro]
|
||||||
|
.replace(/\[vite\]/g, '[astro]')
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ export async function handleRequest(
|
||||||
const err = createSafeError(_err);
|
const err = createSafeError(_err);
|
||||||
// This is our last line of defense regarding errors where we still might have some information about the request
|
// This is our last line of defense regarding errors where we still might have some information about the request
|
||||||
// Our error should already be complete, but let's try to add a bit more through some guesswork
|
// Our error should already be complete, but let's try to add a bit more through some guesswork
|
||||||
const errorWithMetadata = collectErrorMetadata(err);
|
const errorWithMetadata = collectErrorMetadata(err, config.root);
|
||||||
|
|
||||||
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
||||||
handle500Response(moduleLoader, res, errorWithMetadata);
|
handle500Response(moduleLoader, res, errorWithMetadata);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { resolvePages } from '../core/util.js';
|
import { resolvePages } from '../core/util.js';
|
||||||
import { log404 } from './common.js';
|
import { log404 } from './common.js';
|
||||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||||
|
import { AstroErrorData } from '../core/errors/index.js';
|
||||||
|
|
||||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||||
...args: any
|
...args: any
|
||||||
|
@ -63,10 +64,14 @@ export async function matchRoute(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches.length) {
|
if (matches.length) {
|
||||||
|
const possibleRoutes = matches.flatMap((route) => route.component);
|
||||||
|
|
||||||
warn(
|
warn(
|
||||||
logging,
|
logging,
|
||||||
'getStaticPaths',
|
'getStaticPaths',
|
||||||
`Route pattern matched, but no matching static path found. (${pathname})`
|
`${AstroErrorData.NoMatchingStaticPathFound.message(
|
||||||
|
pathname
|
||||||
|
)}\n\n${AstroErrorData.NoMatchingStaticPathFound.hint(possibleRoutes)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { Plugin, ResolvedConfig } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro';
|
import type { AstroSettings } from '../@types/astro';
|
||||||
import { pagesVirtualModuleId } from '../core/app/index.js';
|
import { pagesVirtualModuleId } from '../core/app/index.js';
|
||||||
import { cachedCompilation, CompileProps } from '../core/compile/index.js';
|
import { cachedCompilation, CompileProps } from '../core/compile/index.js';
|
||||||
import { AstroErrorCodes, MarkdownError } from '../core/errors/index.js';
|
import { AstroErrorData, MarkdownError } from '../core/errors/index.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { isMarkdownFile } from '../core/util.js';
|
import { isMarkdownFile } from '../core/util.js';
|
||||||
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
|
||||||
|
@ -27,7 +27,7 @@ function safeMatter(source: string, id: string) {
|
||||||
return matter(source);
|
return matter(source);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const markdownError = new MarkdownError({
|
const markdownError = new MarkdownError({
|
||||||
errorCode: AstroErrorCodes.GenericMarkdownError,
|
code: AstroErrorData.UnknownMarkdownError.code,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
stack: err.stack,
|
stack: err.stack,
|
||||||
location: {
|
location: {
|
||||||
|
@ -36,7 +36,7 @@ function safeMatter(source: string, id: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (err.name === 'YAMLException') {
|
if (err.name === 'YAMLException') {
|
||||||
markdownError.setErrorCode(AstroErrorCodes.MarkdownFrontmatterParseError);
|
markdownError.setErrorCode(AstroErrorData.MarkdownFrontmatterParseError.code);
|
||||||
markdownError.setLocation({
|
markdownError.setLocation({
|
||||||
file: id,
|
file: id,
|
||||||
line: err.mark.line,
|
line: err.mark.line,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import { normalizePath } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import type { AstroSettings } from '../@types/astro';
|
import type { AstroSettings } from '../@types/astro';
|
||||||
import { AstroErrorCodes, MarkdownError } from '../core/errors/index.js';
|
import { AstroErrorData, MarkdownError } from '../core/errors/index.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import { warn } from '../core/logger/core.js';
|
||||||
import { isMarkdownFile } from '../core/util.js';
|
import { isMarkdownFile } from '../core/util.js';
|
||||||
|
@ -22,7 +22,7 @@ function safeMatter(source: string, id: string) {
|
||||||
return matter(source);
|
return matter(source);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const markdownError = new MarkdownError({
|
const markdownError = new MarkdownError({
|
||||||
errorCode: AstroErrorCodes.GenericMarkdownError,
|
code: AstroErrorData.UnknownMarkdownError.code,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
stack: err.stack,
|
stack: err.stack,
|
||||||
location: {
|
location: {
|
||||||
|
@ -31,7 +31,7 @@ function safeMatter(source: string, id: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (err.name === 'YAMLException') {
|
if (err.name === 'YAMLException') {
|
||||||
markdownError.setErrorCode(AstroErrorCodes.MarkdownFrontmatterParseError);
|
markdownError.setErrorCode(AstroErrorData.MarkdownFrontmatterParseError.code);
|
||||||
markdownError.setLocation({
|
markdownError.setLocation({
|
||||||
file: id,
|
file: id,
|
||||||
line: err.mark.line,
|
line: err.mark.line,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { AstroErrorCodes } from '../dist/core/errors/codes.js';
|
import { AstroErrorData } from '../dist/core/errors/errors-data.js';
|
||||||
import * as events from '../dist/events/index.js';
|
import * as events from '../dist/events/index.js';
|
||||||
|
|
||||||
describe('Events', () => {
|
describe('Events', () => {
|
||||||
|
@ -426,7 +426,7 @@ describe('Events', () => {
|
||||||
expect(event).to.deep.equal({
|
expect(event).to.deep.equal({
|
||||||
eventName: 'ASTRO_CLI_ERROR',
|
eventName: 'ASTRO_CLI_ERROR',
|
||||||
payload: {
|
payload: {
|
||||||
code: AstroErrorCodes.ConfigError,
|
code: AstroErrorData.UnknownConfigError.code,
|
||||||
isFatal: true,
|
isFatal: true,
|
||||||
isConfig: true,
|
isConfig: true,
|
||||||
cliCommand: 'COMMAND_NAME',
|
cliCommand: 'COMMAND_NAME',
|
||||||
|
@ -467,7 +467,7 @@ describe('Events', () => {
|
||||||
expect(event).to.deep.equal({
|
expect(event).to.deep.equal({
|
||||||
eventName: 'ASTRO_CLI_ERROR',
|
eventName: 'ASTRO_CLI_ERROR',
|
||||||
payload: {
|
payload: {
|
||||||
code: AstroErrorCodes.UnknownError,
|
code: AstroErrorData.UnknownError.code,
|
||||||
plugin: undefined,
|
plugin: undefined,
|
||||||
isFatal: false,
|
isFatal: false,
|
||||||
cliCommand: 'COMMAND_NAME',
|
cliCommand: 'COMMAND_NAME',
|
||||||
|
|
|
@ -492,7 +492,6 @@ importers:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
deepmerge-ts: 4.2.2
|
deepmerge-ts: 4.2.2
|
||||||
diff: 5.1.0
|
diff: 5.1.0
|
||||||
eol: 0.9.1
|
|
||||||
es-module-lexer: 0.10.5
|
es-module-lexer: 0.10.5
|
||||||
esbuild: 0.14.54
|
esbuild: 0.14.54
|
||||||
execa: 6.1.0
|
execa: 6.1.0
|
||||||
|
@ -557,6 +556,7 @@ importers:
|
||||||
astro-scripts: link:../../scripts
|
astro-scripts: link:../../scripts
|
||||||
chai: 4.3.7
|
chai: 4.3.7
|
||||||
cheerio: 1.0.0-rc.12
|
cheerio: 1.0.0-rc.12
|
||||||
|
eol: 0.9.1
|
||||||
memfs: 3.4.10
|
memfs: 3.4.10
|
||||||
mocha: 9.2.2
|
mocha: 9.2.2
|
||||||
node-fetch: 3.2.10
|
node-fetch: 3.2.10
|
||||||
|
@ -11635,7 +11635,7 @@ packages:
|
||||||
|
|
||||||
/eol/0.9.1:
|
/eol/0.9.1:
|
||||||
resolution: {integrity: sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==}
|
resolution: {integrity: sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==}
|
||||||
dev: false
|
dev: true
|
||||||
|
|
||||||
/error-ex/1.3.2:
|
/error-ex/1.3.2:
|
||||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||||
|
|
Loading…
Reference in a new issue