Improve error handling (#3859)
* feat: tag JSX exports with correct renderer * feat(error): enhance generic errors with frame * feat(error): surface errors from streaming response * feat(error): use vite overlay to display errors * chore: fix build issues * feat: use custom logger to hide vite errors for known packages * chore: move error-react-spectrum to e2e test * chore: add todo comment * test: fix error overlay handling * refactor: extract overlay message to util * test(e2e): update shared component tests * fix: give error overlay more time * refactor: move errors tests to e2e * fix: appease ts * test: move sass error to e2e tests * fix: scope optimizeDeps to `src/pages/**/*` * chore: update lockfile * chore: update test script * chore: log error overlay * chore: log error tests * chore: update playwright config * test(e2e): update errors tests * test(e2e): fix overlay util * test(e2e): fix test utils * test(e2e): try timeout * test(e2e): give up on overlay tests * fix: typo * fix: typo * refactor: collapse definition * fix: let errors throw * chore: revert scanner change * chore: refactor err.plugin handling * chore: add clarifying comments * fix: make astro:renderer non enumerable * chore: update comments * refactor: replace astro:renderer string with Symbol * chore: add comment about tagged components * feat: improve error overlay when hint exists Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
4ee997da43
commit
4412fe61f4
68 changed files with 453 additions and 375 deletions
24
packages/astro/e2e/error-react-spectrum.test.js
Normal file
24
packages/astro/e2e/error-react-spectrum.test.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { testFactory, getErrorOverlayMessage } from './test-utils.js';
|
||||
|
||||
const test = testFactory({ root: './fixtures/error-react-spectrum/' });
|
||||
|
||||
let devServer;
|
||||
|
||||
test.beforeEach(async ({ astro }) => {
|
||||
devServer = await astro.startDevServer();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ astro }) => {
|
||||
await devServer.stop();
|
||||
astro.resetAllFiles();
|
||||
});
|
||||
|
||||
test.describe('Error: React Spectrum', () => {
|
||||
test('overlay', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/'));
|
||||
|
||||
const message = await getErrorOverlayMessage(page);
|
||||
expect(message).toMatch('@adobe/react-spectrum is not compatible')
|
||||
});
|
||||
});
|
24
packages/astro/e2e/error-sass.test.js
Normal file
24
packages/astro/e2e/error-sass.test.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { testFactory, getErrorOverlayMessage } from './test-utils.js';
|
||||
|
||||
const test = testFactory({ root: './fixtures/error-sass/' });
|
||||
|
||||
let devServer;
|
||||
|
||||
test.beforeEach(async ({ astro }) => {
|
||||
devServer = await astro.startDevServer();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ astro }) => {
|
||||
await devServer.stop();
|
||||
astro.resetAllFiles();
|
||||
});
|
||||
|
||||
test.describe('Error: Sass', () => {
|
||||
test('overlay', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/'));
|
||||
|
||||
const message = await getErrorOverlayMessage(page);
|
||||
expect(message).toMatch('Undefined variable')
|
||||
});
|
||||
});
|
67
packages/astro/e2e/errors.test.js
Normal file
67
packages/astro/e2e/errors.test.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { testFactory, getErrorOverlayMessage } from './test-utils.js';
|
||||
|
||||
const test = testFactory({ root: './fixtures/errors/' });
|
||||
|
||||
let devServer;
|
||||
|
||||
test.beforeEach(async ({ astro }) => {
|
||||
devServer = await astro.startDevServer();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ astro }) => {
|
||||
await devServer.stop();
|
||||
astro.resetAllFiles();
|
||||
});
|
||||
|
||||
test.describe('Error display', () => {
|
||||
test('detect syntax errors in template', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/astro-syntax-error'));
|
||||
|
||||
const message = await getErrorOverlayMessage(page);
|
||||
expect(message).toMatch('Unexpected "}"')
|
||||
|
||||
await Promise.all([
|
||||
// Wait for page reload
|
||||
page.waitForNavigation(),
|
||||
// Edit the component file
|
||||
await astro.editFile('./src/pages/astro-syntax-error.astro', () => `<h1>No syntax error</h1>`)
|
||||
]);
|
||||
|
||||
expect(await page.locator('vite-error-overlay').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('shows useful error when frontmatter import is not found', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/import-not-found'));
|
||||
|
||||
const message = await getErrorOverlayMessage(page);
|
||||
expect(message).toMatch('failed to load module for ssr: ../abc.astro')
|
||||
|
||||
await Promise.all([
|
||||
// Wait for page reload
|
||||
page.waitForNavigation(),
|
||||
// Edit the component file
|
||||
astro.editFile('./src/pages/import-not-found.astro', () => `<h1>No import error</h1>`)
|
||||
]);
|
||||
|
||||
expect(await page.locator('vite-error-overlay').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('framework errors recover when fixed', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/svelte-syntax-error'));
|
||||
|
||||
const message = await getErrorOverlayMessage(page);
|
||||
expect(message).toMatch('</div> attempted to close an element that was not open')
|
||||
|
||||
await Promise.all([
|
||||
// Wait for page reload
|
||||
page.waitForNavigation(),
|
||||
// Edit the component file
|
||||
astro.editFile('./src/components/SvelteSyntaxError.svelte', () => `<h1>No mismatch</h1>`)
|
||||
]);
|
||||
|
||||
expect(await page.locator('vite-error-overlay').count()).toEqual(0);
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@test/error-react-spectrum",
|
||||
"name": "@e2e/error-react-spectrum",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@test/sass",
|
||||
"name": "@e2e/error-sass",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@test/errors",
|
||||
"name": "@e2e/errors",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
export { default } from '../components/Layout.astro';
|
||||
import Counter from '../components/Counter.jsx';
|
||||
import PreactComponent from '../components/JSXComponent.jsx';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default } from '../components/Layout.astro';
|
||||
import Counter from '../components/Counter.jsx';
|
||||
import ReactComponent from '../components/JSXComponent.jsx';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default } from '../components/Layout.astro';
|
||||
import Counter from '../components/Counter.jsx';
|
||||
import SolidComponent from '../components/SolidComponent.jsx';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default } from '../components/Layout.astro';
|
||||
import Counter from '../components/Counter.svelte';
|
||||
import SvelteComponent from '../components/SvelteComponent.svelte';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default } from '../components/Layout.astro';
|
||||
import Counter from '../components/Counter.vue';
|
||||
import VueComponent from '../components/VueComponent.vue';
|
||||
|
||||
|
|
|
@ -2,18 +2,23 @@ import { prepareTestFactory } from './shared-component-tests.js';
|
|||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/preact-compat-component/' });
|
||||
|
||||
const config = {
|
||||
counterComponentFilePath: './src/components/Counter.jsx',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
}
|
||||
|
||||
test.describe('preact/compat components in Astro files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/',
|
||||
pageSourceFilePath: './src/pages/index.astro',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('preact/compat components in Markdown files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/markdown/',
|
||||
pageSourceFilePath: './src/pages/markdown.md',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,26 +2,31 @@ import { prepareTestFactory } from './shared-component-tests.js';
|
|||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/preact-component/' });
|
||||
|
||||
const config = {
|
||||
counterComponentFilePath: './src/components/Counter.jsx',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
}
|
||||
|
||||
test.describe('Preact components in Astro files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/',
|
||||
pageSourceFilePath: './src/pages/index.astro',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Preact components in Markdown files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/markdown/',
|
||||
pageSourceFilePath: './src/pages/markdown.md',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Preact components in MDX files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/mdx/',
|
||||
pageSourceFilePath: './src/pages/mdx.mdx',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,26 +2,31 @@ import { prepareTestFactory } from './shared-component-tests.js';
|
|||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/react-component/' });
|
||||
|
||||
const config = {
|
||||
counterComponentFilePath: './src/components/Counter.jsx',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
}
|
||||
|
||||
test.describe('React components in Astro files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/',
|
||||
pageSourceFilePath: './src/pages/index.astro',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('React components in Markdown files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/markdown/',
|
||||
pageSourceFilePath: './src/pages/markdown.md',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('React components in MDX files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/mdx/',
|
||||
pageSourceFilePath: './src/pages/mdx.mdx',
|
||||
componentFilePath: './src/components/JSXComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { testFactory } from './test-utils.js';
|
||||
import { testFactory, getErrorOverlayMessage } from './test-utils.js';
|
||||
|
||||
export function prepareTestFactory(opts) {
|
||||
const test = testFactory(opts);
|
||||
|
|
|
@ -2,26 +2,31 @@ import { prepareTestFactory } from './shared-component-tests.js';
|
|||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/solid-component/' });
|
||||
|
||||
const config = {
|
||||
componentFilePath: './src/components/SolidComponent.jsx',
|
||||
counterComponentFilePath: './src/components/Counter.jsx',
|
||||
}
|
||||
|
||||
test.describe('Solid components in Astro files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/',
|
||||
pageSourceFilePath: './src/pages/index.astro',
|
||||
componentFilePath: './src/components/SolidComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Solid components in Markdown files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/markdown/',
|
||||
pageSourceFilePath: './src/pages/markdown.md',
|
||||
componentFilePath: './src/components/SolidComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Solid components in MDX files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/mdx/',
|
||||
pageSourceFilePath: './src/pages/mdx.mdx',
|
||||
componentFilePath: './src/components/SolidComponent.jsx',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,29 +2,32 @@ import { prepareTestFactory } from './shared-component-tests.js';
|
|||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/svelte-component/' });
|
||||
|
||||
const config = {
|
||||
componentFilePath: './src/components/SvelteComponent.svelte',
|
||||
counterComponentFilePath: './src/components/Counter.svelte',
|
||||
counterCssFilePath: './src/components/Counter.svelte',
|
||||
}
|
||||
|
||||
test.describe('Svelte components in Astro files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/',
|
||||
pageSourceFilePath: './src/pages/index.astro',
|
||||
componentFilePath: './src/components/SvelteComponent.svelte',
|
||||
counterCssFilePath: './src/components/Counter.svelte',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Svelte components in Markdown files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/markdown/',
|
||||
pageSourceFilePath: './src/pages/markdown.md',
|
||||
componentFilePath: './src/components/SvelteComponent.svelte',
|
||||
counterCssFilePath: './src/components/Counter.svelte',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Svelte components in MDX files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/mdx/',
|
||||
pageSourceFilePath: './src/pages/mdx.mdx',
|
||||
componentFilePath: './src/components/SvelteComponent.svelte',
|
||||
counterCssFilePath: './src/components/Counter.svelte',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { test as testBase } from '@playwright/test';
|
||||
import { test as testBase, expect } from '@playwright/test';
|
||||
import { loadFixture as baseLoadFixture } from '../test/test-utils.js';
|
||||
|
||||
export function loadFixture(inlineConfig) {
|
||||
|
@ -29,3 +29,11 @@ export function testFactory(inlineConfig) {
|
|||
|
||||
return test;
|
||||
}
|
||||
|
||||
export async function getErrorOverlayMessage(page) {
|
||||
const overlay = await page.waitForSelector('vite-error-overlay', { strict: true, timeout: 10 * 1000 })
|
||||
|
||||
expect(overlay).toBeTruthy()
|
||||
|
||||
return await overlay.$$eval('.message-body', (m) => m[0].textContent)
|
||||
}
|
||||
|
|
|
@ -2,29 +2,32 @@ import { prepareTestFactory } from './shared-component-tests.js';
|
|||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/vue-component/' });
|
||||
|
||||
const config = {
|
||||
componentFilePath: './src/components/VueComponent.vue',
|
||||
counterCssFilePath: './src/components/Counter.vue',
|
||||
counterComponentFilePath: './src/components/Counter.vue',
|
||||
}
|
||||
|
||||
test.describe('Vue components in Astro files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/',
|
||||
pageSourceFilePath: './src/pages/index.astro',
|
||||
componentFilePath: './src/components/VueComponent.vue',
|
||||
counterCssFilePath: './src/components/Counter.vue',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Vue components in Markdown files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/markdown/',
|
||||
pageSourceFilePath: './src/pages/markdown.md',
|
||||
componentFilePath: './src/components/VueComponent.vue',
|
||||
counterCssFilePath: './src/components/Counter.vue',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Vue components in MDX files', () => {
|
||||
createTests({
|
||||
...config,
|
||||
pageUrl: '/mdx/',
|
||||
pageSourceFilePath: './src/pages/mdx.mdx',
|
||||
componentFilePath: './src/components/VueComponent.vue',
|
||||
counterCssFilePath: './src/components/Counter.vue',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
|
||||
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
||||
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
||||
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js --ignore **/errors.test.js && mocha --timeout 20000 **/lit-element.test.js && mocha --timeout 20000 **/errors.test.js",
|
||||
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
|
||||
"test:match": "mocha --timeout 20000 -g",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:match": "playwright test -g"
|
||||
|
|
|
@ -9,12 +9,12 @@ const config = {
|
|||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
timeout: 3000,
|
||||
},
|
||||
/* Fail the build on CI if you accidentally left test in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 5 : 0,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1,
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
|
@ -33,6 +33,7 @@ const config = {
|
|||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome',
|
||||
args: ["--use-gl=egl"]
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { LogOptions } from './logger/core';
|
|||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as vite from 'vite';
|
||||
import { createCustomViteLogger } from './errors.js';
|
||||
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
|
||||
import astroViteServerPlugin from '../vite-plugin-astro-server/index.js';
|
||||
import astroVitePlugin from '../vite-plugin-astro/index.js';
|
||||
|
@ -60,7 +61,7 @@ export async function createVite(
|
|||
clearScreen: false, // we want to control the output, not Vite
|
||||
logLevel: 'warn', // log warnings and errors only
|
||||
optimizeDeps: {
|
||||
entries: ['src/**/*'], // Try and scan a user’s project (won’t catch everything),
|
||||
entries: ['src/**/*'],
|
||||
exclude: ['node-fetch'],
|
||||
},
|
||||
plugins: [
|
||||
|
@ -133,6 +134,8 @@ export async function createVite(
|
|||
sortPlugins(result.plugins);
|
||||
}
|
||||
|
||||
result.customLogger = createCustomViteLogger(result.logLevel ?? 'warn');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import eol from 'eol';
|
||||
import type { BuildResult } from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import type { ViteDevServer } from 'vite';
|
||||
import type { ViteDevServer, ErrorPayload, LogLevel, Logger } from 'vite';
|
||||
import type { SSRError } from '../@types/astro';
|
||||
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createLogger } from 'vite';
|
||||
import eol from 'eol';
|
||||
import fs from 'fs';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { codeFrame, createSafeError } from './util.js';
|
||||
|
||||
export enum AstroErrorCodes {
|
||||
|
@ -34,11 +38,12 @@ export function cleanErrorStack(stack: string) {
|
|||
return stack
|
||||
.split(/\n/g)
|
||||
.filter((l) => /^\s*at/.test(l))
|
||||
.map(l => l.replace(/\/@fs\//g, '/'))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/** Update the error message to correct any vite-isms that we don't want to expose to the user. */
|
||||
export function fixViteErrorMessage(_err: unknown, server: ViteDevServer) {
|
||||
export function fixViteErrorMessage(_err: unknown, server: ViteDevServer, filePath?: URL) {
|
||||
const err = createSafeError(_err);
|
||||
// Vite will give you better stacktraces, using sourcemaps.
|
||||
server.ssrFixStacktrace(err);
|
||||
|
@ -47,6 +52,18 @@ export function fixViteErrorMessage(_err: unknown, server: ViteDevServer) {
|
|||
if (err.message === 'import.meta.glob() can only accept string literals.') {
|
||||
err.message = 'Astro.glob() and import.meta.glob() can only accept string literals.';
|
||||
}
|
||||
if (filePath && /failed to load module for ssr:/.test(err.message)) {
|
||||
const importName = err.message.split('for ssr:').at(1)?.trim();
|
||||
if (importName) {
|
||||
const content = fs.readFileSync(fileURLToPath(filePath)).toString();
|
||||
const lns = content.split('\n')
|
||||
const line = lns.findIndex(ln => ln.includes(importName));
|
||||
const column = lns[line].indexOf(importName);
|
||||
if (!(err as any).id) {
|
||||
(err as any).id = `${fileURLToPath(filePath)}:${line + 1}:${column + 1}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
|
@ -55,7 +72,21 @@ const incompatiblePackages = {
|
|||
};
|
||||
const incompatPackageExp = new RegExp(`(${Object.keys(incompatiblePackages).join('|')})`);
|
||||
|
||||
function generateHint(err: ErrorWithMetadata): string | undefined {
|
||||
|
||||
export function createCustomViteLogger(logLevel: LogLevel): Logger {
|
||||
const viteLogger = createLogger(logLevel);
|
||||
const logger: Logger = {
|
||||
...viteLogger,
|
||||
error(msg, options?) {
|
||||
// Silence warnings from incompatible packages (we log better errors for these)
|
||||
if (incompatPackageExp.test(msg)) return;
|
||||
return viteLogger.error(msg, options);
|
||||
},
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
|
||||
function generateHint(err: ErrorWithMetadata, filePath?: URL): string | undefined {
|
||||
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.';
|
||||
} else {
|
||||
|
@ -72,29 +103,54 @@ function generateHint(err: ErrorWithMetadata): string | undefined {
|
|||
* Takes any error-like object and returns a standardized Error + metadata object.
|
||||
* Useful for consistent reporting regardless of where the error surfaced from.
|
||||
*/
|
||||
export function collectErrorMetadata(e: any): ErrorWithMetadata {
|
||||
// normalize error stack line-endings to \n
|
||||
export function collectErrorMetadata(e: any, filePath?: URL): ErrorWithMetadata {
|
||||
const err = e as SSRError;
|
||||
|
||||
if ((e as any).stack) {
|
||||
// normalize error stack line-endings to \n
|
||||
(e as any).stack = eol.lf((e as any).stack);
|
||||
// derive error location from stack (if possible)
|
||||
const stackText = stripAnsi(e.stack);
|
||||
// TODO: this could be better, `src` might be something else
|
||||
const possibleFilePath = err.pluginCode || err.id || stackText.split('\n').find(ln => ln.includes('src') || ln.includes('node_modules'));
|
||||
const source = possibleFilePath?.replace(/^[^(]+\(([^)]+).*$/, '$1');
|
||||
const [file, line, column] = source?.split(':') ?? [];
|
||||
if (!err.loc && line && column) {
|
||||
err.loc = {
|
||||
file,
|
||||
line: Number.parseInt(line),
|
||||
column: Number.parseInt(column)
|
||||
}
|
||||
}
|
||||
|
||||
// Derive plugin from stack (if possible)
|
||||
if (!err.plugin) {
|
||||
err.plugin =
|
||||
/withastro\/astro\/packages\/integrations\/([\w-]+)/gmi.exec(stackText)?.at(1) ||
|
||||
/(@astrojs\/[\w-]+)\/(server|client|index)/gmi.exec(stackText)?.at(1) ||
|
||||
undefined;
|
||||
}
|
||||
|
||||
// Normalize stack (remove `/@fs/` urls, etc)
|
||||
err.stack = cleanErrorStack(e.stack)
|
||||
}
|
||||
|
||||
if (e.name === 'YAMLException') {
|
||||
const err = e as SSRError;
|
||||
err.loc = { file: (e as any).id, line: (e as any).mark.line, column: (e as any).mark.column };
|
||||
err.message = (e as any).reason;
|
||||
}
|
||||
|
||||
if (!err.frame) {
|
||||
try {
|
||||
const fileContents = fs.readFileSync(err.loc.file!, 'utf8');
|
||||
err.frame = codeFrame(fileContents, err.loc);
|
||||
} catch {}
|
||||
}
|
||||
if (!err.frame && err.loc) {
|
||||
try {
|
||||
const fileContents = fs.readFileSync(err.loc.file!, 'utf8');
|
||||
const frame = codeFrame(fileContents, err.loc);
|
||||
err.frame = frame;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
|
||||
if (Array.isArray((e as any).errors)) {
|
||||
const { location, pluginName, text } = (e as BuildResult).errors[0];
|
||||
const err = e as SSRError;
|
||||
if (location) {
|
||||
err.loc = { file: location.file, line: location.line, column: location.column };
|
||||
err.id = err.id || location?.file;
|
||||
|
@ -111,11 +167,28 @@ export function collectErrorMetadata(e: any): ErrorWithMetadata {
|
|||
if (pluginName) {
|
||||
err.plugin = pluginName;
|
||||
}
|
||||
err.hint = generateHint(err);
|
||||
err.hint = generateHint(err, filePath);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Generic error (probably from Vite, and already formatted)
|
||||
e.hint = generateHint(e);
|
||||
return e;
|
||||
err.hint = generateHint(e, filePath);
|
||||
return err;
|
||||
}
|
||||
|
||||
export function getViteErrorPayload(err: ErrorWithMetadata): ErrorPayload {
|
||||
let plugin = err.plugin;
|
||||
if (!plugin && err.hint) {
|
||||
plugin = 'astro';
|
||||
}
|
||||
const message = `${err.message}\n\n${err.hint ?? ''}`;
|
||||
return {
|
||||
type: 'error',
|
||||
err: {
|
||||
...err,
|
||||
plugin,
|
||||
message: message.trim(),
|
||||
stack: err.stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ export function createSafeError(err: any): Error {
|
|||
/** generate code frame from esbuild error */
|
||||
export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string {
|
||||
if (!loc) return '';
|
||||
const lines = eol.lf(src).split('\n');
|
||||
const lines = eol.lf(src).split('\n').map(ln => ln.replace(/\t/g, ' '));
|
||||
// grab 2 lines before, and 3 lines after focused line
|
||||
const visibleLines = [];
|
||||
for (let n = -2; n <= 2; n++) {
|
||||
|
@ -98,9 +98,7 @@ export function codeFrame(src: string, loc: ErrorPayload['err']['loc']): string
|
|||
output += isFocusedLine ? '> ' : ' ';
|
||||
output += `${lineNo + 1} | ${lines[lineNo]}\n`;
|
||||
if (isFocusedLine)
|
||||
output += `${[...new Array(gutterWidth)].join(' ')} | ${[...new Array(loc.column)].join(
|
||||
' '
|
||||
)}^\n`;
|
||||
output += `${Array.from({ length: gutterWidth }).join(' ')} | ${Array.from({ length: loc.column }).join(' ')}^\n`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@ export function mergeSlots(...slotted: unknown[]) {
|
|||
}
|
||||
|
||||
export const Fragment = Symbol.for('astro:fragment');
|
||||
export const Renderer = Symbol.for('astro:renderer');
|
||||
export const ClientOnlyPlaceholder = 'astro-client-only';
|
||||
|
||||
function guessRenderers(componentUrl?: string): string[] {
|
||||
|
@ -182,6 +183,17 @@ function formatList(values: string[]): string {
|
|||
|
||||
const rendererAliases = new Map([['solid', 'solid-js']]);
|
||||
|
||||
/** @internal Assosciate JSX components with a specific renderer (see /src/vite-plugin-jsx/tag.ts) */
|
||||
export function __astro_tag_component__(Component: unknown, rendererName: string) {
|
||||
if (!Component) return;
|
||||
if (typeof Component !== 'function') return;
|
||||
Object.defineProperty(Component, Renderer, {
|
||||
value: rendererName,
|
||||
enumerable: false,
|
||||
writable: false
|
||||
})
|
||||
}
|
||||
|
||||
export async function renderComponent(
|
||||
result: SSRResult,
|
||||
displayName: string,
|
||||
|
@ -270,20 +282,30 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
|
|||
// Call the renderers `check` hook to see if any claim this component.
|
||||
let renderer: SSRLoadedRenderer | undefined;
|
||||
if (metadata.hydrate !== 'only') {
|
||||
let error;
|
||||
for (const r of renderers) {
|
||||
try {
|
||||
if (await r.ssr.check.call({ result }, Component, props, children)) {
|
||||
renderer = r;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
error ??= e;
|
||||
}
|
||||
// If this component ran through `__astro_tag_component__`, we already know
|
||||
// which renderer to match to and can skip the usual `check` calls.
|
||||
// This will help us throw most relevant error message for modules with runtime errors
|
||||
if (Component && (Component as any)[Renderer]) {
|
||||
const rendererName = (Component as any)[Renderer];
|
||||
renderer = renderers.find(({ name }) => name === rendererName);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
if (!renderer) {
|
||||
let error;
|
||||
for (const r of renderers) {
|
||||
try {
|
||||
if (await r.ssr.check.call({ result }, Component, props, children)) {
|
||||
renderer = r;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
error ??= e;
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
|
||||
|
@ -298,9 +320,9 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
|
|||
const rendererName = rendererAliases.has(passedName)
|
||||
? rendererAliases.get(passedName)
|
||||
: passedName;
|
||||
renderer = renderers.filter(
|
||||
renderer = renderers.find(
|
||||
({ name }) => name === `@astrojs/${rendererName}` || name === rendererName
|
||||
)[0];
|
||||
);
|
||||
}
|
||||
// Attempt: user only has a single renderer, default to that
|
||||
if (!renderer && renderers.length === 1) {
|
||||
|
@ -766,17 +788,21 @@ export async function renderPage(
|
|||
start(controller) {
|
||||
async function read() {
|
||||
let i = 0;
|
||||
for await (const chunk of iterable) {
|
||||
let html = chunk.toString();
|
||||
if (i === 0) {
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
controller.enqueue(encoder.encode('<!DOCTYPE html>\n'));
|
||||
try {
|
||||
for await (const chunk of iterable) {
|
||||
let html = chunk.toString();
|
||||
if (i === 0) {
|
||||
if (!/<!doctype html/i.test(html)) {
|
||||
controller.enqueue(encoder.encode('<!DOCTYPE html>\n'));
|
||||
}
|
||||
}
|
||||
controller.enqueue(encoder.encode(html));
|
||||
i++;
|
||||
}
|
||||
controller.enqueue(encoder.encode(html));
|
||||
i++;
|
||||
controller.close();
|
||||
} catch (e) {
|
||||
controller.error(e)
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
read();
|
||||
},
|
||||
|
@ -851,7 +877,7 @@ export async function* renderAstroComponent(
|
|||
if (value || value === 0) {
|
||||
for await (const chunk of _render(value)) {
|
||||
yield markHTMLString(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { encode } from 'html-entities';
|
||||
import { baseCSS } from './css.js';
|
||||
|
||||
interface ErrorTemplateOptions {
|
||||
/** a short description of the error */
|
||||
message: string;
|
||||
/** information about where the error occurred */
|
||||
stack?: string;
|
||||
/** HTTP error code */
|
||||
statusCode?: number;
|
||||
/** HTML <title> */
|
||||
tabTitle: string;
|
||||
/** page title */
|
||||
title: string;
|
||||
/** show user a URL for more info or action to take */
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/** Display all errors */
|
||||
export default function template({
|
||||
title,
|
||||
url,
|
||||
message,
|
||||
stack,
|
||||
statusCode,
|
||||
tabTitle,
|
||||
}: ErrorTemplateOptions): string {
|
||||
let error = url ? message.replace(url, '') : message;
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${tabTitle}</title>
|
||||
<style>
|
||||
${baseCSS}
|
||||
|
||||
.wrapper {
|
||||
max-width: 80rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.statusCode {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 3rem;
|
||||
margin-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.astro {
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrapper">
|
||||
<header>
|
||||
<svg class="astro" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z" fill="white"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z" fill="#ff5d01"></path></svg>
|
||||
<h1>${
|
||||
statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ''
|
||||
}<span class="statusMessage">${title}</span></h1>
|
||||
</header>
|
||||
<pre class="error-message">${encode(error)}</pre>
|
||||
${url ? `<a target="_blank" href="${url}">${url}</a>` : ''}
|
||||
<pre class="error-stack">${encode(stack)}</pre>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
|
@ -5,9 +5,8 @@ import type { AstroConfig, ManifestData } from '../@types/astro';
|
|||
import type { SSROptions } from '../core/render/dev/index';
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||
import { collectErrorMetadata, fixViteErrorMessage } from '../core/errors.js';
|
||||
import { collectErrorMetadata, ErrorWithMetadata, fixViteErrorMessage, getViteErrorPayload } from '../core/errors.js';
|
||||
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
||||
import * as msg from '../core/messages.js';
|
||||
import { appendForwardSlash } from '../core/path.js';
|
||||
|
@ -18,7 +17,6 @@ import { createRequest } from '../core/request.js';
|
|||
import { createRouteManifest, matchRoute } from '../core/routing/index.js';
|
||||
import { createSafeError, isBuildingToSSR, resolvePages } from '../core/util.js';
|
||||
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
||||
import serverErrorTemplate from '../template/5xx.js';
|
||||
|
||||
interface AstroPluginOptions {
|
||||
config: AstroConfig;
|
||||
|
@ -151,19 +149,14 @@ async function handle500Response(
|
|||
origin: string,
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
err: any
|
||||
err: ErrorWithMetadata
|
||||
) {
|
||||
const pathname = decodeURI(new URL('./index.html', origin + req.url).pathname);
|
||||
const html = serverErrorTemplate({
|
||||
statusCode: 500,
|
||||
title: 'Internal Error',
|
||||
tabTitle: '500: Error',
|
||||
message: stripAnsi(err.hint ?? err.message),
|
||||
url: err.url || undefined,
|
||||
stack: truncateString(stripAnsi(err.stack), 500),
|
||||
});
|
||||
const transformedHtml = await viteServer.transformIndexHtml(pathname, html);
|
||||
writeHtmlResponse(res, 500, transformedHtml);
|
||||
res.on('close', () => setTimeout(() => viteServer.ws.send(getViteErrorPayload(err)), 200))
|
||||
if (res.headersSent) {
|
||||
res.end()
|
||||
} else {
|
||||
writeHtmlResponse(res, 500, `<title>${err.name}</title><script type="module" src="/@vite/client"></script>`);
|
||||
}
|
||||
}
|
||||
|
||||
function getCustom404Route(config: AstroConfig, manifest: ManifestData) {
|
||||
|
@ -232,6 +225,7 @@ async function handleRequest(
|
|||
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
|
||||
});
|
||||
|
||||
let filePath: URL|undefined;
|
||||
try {
|
||||
if (!pathname.startsWith(devRoot)) {
|
||||
log404(logging, pathname);
|
||||
|
@ -252,7 +246,7 @@ async function handleRequest(
|
|||
}
|
||||
}
|
||||
|
||||
const filePath = new URL(`./${route.component}`, config.root);
|
||||
filePath = new URL(`./${route.component}`, config.root);
|
||||
const preloadedComponent = await preload({ astroConfig: config, filePath, viteServer });
|
||||
const [, mod] = preloadedComponent;
|
||||
// attempt to get static paths
|
||||
|
@ -330,10 +324,10 @@ async function handleRequest(
|
|||
return await writeSSRResult(result, res);
|
||||
}
|
||||
} catch (_err) {
|
||||
const err = fixViteErrorMessage(createSafeError(_err), viteServer);
|
||||
const errorWithMetadata = collectErrorMetadata(_err);
|
||||
const err = fixViteErrorMessage(createSafeError(_err), viteServer, filePath);
|
||||
const errorWithMetadata = collectErrorMetadata(err);
|
||||
error(logging, null, msg.formatErrorMessage(errorWithMetadata));
|
||||
handle500Response(viteServer, origin, req, res, err);
|
||||
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { LogOptions } from '../core/logger/core.js';
|
|||
import type { PluginMetadata } from '../vite-plugin-astro/types';
|
||||
|
||||
import babel from '@babel/core';
|
||||
import tagExportsPlugin from './tag.js';
|
||||
import * as eslexer from 'es-module-lexer';
|
||||
import esbuild from 'esbuild';
|
||||
import * as colors from 'kleur/colors';
|
||||
|
@ -55,7 +56,7 @@ async function transformJSX({
|
|||
}: TransformJSXOptions): Promise<TransformResult> {
|
||||
const { jsxTransformOptions } = renderer;
|
||||
const options = await jsxTransformOptions!({ mode, ssr });
|
||||
const plugins = [...(options.plugins || [])];
|
||||
const plugins = [...(options.plugins || []), tagExportsPlugin({ rendererName: renderer.name })];
|
||||
const result = await babel.transformAsync(code, {
|
||||
presets: options.presets,
|
||||
plugins,
|
||||
|
|
64
packages/astro/src/vite-plugin-jsx/tag.ts
Normal file
64
packages/astro/src/vite-plugin-jsx/tag.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import type { PluginObj } from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
/**
|
||||
* This plugin handles every file that runs through our JSX plugin.
|
||||
* Since we statically match every JSX file to an Astro renderer based on import scanning,
|
||||
* it would be helpful to embed some of that metadata at runtime.
|
||||
*
|
||||
* This plugin crawls each export in the file and "tags" each export with a given `rendererName`.
|
||||
* This allows us to automatically match a component to a renderer and skip the usual `check()` calls.
|
||||
*/
|
||||
export default function tagExportsWithRenderer({ rendererName }: { rendererName: string }): PluginObj {
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
// Inject `import { __astro_tag_component__ } from 'astro/server/index.js'`
|
||||
enter(path) {
|
||||
path.node.body.splice(
|
||||
0,
|
||||
0,
|
||||
t.importDeclaration(
|
||||
[t.importSpecifier(t.identifier('__astro_tag_component__'), t.identifier('__astro_tag_component__'))],
|
||||
t.stringLiteral('astro/server/index.js')
|
||||
)
|
||||
);
|
||||
},
|
||||
// For each export we found, inject `__astro_tag_component__(exportName, rendererName)`
|
||||
exit(path, state) {
|
||||
const exportedIds = state.get('astro:tags')
|
||||
if (exportedIds) {
|
||||
for (const id of exportedIds) {
|
||||
path.node.body.push(
|
||||
t.expressionStatement(t.callExpression(t.identifier('__astro_tag_component__'), [t.identifier(id), t.stringLiteral(rendererName)]))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ExportDeclaration(path, state) {
|
||||
const node = path.node;
|
||||
if (node.exportKind === 'type') return;
|
||||
if (node.type === 'ExportAllDeclaration') return;
|
||||
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
if (t.isFunctionDeclaration(node.declaration)) {
|
||||
if (node.declaration.id?.name) {
|
||||
const id = node.declaration.id.name;
|
||||
const tags = state.get('astro:tags') ?? []
|
||||
state.set('astro:tags', [...tags, id])
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'ExportDefaultDeclaration') {
|
||||
if (t.isFunctionDeclaration(node.declaration)) {
|
||||
if (node.declaration.id?.name) {
|
||||
const id = node.declaration.id.name;
|
||||
const tags = state.get('astro:tags') ?? []
|
||||
state.set('astro:tags', [...tags, id])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { isWindows, loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
describe('Error packages: react-spectrum', () => {
|
||||
if (isWindows) return;
|
||||
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/error-react-spectrum',
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
devServer && devServer.stop();
|
||||
});
|
||||
|
||||
it('properly detect syntax errors in template', async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
let html = await fixture.fetch('/').then((res) => res.text());
|
||||
let $ = cheerio.load(html);
|
||||
const msg = $('.error-message').text();
|
||||
expect(msg).to.match(/@adobe\/react-spectrum is not compatible/);
|
||||
});
|
||||
});
|
|
@ -1,100 +0,0 @@
|
|||
import { isWindows, isLinux, loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
describe('Error display', () => {
|
||||
if (isWindows) return;
|
||||
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/errors',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO: Track down reliability issue
|
||||
*
|
||||
* After fixing a syntax error on one page, the dev server hangs on the hmr.js request.
|
||||
* This is specific to a project that has other framework component errors,
|
||||
* in this case the fixture has multiple broken pages and components.
|
||||
*
|
||||
* The issue could be internal to vite, the hmr.js request triggers connect:dispatcher
|
||||
* events but vite:load is never actually called.
|
||||
*/
|
||||
describe.skip('Astro template syntax', async () => {
|
||||
let devServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('properly detect syntax errors in template', async () => {
|
||||
let html = await fixture.fetch('/astro-syntax-error').then((res) => res.text());
|
||||
|
||||
// 1. Verify an error message is being shown.
|
||||
let $ = cheerio.load(html);
|
||||
expect($('.statusMessage').text()).to.equal('Internal Error');
|
||||
expect($('.error-message').text()).to.contain('Unexpected "}"');
|
||||
|
||||
// 2. Edit the file, fixing the error
|
||||
await fixture.editFile('./src/pages/astro-syntax-error.astro', `<h1>No syntax error</h1>`);
|
||||
|
||||
// 3. Verify that the file is fixed.
|
||||
html = await fixture.fetch('/astro-syntax-error').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
expect($('h1').text()).to.equal('No syntax error');
|
||||
});
|
||||
|
||||
it('shows useful error when frontmatter import is not found', async () => {
|
||||
let html = await fixture.fetch('/import-not-found').then((res) => res.text());
|
||||
|
||||
// 1. Verify an error message is being shown.
|
||||
let $ = cheerio.load(html);
|
||||
expect($('.statusMessage').text()).to.equal('Internal Error');
|
||||
expect($('.error-message').text()).to.equal('failed to load module for ssr: ../abc.astro');
|
||||
|
||||
// 2. Edit the file, fixing the error
|
||||
await fixture.editFile('./src/pages/import-not-found.astro', '<h1>No import error</h1>');
|
||||
|
||||
// 3. Verify that the file is fixed.
|
||||
html = await fixture.fetch('/import-not-found').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
expect($('h1').text()).to.equal('No import error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Framework components', function () {
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('Errors recover when fixed', async () => {
|
||||
let html = await fixture.fetch('/svelte-syntax-error').then((res) => res.text());
|
||||
|
||||
// 1. Verify an error message is being shown.
|
||||
let $ = cheerio.load(html);
|
||||
expect($('.statusMessage').text()).to.equal('Internal Error');
|
||||
|
||||
// 2. Edit the file, fixing the error
|
||||
await fixture.editFile('./src/components/SvelteSyntaxError.svelte', `<h1>No mismatch</h1>`);
|
||||
|
||||
// 3. Verify that the file is fixed.
|
||||
html = await fixture.fetch('/svelte-syntax-error').then((res) => res.text());
|
||||
$ = cheerio.load(html);
|
||||
expect($('h1').text()).to.equal('No mismatch');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import { expect } from 'chai';
|
||||
import os from 'os';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
// note: many Sass tests live in 0-css.test.js to test within context of a framework.
|
||||
// these tests are independent of framework.
|
||||
describe('Sass', () => {
|
||||
let fixture;
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ root: './fixtures/sass/' });
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
// TODO: Sass cannot be found on macOS for some reason... Vite issue?
|
||||
const test = os.platform() === 'darwin' ? it.skip : it;
|
||||
test('shows helpful error on failure', async () => {
|
||||
const text = await fixture.fetch('/error').then((res) => res.text());
|
||||
expect(text).to.include('Undefined variable');
|
||||
});
|
||||
});
|
|
@ -700,6 +700,48 @@ importers:
|
|||
'@astrojs/vue': link:../../../../integrations/vue
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/e2e/fixtures/error-react-spectrum:
|
||||
specifiers:
|
||||
'@adobe/react-spectrum': ^3.18.0
|
||||
'@astrojs/react': workspace:*
|
||||
astro: workspace:*
|
||||
react: ^18.1.0
|
||||
react-dom: ^18.1.0
|
||||
dependencies:
|
||||
'@adobe/react-spectrum': 3.19.0_biqbaboplfbrettd7655fr4n2y
|
||||
'@astrojs/react': link:../../../../integrations/react
|
||||
astro: link:../../..
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
|
||||
packages/astro/e2e/fixtures/error-sass:
|
||||
specifiers:
|
||||
astro: workspace:*
|
||||
sass: ^1.52.2
|
||||
dependencies:
|
||||
astro: link:../../..
|
||||
sass: 1.53.0
|
||||
|
||||
packages/astro/e2e/fixtures/errors:
|
||||
specifiers:
|
||||
'@astrojs/preact': workspace:*
|
||||
'@astrojs/react': workspace:*
|
||||
'@astrojs/solid-js': workspace:*
|
||||
'@astrojs/svelte': workspace:*
|
||||
'@astrojs/vue': workspace:*
|
||||
astro: workspace:*
|
||||
react: ^18.1.0
|
||||
react-dom: ^18.1.0
|
||||
dependencies:
|
||||
'@astrojs/preact': link:../../../../integrations/preact
|
||||
'@astrojs/react': link:../../../../integrations/react
|
||||
'@astrojs/solid-js': link:../../../../integrations/solid
|
||||
'@astrojs/svelte': link:../../../../integrations/svelte
|
||||
'@astrojs/vue': link:../../../../integrations/vue
|
||||
astro: link:../../..
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
|
||||
packages/astro/e2e/fixtures/lit-component:
|
||||
specifiers:
|
||||
'@astrojs/lit': workspace:*
|
||||
|
@ -1467,40 +1509,6 @@ importers:
|
|||
'@astrojs/preact': link:../../../../integrations/preact
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/error-react-spectrum:
|
||||
specifiers:
|
||||
'@adobe/react-spectrum': ^3.18.0
|
||||
'@astrojs/react': workspace:*
|
||||
astro: workspace:*
|
||||
react: ^18.1.0
|
||||
react-dom: ^18.1.0
|
||||
dependencies:
|
||||
'@adobe/react-spectrum': 3.19.0_biqbaboplfbrettd7655fr4n2y
|
||||
'@astrojs/react': link:../../../../integrations/react
|
||||
astro: link:../../..
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
|
||||
packages/astro/test/fixtures/errors:
|
||||
specifiers:
|
||||
'@astrojs/preact': workspace:*
|
||||
'@astrojs/react': workspace:*
|
||||
'@astrojs/solid-js': workspace:*
|
||||
'@astrojs/svelte': workspace:*
|
||||
'@astrojs/vue': workspace:*
|
||||
astro: workspace:*
|
||||
react: ^18.1.0
|
||||
react-dom: ^18.1.0
|
||||
dependencies:
|
||||
'@astrojs/preact': link:../../../../integrations/preact
|
||||
'@astrojs/react': link:../../../../integrations/react
|
||||
'@astrojs/solid-js': link:../../../../integrations/solid
|
||||
'@astrojs/svelte': link:../../../../integrations/svelte
|
||||
'@astrojs/vue': link:../../../../integrations/vue
|
||||
astro: link:../../..
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
|
||||
packages/astro/test/fixtures/fetch:
|
||||
specifiers:
|
||||
'@astrojs/preact': workspace:*
|
||||
|
@ -1679,14 +1687,6 @@ importers:
|
|||
dependencies:
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/sass:
|
||||
specifiers:
|
||||
astro: workspace:*
|
||||
sass: ^1.52.2
|
||||
dependencies:
|
||||
astro: link:../../..
|
||||
sass: 1.53.0
|
||||
|
||||
packages/astro/test/fixtures/slots-preact:
|
||||
specifiers:
|
||||
'@astrojs/preact': workspace:*
|
||||
|
|
Loading…
Reference in a new issue