Graceful error recovery in the dev server (#5198)
* Graceful error recovery in the dev server Move dev-container to dev Update for the lockfile Invalidate modules in an error state Test invalidation of broken modules Remove unused error state Normalize for windows try a larger timeout Fixes build just for testing more testing Keep it posix fully posix * Fix up Windows path for testing * some debugging * use posix join * finally fixed * Remove leftover debugging * Reset the timeout * Adding a changeset
This commit is contained in:
parent
06c5d51b37
commit
c77a6cbe34
39 changed files with 1629 additions and 642 deletions
7
.changeset/thin-trains-run.md
Normal file
7
.changeset/thin-trains-run.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
HMR - Improved error recovery
|
||||||
|
|
||||||
|
This improves error recovery for HMR. Now when the dev server finds itself in an error state (because a route contained an error), it will recover from that state and refresh the page when the user has corrected the mistake.
|
|
@ -93,7 +93,7 @@
|
||||||
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
|
"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\"",
|
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
|
||||||
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
|
||||||
"test:unit": "mocha --exit --timeout 2000 ./test/units/**/*.test.js",
|
"test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js",
|
||||||
"test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
|
"test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
|
||||||
"test:match": "mocha --timeout 20000 -g",
|
"test:match": "mocha --timeout 20000 -g",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
|
@ -189,8 +189,10 @@
|
||||||
"astro-scripts": "workspace:*",
|
"astro-scripts": "workspace:*",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
"cheerio": "^1.0.0-rc.11",
|
"cheerio": "^1.0.0-rc.11",
|
||||||
|
"memfs": "^3.4.7",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"node-fetch": "^3.2.5",
|
"node-fetch": "^3.2.5",
|
||||||
|
"node-mocks-http": "^1.11.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
"rehype-slug": "^5.0.1",
|
"rehype-slug": "^5.0.1",
|
||||||
"rehype-toc": "^3.0.2",
|
"rehype-toc": "^3.0.2",
|
||||||
|
|
49
packages/astro/src/@types/typed-emitter.ts
Normal file
49
packages/astro/src/@types/typed-emitter.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* The MIT License (MIT)
|
||||||
|
* Copyright (c) 2018 Andy Wermke
|
||||||
|
* https://github.com/andywer/typed-emitter/blob/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type EventMap = {
|
||||||
|
[key: string]: (...args: any[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe event emitter.
|
||||||
|
*
|
||||||
|
* Use it like this:
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* type MyEvents = {
|
||||||
|
* error: (error: Error) => void;
|
||||||
|
* message: (from: string, content: string) => void;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const myEmitter = new EventEmitter() as TypedEmitter<MyEvents>;
|
||||||
|
*
|
||||||
|
* myEmitter.emit("error", "x") // <- Will catch this type error;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
interface TypedEventEmitter<Events extends EventMap> {
|
||||||
|
addListener<E extends keyof Events> (event: E, listener: Events[E]): this
|
||||||
|
on<E extends keyof Events> (event: E, listener: Events[E]): this
|
||||||
|
once<E extends keyof Events> (event: E, listener: Events[E]): this
|
||||||
|
prependListener<E extends keyof Events> (event: E, listener: Events[E]): this
|
||||||
|
prependOnceListener<E extends keyof Events> (event: E, listener: Events[E]): this
|
||||||
|
|
||||||
|
off<E extends keyof Events>(event: E, listener: Events[E]): this
|
||||||
|
removeAllListeners<E extends keyof Events> (event?: E): this
|
||||||
|
removeListener<E extends keyof Events> (event: E, listener: Events[E]): this
|
||||||
|
|
||||||
|
emit<E extends keyof Events> (event: E, ...args: Parameters<Events[E]>): boolean
|
||||||
|
// The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5
|
||||||
|
eventNames (): (keyof Events | string | symbol)[]
|
||||||
|
rawListeners<E extends keyof Events> (event: E): Events[E][]
|
||||||
|
listeners<E extends keyof Events> (event: E): Events[E][]
|
||||||
|
listenerCount<E extends keyof Events> (event: E): number
|
||||||
|
|
||||||
|
getMaxListeners (): number
|
||||||
|
setMaxListeners (maxListeners: number): this
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypedEventEmitter
|
|
@ -31,8 +31,7 @@ export const LEGACY_ASTRO_CONFIG_KEYS = new Set([
|
||||||
export async function validateConfig(
|
export async function validateConfig(
|
||||||
userConfig: any,
|
userConfig: any,
|
||||||
root: string,
|
root: string,
|
||||||
cmd: string,
|
cmd: string
|
||||||
logging: LogOptions
|
|
||||||
): Promise<AstroConfig> {
|
): Promise<AstroConfig> {
|
||||||
const fileProtocolRoot = pathToFileURL(root + path.sep);
|
const fileProtocolRoot = pathToFileURL(root + path.sep);
|
||||||
// Manual deprecation checks
|
// Manual deprecation checks
|
||||||
|
@ -195,8 +194,7 @@ export async function openConfig(configOptions: LoadConfigOptions): Promise<Open
|
||||||
userConfig,
|
userConfig,
|
||||||
root,
|
root,
|
||||||
flags,
|
flags,
|
||||||
configOptions.cmd,
|
configOptions.cmd
|
||||||
configOptions.logging
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -302,7 +300,7 @@ export async function loadConfig(configOptions: LoadConfigOptions): Promise<Astr
|
||||||
if (config) {
|
if (config) {
|
||||||
userConfig = config.value;
|
userConfig = config.value;
|
||||||
}
|
}
|
||||||
return resolveConfig(userConfig, root, flags, configOptions.cmd, configOptions.logging);
|
return resolveConfig(userConfig, root, flags, configOptions.cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
|
/** Attempt to resolve an Astro configuration object. Normalize, validate, and return. */
|
||||||
|
@ -310,15 +308,21 @@ export async function resolveConfig(
|
||||||
userConfig: AstroUserConfig,
|
userConfig: AstroUserConfig,
|
||||||
root: string,
|
root: string,
|
||||||
flags: CLIFlags = {},
|
flags: CLIFlags = {},
|
||||||
cmd: string,
|
cmd: string
|
||||||
logging: LogOptions
|
|
||||||
): Promise<AstroConfig> {
|
): Promise<AstroConfig> {
|
||||||
const mergedConfig = mergeCLIFlags(userConfig, flags, cmd);
|
const mergedConfig = mergeCLIFlags(userConfig, flags, cmd);
|
||||||
const validatedConfig = await validateConfig(mergedConfig, root, cmd, logging);
|
const validatedConfig = await validateConfig(mergedConfig, root, cmd);
|
||||||
|
|
||||||
return validatedConfig;
|
return validatedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDefaultDevConfig(
|
||||||
|
userConfig: AstroUserConfig = {},
|
||||||
|
root: string = process.cwd(),
|
||||||
|
) {
|
||||||
|
return resolveConfig(userConfig, root, undefined, 'dev');
|
||||||
|
}
|
||||||
|
|
||||||
function mergeConfigRecursively(
|
function mergeConfigRecursively(
|
||||||
defaults: Record<string, any>,
|
defaults: Record<string, any>,
|
||||||
overrides: Record<string, any>,
|
overrides: Record<string, any>,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export {
|
export {
|
||||||
|
createDefaultDevConfig,
|
||||||
openConfig,
|
openConfig,
|
||||||
resolveConfigPath,
|
resolveConfigPath,
|
||||||
resolveFlags,
|
resolveFlags,
|
||||||
|
@ -6,5 +7,5 @@ export {
|
||||||
validateConfig,
|
validateConfig,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
export type { AstroConfigSchema } from './schema';
|
export type { AstroConfigSchema } from './schema';
|
||||||
export { createSettings } from './settings.js';
|
export { createSettings, createDefaultDevSettings } from './settings.js';
|
||||||
export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js';
|
export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js';
|
||||||
|
|
|
@ -1,22 +1,43 @@
|
||||||
import type { AstroConfig, AstroSettings } from '../../@types/astro';
|
import type { AstroConfig, AstroSettings, AstroUserConfig } from '../../@types/astro';
|
||||||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js';
|
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../constants.js';
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { createDefaultDevConfig } from './config.js';
|
||||||
import jsxRenderer from '../../jsx/renderer.js';
|
import jsxRenderer from '../../jsx/renderer.js';
|
||||||
import { loadTSConfig } from './tsconfig.js';
|
import { loadTSConfig } from './tsconfig.js';
|
||||||
|
|
||||||
export function createSettings(config: AstroConfig, cwd?: string): AstroSettings {
|
export function createBaseSettings(config: AstroConfig): AstroSettings {
|
||||||
const tsconfig = loadTSConfig(cwd);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
tsConfig: tsconfig?.config,
|
tsConfig: undefined,
|
||||||
tsConfigPath: tsconfig?.path,
|
tsConfigPath: undefined,
|
||||||
|
|
||||||
adapter: undefined,
|
adapter: undefined,
|
||||||
injectedRoutes: [],
|
injectedRoutes: [],
|
||||||
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
pageExtensions: ['.astro', '.html', ...SUPPORTED_MARKDOWN_FILE_EXTENSIONS],
|
||||||
renderers: [jsxRenderer],
|
renderers: [jsxRenderer],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
watchFiles: tsconfig?.exists ? [tsconfig.path, ...tsconfig.extendedPaths] : [],
|
watchFiles: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createSettings(config: AstroConfig, cwd?: string): AstroSettings {
|
||||||
|
const tsconfig = loadTSConfig(cwd);
|
||||||
|
const settings = createBaseSettings(config);
|
||||||
|
settings.tsConfig = tsconfig?.config;
|
||||||
|
settings.tsConfigPath = tsconfig?.path;
|
||||||
|
settings.watchFiles = tsconfig?.exists ? [tsconfig.path, ...tsconfig.extendedPaths] : [];
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDefaultDevSettings(
|
||||||
|
userConfig: AstroUserConfig = {},
|
||||||
|
root?: string | URL
|
||||||
|
): Promise<AstroSettings> {
|
||||||
|
if(root && typeof root !== 'string') {
|
||||||
|
root = fileURLToPath(root);
|
||||||
|
}
|
||||||
|
const config = await createDefaultDevConfig(userConfig, root);
|
||||||
|
return createBaseSettings(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type { AstroSettings } from '../@types/astro';
|
import type { AstroSettings } from '../@types/astro';
|
||||||
import type { LogOptions } from './logger/core';
|
import type { LogOptions } from './logger/core';
|
||||||
|
|
||||||
|
import nodeFs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import * as vite from 'vite';
|
import * as vite from 'vite';
|
||||||
import { crawlFrameworkPkgs } from 'vitefu';
|
import { crawlFrameworkPkgs } from 'vitefu';
|
||||||
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
|
import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js';
|
||||||
import astroViteServerPlugin from '../vite-plugin-astro-server/index.js';
|
import { vitePluginAstroServer } from '../vite-plugin-astro-server/index.js';
|
||||||
import astroVitePlugin from '../vite-plugin-astro/index.js';
|
import astroVitePlugin from '../vite-plugin-astro/index.js';
|
||||||
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
|
import configAliasVitePlugin from '../vite-plugin-config-alias/index.js';
|
||||||
import envVitePlugin from '../vite-plugin-env/index.js';
|
import envVitePlugin from '../vite-plugin-env/index.js';
|
||||||
|
@ -17,12 +18,14 @@ import markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||||
import { createCustomViteLogger } from './errors/dev/index.js';
|
import { createCustomViteLogger } from './errors/dev/index.js';
|
||||||
|
import astroLoadFallbackPlugin from '../vite-plugin-load-fallback/index.js';
|
||||||
import { resolveDependency } from './util.js';
|
import { resolveDependency } from './util.js';
|
||||||
|
|
||||||
interface CreateViteOptions {
|
interface CreateViteOptions {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
mode: 'dev' | 'build' | string;
|
mode: 'dev' | 'build' | string;
|
||||||
|
fs?: typeof nodeFs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALWAYS_NOEXTERNAL = new Set([
|
const ALWAYS_NOEXTERNAL = new Set([
|
||||||
|
@ -54,7 +57,7 @@ function getSsrNoExternalDeps(projectRoot: URL): string[] {
|
||||||
/** Return a common starting point for all Vite actions */
|
/** Return a common starting point for all Vite actions */
|
||||||
export async function createVite(
|
export async function createVite(
|
||||||
commandConfig: vite.InlineConfig,
|
commandConfig: vite.InlineConfig,
|
||||||
{ settings, logging, mode }: CreateViteOptions
|
{ settings, logging, mode, fs = nodeFs }: CreateViteOptions
|
||||||
): Promise<vite.InlineConfig> {
|
): Promise<vite.InlineConfig> {
|
||||||
const astroPkgsConfig = await crawlFrameworkPkgs({
|
const astroPkgsConfig = await crawlFrameworkPkgs({
|
||||||
root: fileURLToPath(settings.config.root),
|
root: fileURLToPath(settings.config.root),
|
||||||
|
@ -97,7 +100,7 @@ export async function createVite(
|
||||||
astroScriptsPlugin({ settings }),
|
astroScriptsPlugin({ settings }),
|
||||||
// The server plugin is for dev only and having it run during the build causes
|
// The server plugin is for dev only and having it run during the build causes
|
||||||
// the build to run very slow as the filewatcher is triggered often.
|
// the build to run very slow as the filewatcher is triggered often.
|
||||||
mode !== 'build' && astroViteServerPlugin({ settings, logging }),
|
mode !== 'build' && vitePluginAstroServer({ settings, logging, fs }),
|
||||||
envVitePlugin({ settings }),
|
envVitePlugin({ settings }),
|
||||||
settings.config.legacy.astroFlavoredMarkdown
|
settings.config.legacy.astroFlavoredMarkdown
|
||||||
? legacyMarkdownVitePlugin({ settings, logging })
|
? legacyMarkdownVitePlugin({ settings, logging })
|
||||||
|
@ -107,6 +110,7 @@ export async function createVite(
|
||||||
astroPostprocessVitePlugin({ settings }),
|
astroPostprocessVitePlugin({ settings }),
|
||||||
astroIntegrationsContainerPlugin({ settings, logging }),
|
astroIntegrationsContainerPlugin({ settings, logging }),
|
||||||
astroScriptsPageSSRPlugin({ settings }),
|
astroScriptsPageSSRPlugin({ settings }),
|
||||||
|
astroLoadFallbackPlugin({ fs })
|
||||||
],
|
],
|
||||||
publicDir: fileURLToPath(settings.config.publicDir),
|
publicDir: fileURLToPath(settings.config.publicDir),
|
||||||
root: fileURLToPath(settings.config.root),
|
root: fileURLToPath(settings.config.root),
|
||||||
|
|
124
packages/astro/src/core/dev/container.ts
Normal file
124
packages/astro/src/core/dev/container.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
|
||||||
|
import type { AddressInfo } from 'net';
|
||||||
|
import type { AstroSettings, AstroUserConfig } from '../../@types/astro';
|
||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
|
import {
|
||||||
|
runHookConfigDone,
|
||||||
|
runHookConfigSetup,
|
||||||
|
runHookServerSetup,
|
||||||
|
runHookServerStart,
|
||||||
|
} from '../../integrations/index.js';
|
||||||
|
import { createVite } from '../create-vite.js';
|
||||||
|
import { LogOptions } from '../logger/core.js';
|
||||||
|
import { nodeLogDestination } from '../logger/node.js';
|
||||||
|
import nodeFs from 'fs';
|
||||||
|
import * as vite from 'vite';
|
||||||
|
import { createDefaultDevSettings } from '../config/index.js';
|
||||||
|
import { apply as applyPolyfill } from '../polyfill.js';
|
||||||
|
|
||||||
|
|
||||||
|
const defaultLogging: LogOptions = {
|
||||||
|
dest: nodeLogDestination,
|
||||||
|
level: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Container {
|
||||||
|
fs: typeof nodeFs;
|
||||||
|
logging: LogOptions;
|
||||||
|
settings: AstroSettings;
|
||||||
|
viteConfig: vite.InlineConfig;
|
||||||
|
viteServer: vite.ViteDevServer;
|
||||||
|
handle: (req: http.IncomingMessage, res: http.ServerResponse) => void;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateContainerParams {
|
||||||
|
isRestart?: boolean;
|
||||||
|
logging?: LogOptions;
|
||||||
|
userConfig?: AstroUserConfig;
|
||||||
|
settings?: AstroSettings;
|
||||||
|
fs?: typeof nodeFs;
|
||||||
|
root?: string | URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createContainer(params: CreateContainerParams = {}): Promise<Container> {
|
||||||
|
let {
|
||||||
|
isRestart = false,
|
||||||
|
logging = defaultLogging,
|
||||||
|
settings = await createDefaultDevSettings(params.userConfig, params.root),
|
||||||
|
fs = nodeFs
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
applyPolyfill();
|
||||||
|
settings = await runHookConfigSetup({
|
||||||
|
settings,
|
||||||
|
command: 'dev',
|
||||||
|
logging,
|
||||||
|
isRestart,
|
||||||
|
});
|
||||||
|
const { host } = settings.config.server;
|
||||||
|
|
||||||
|
// The client entrypoint for renderers. Since these are imported dynamically
|
||||||
|
// we need to tell Vite to preoptimize them.
|
||||||
|
const rendererClientEntries = settings.renderers
|
||||||
|
.map((r) => r.clientEntrypoint)
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
const viteConfig = await createVite(
|
||||||
|
{
|
||||||
|
mode: 'development',
|
||||||
|
server: { host },
|
||||||
|
optimizeDeps: {
|
||||||
|
include: rendererClientEntries,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'import.meta.env.BASE_URL': settings.config.base
|
||||||
|
? `'${settings.config.base}'`
|
||||||
|
: 'undefined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ settings, logging, mode: 'dev', fs }
|
||||||
|
);
|
||||||
|
await runHookConfigDone({ settings, logging });
|
||||||
|
const viteServer = await vite.createServer(viteConfig);
|
||||||
|
runHookServerSetup({ config: settings.config, server: viteServer, logging });
|
||||||
|
|
||||||
|
return {
|
||||||
|
fs,
|
||||||
|
logging,
|
||||||
|
settings,
|
||||||
|
viteConfig,
|
||||||
|
viteServer,
|
||||||
|
|
||||||
|
handle(req, res) {
|
||||||
|
viteServer.middlewares.handle(req, res, Function.prototype);
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
return viteServer.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startContainer({ settings, viteServer, logging }: Container): Promise<AddressInfo> {
|
||||||
|
const { port } = settings.config.server;
|
||||||
|
await viteServer.listen(port);
|
||||||
|
const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo;
|
||||||
|
await runHookServerStart({
|
||||||
|
config: settings.config,
|
||||||
|
address: devServerAddressInfo,
|
||||||
|
logging,
|
||||||
|
});
|
||||||
|
|
||||||
|
return devServerAddressInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runInContainer(params: CreateContainerParams, callback: (container: Container) => Promise<void> | void) {
|
||||||
|
const container = await createContainer(params);
|
||||||
|
try {
|
||||||
|
await callback(container);
|
||||||
|
} finally {
|
||||||
|
await container.close();
|
||||||
|
}
|
||||||
|
}
|
74
packages/astro/src/core/dev/dev.ts
Normal file
74
packages/astro/src/core/dev/dev.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import type { AstroTelemetry } from '@astrojs/telemetry';
|
||||||
|
import type { AddressInfo } from 'net';
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
|
import * as vite from 'vite';
|
||||||
|
import type { AstroSettings } from '../../@types/astro';
|
||||||
|
import { runHookServerDone } from '../../integrations/index.js';
|
||||||
|
import { info, LogOptions, warn } from '../logger/core.js';
|
||||||
|
import * as msg from '../messages.js';
|
||||||
|
import { createContainer, startContainer } from './container.js';
|
||||||
|
|
||||||
|
export interface DevOptions {
|
||||||
|
logging: LogOptions;
|
||||||
|
telemetry: AstroTelemetry;
|
||||||
|
isRestart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DevServer {
|
||||||
|
address: AddressInfo;
|
||||||
|
watcher: vite.FSWatcher;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `astro dev` */
|
||||||
|
export default async function dev(
|
||||||
|
settings: AstroSettings,
|
||||||
|
options: DevOptions
|
||||||
|
): Promise<DevServer> {
|
||||||
|
const devStart = performance.now();
|
||||||
|
await options.telemetry.record([]);
|
||||||
|
|
||||||
|
// Create a container which sets up the Vite server.
|
||||||
|
const container = await createContainer({
|
||||||
|
settings,
|
||||||
|
logging: options.logging,
|
||||||
|
isRestart: options.isRestart,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start listening to the port
|
||||||
|
const devServerAddressInfo = await startContainer(container);
|
||||||
|
|
||||||
|
const site = settings.config.site
|
||||||
|
? new URL(settings.config.base, settings.config.site)
|
||||||
|
: undefined;
|
||||||
|
info(
|
||||||
|
options.logging,
|
||||||
|
null,
|
||||||
|
msg.serverStart({
|
||||||
|
startupTime: performance.now() - devStart,
|
||||||
|
resolvedUrls: container.viteServer.resolvedUrls || { local: [], network: [] },
|
||||||
|
host: settings.config.server.host,
|
||||||
|
site,
|
||||||
|
isRestart: options.isRestart,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
|
||||||
|
if (currentVersion.includes('-')) {
|
||||||
|
warn(options.logging, null, msg.prerelease({ currentVersion }));
|
||||||
|
}
|
||||||
|
if (container.viteConfig.server?.fs?.strict === false) {
|
||||||
|
warn(options.logging, null, msg.fsStrictWarning());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: devServerAddressInfo,
|
||||||
|
get watcher() {
|
||||||
|
return container.viteServer.watcher;
|
||||||
|
},
|
||||||
|
stop: async () => {
|
||||||
|
await container.close();
|
||||||
|
await runHookServerDone({ config: settings.config, logging: options.logging });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,113 +1,9 @@
|
||||||
import type { AstroTelemetry } from '@astrojs/telemetry';
|
export {
|
||||||
import type { AddressInfo } from 'net';
|
createContainer,
|
||||||
import { performance } from 'perf_hooks';
|
startContainer,
|
||||||
import * as vite from 'vite';
|
runInContainer
|
||||||
import type { AstroSettings } from '../../@types/astro';
|
} from './container.js';
|
||||||
import {
|
|
||||||
runHookConfigDone,
|
|
||||||
runHookConfigSetup,
|
|
||||||
runHookServerDone,
|
|
||||||
runHookServerSetup,
|
|
||||||
runHookServerStart,
|
|
||||||
} from '../../integrations/index.js';
|
|
||||||
import { createVite } from '../create-vite.js';
|
|
||||||
import { info, LogOptions, warn } from '../logger/core.js';
|
|
||||||
import * as msg from '../messages.js';
|
|
||||||
import { apply as applyPolyfill } from '../polyfill.js';
|
|
||||||
|
|
||||||
export interface DevOptions {
|
export {
|
||||||
logging: LogOptions;
|
default
|
||||||
telemetry: AstroTelemetry;
|
} from './dev.js';
|
||||||
isRestart?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DevServer {
|
|
||||||
address: AddressInfo;
|
|
||||||
watcher: vite.FSWatcher;
|
|
||||||
stop(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** `astro dev` */
|
|
||||||
export default async function dev(
|
|
||||||
settings: AstroSettings,
|
|
||||||
options: DevOptions
|
|
||||||
): Promise<DevServer> {
|
|
||||||
const devStart = performance.now();
|
|
||||||
applyPolyfill();
|
|
||||||
await options.telemetry.record([]);
|
|
||||||
settings = await runHookConfigSetup({
|
|
||||||
settings,
|
|
||||||
command: 'dev',
|
|
||||||
logging: options.logging,
|
|
||||||
isRestart: options.isRestart,
|
|
||||||
});
|
|
||||||
const { host, port } = settings.config.server;
|
|
||||||
const { isRestart = false } = options;
|
|
||||||
|
|
||||||
// The client entrypoint for renderers. Since these are imported dynamically
|
|
||||||
// we need to tell Vite to preoptimize them.
|
|
||||||
const rendererClientEntries = settings.renderers
|
|
||||||
.map((r) => r.clientEntrypoint)
|
|
||||||
.filter(Boolean) as string[];
|
|
||||||
|
|
||||||
const viteConfig = await createVite(
|
|
||||||
{
|
|
||||||
mode: 'development',
|
|
||||||
server: { host },
|
|
||||||
optimizeDeps: {
|
|
||||||
include: rendererClientEntries,
|
|
||||||
},
|
|
||||||
define: {
|
|
||||||
'import.meta.env.BASE_URL': settings.config.base
|
|
||||||
? `'${settings.config.base}'`
|
|
||||||
: 'undefined',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ settings, logging: options.logging, mode: 'dev' }
|
|
||||||
);
|
|
||||||
await runHookConfigDone({ settings, logging: options.logging });
|
|
||||||
const viteServer = await vite.createServer(viteConfig);
|
|
||||||
runHookServerSetup({ config: settings.config, server: viteServer, logging: options.logging });
|
|
||||||
await viteServer.listen(port);
|
|
||||||
|
|
||||||
const site = settings.config.site
|
|
||||||
? new URL(settings.config.base, settings.config.site)
|
|
||||||
: undefined;
|
|
||||||
info(
|
|
||||||
options.logging,
|
|
||||||
null,
|
|
||||||
msg.serverStart({
|
|
||||||
startupTime: performance.now() - devStart,
|
|
||||||
resolvedUrls: viteServer.resolvedUrls || { local: [], network: [] },
|
|
||||||
host: settings.config.server.host,
|
|
||||||
site,
|
|
||||||
isRestart,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentVersion = process.env.PACKAGE_VERSION ?? '0.0.0';
|
|
||||||
if (currentVersion.includes('-')) {
|
|
||||||
warn(options.logging, null, msg.prerelease({ currentVersion }));
|
|
||||||
}
|
|
||||||
if (viteConfig.server?.fs?.strict === false) {
|
|
||||||
warn(options.logging, null, msg.fsStrictWarning());
|
|
||||||
}
|
|
||||||
|
|
||||||
const devServerAddressInfo = viteServer.httpServer!.address() as AddressInfo;
|
|
||||||
await runHookServerStart({
|
|
||||||
config: settings.config,
|
|
||||||
address: devServerAddressInfo,
|
|
||||||
logging: options.logging,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
address: devServerAddressInfo,
|
|
||||||
get watcher() {
|
|
||||||
return viteServer.watcher;
|
|
||||||
},
|
|
||||||
stop: async () => {
|
|
||||||
await viteServer.close();
|
|
||||||
await runHookServerDone({ config: settings.config, logging: options.logging });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ModuleLoader } from '../../module-loader/index.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import {
|
import {
|
||||||
|
@ -5,7 +6,6 @@ import {
|
||||||
type ErrorPayload,
|
type ErrorPayload,
|
||||||
type Logger,
|
type Logger,
|
||||||
type LogLevel,
|
type LogLevel,
|
||||||
type ViteDevServer,
|
|
||||||
} from 'vite';
|
} from 'vite';
|
||||||
import { AstroErrorCodes } from '../codes.js';
|
import { AstroErrorCodes } from '../codes.js';
|
||||||
import { AstroError, type ErrorWithMetadata } from '../errors.js';
|
import { AstroError, type ErrorWithMetadata } from '../errors.js';
|
||||||
|
@ -30,12 +30,12 @@ export function createCustomViteLogger(logLevel: LogLevel): Logger {
|
||||||
export function enhanceViteSSRError(
|
export function enhanceViteSSRError(
|
||||||
error: Error,
|
error: Error,
|
||||||
filePath?: URL,
|
filePath?: URL,
|
||||||
viteServer?: ViteDevServer
|
loader?: ModuleLoader,
|
||||||
): AstroError {
|
): AstroError {
|
||||||
// Vite will give you better stacktraces, using sourcemaps.
|
// Vite will give you better stacktraces, using sourcemaps.
|
||||||
if (viteServer) {
|
if (loader) {
|
||||||
try {
|
try {
|
||||||
viteServer.ssrFixStacktrace(error);
|
loader.fixStacktrace(error);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
packages/astro/src/core/module-loader/index.ts
Normal file
14
packages/astro/src/core/module-loader/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export type {
|
||||||
|
ModuleInfo,
|
||||||
|
ModuleLoader,
|
||||||
|
ModuleNode,
|
||||||
|
LoaderEvents
|
||||||
|
} from './loader.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
createLoader
|
||||||
|
} from './loader.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
createViteLoader
|
||||||
|
} from './vite.js';
|
71
packages/astro/src/core/module-loader/loader.ts
Normal file
71
packages/astro/src/core/module-loader/loader.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import type TypedEmitter from '../../@types/typed-emitter';
|
||||||
|
import type * as fs from 'fs';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
// This is a generic interface for a module loader. In the astro cli this is
|
||||||
|
// fulfilled by Vite, see vite.ts
|
||||||
|
|
||||||
|
export type LoaderEvents = {
|
||||||
|
'file-add': (msg: [path: string, stats?: fs.Stats | undefined]) => void;
|
||||||
|
'file-change': (msg: [path: string, stats?: fs.Stats | undefined]) => void;
|
||||||
|
'file-unlink': (msg: [path: string, stats?: fs.Stats | undefined]) => void;
|
||||||
|
'hmr-error': (msg: {
|
||||||
|
type: 'error',
|
||||||
|
err: {
|
||||||
|
message: string;
|
||||||
|
stack: string
|
||||||
|
};
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModuleLoaderEventEmitter = TypedEmitter<LoaderEvents>;
|
||||||
|
|
||||||
|
export interface ModuleLoader {
|
||||||
|
import: (src: string) => Promise<Record<string, any>>;
|
||||||
|
resolveId: (specifier: string, parentId: string | undefined) => Promise<string | undefined>;
|
||||||
|
getModuleById: (id: string) => ModuleNode | undefined;
|
||||||
|
getModulesByFile: (file: string) => Set<ModuleNode> | undefined;
|
||||||
|
getModuleInfo: (id: string) => ModuleInfo | null;
|
||||||
|
|
||||||
|
eachModule(callbackfn: (value: ModuleNode, key: string) => void): void;
|
||||||
|
invalidateModule(mod: ModuleNode): void;
|
||||||
|
|
||||||
|
fixStacktrace: (error: Error) => void;
|
||||||
|
|
||||||
|
clientReload: () => void;
|
||||||
|
webSocketSend: (msg: any) => void;
|
||||||
|
isHttps: () => boolean;
|
||||||
|
events: TypedEmitter<LoaderEvents>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModuleNode {
|
||||||
|
id: string | null;
|
||||||
|
url: string;
|
||||||
|
ssrModule: Record<string, any> | null;
|
||||||
|
ssrError: Error | null;
|
||||||
|
importedModules: Set<ModuleNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModuleInfo {
|
||||||
|
id: string;
|
||||||
|
meta?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLoader(overrides: Partial<ModuleLoader>): ModuleLoader {
|
||||||
|
return {
|
||||||
|
import() { throw new Error(`Not implemented`); },
|
||||||
|
resolveId(id) { return Promise.resolve(id); },
|
||||||
|
getModuleById() {return undefined },
|
||||||
|
getModulesByFile() { return undefined },
|
||||||
|
getModuleInfo() { return null; },
|
||||||
|
eachModule() { throw new Error(`Not implemented`); },
|
||||||
|
invalidateModule() {},
|
||||||
|
fixStacktrace() {},
|
||||||
|
clientReload() {},
|
||||||
|
webSocketSend() {},
|
||||||
|
isHttps() { return true; },
|
||||||
|
events: new EventEmitter() as ModuleLoaderEventEmitter,
|
||||||
|
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
67
packages/astro/src/core/module-loader/vite.ts
Normal file
67
packages/astro/src/core/module-loader/vite.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import type * as vite from 'vite';
|
||||||
|
import type { ModuleLoader, ModuleLoaderEventEmitter } from './loader';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export function createViteLoader(viteServer: vite.ViteDevServer): ModuleLoader {
|
||||||
|
const events = new EventEmitter() as ModuleLoaderEventEmitter;
|
||||||
|
|
||||||
|
viteServer.watcher.on('add', (...args) => events.emit('file-add', args));
|
||||||
|
viteServer.watcher.on('unlink', (...args) => events.emit('file-unlink', args));
|
||||||
|
viteServer.watcher.on('change', (...args) => events.emit('file-change', args));
|
||||||
|
|
||||||
|
wrapMethod(viteServer.ws, 'send', msg => {
|
||||||
|
if(msg?.type === 'error') {
|
||||||
|
events.emit('hmr-error', msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
import(src) {
|
||||||
|
return viteServer.ssrLoadModule(src);
|
||||||
|
},
|
||||||
|
async resolveId(spec, parent) {
|
||||||
|
const ret = await viteServer.pluginContainer.resolveId(spec, parent);
|
||||||
|
return ret?.id;
|
||||||
|
},
|
||||||
|
getModuleById(id) {
|
||||||
|
return viteServer.moduleGraph.getModuleById(id);
|
||||||
|
},
|
||||||
|
getModulesByFile(file) {
|
||||||
|
return viteServer.moduleGraph.getModulesByFile(file);
|
||||||
|
},
|
||||||
|
getModuleInfo(id) {
|
||||||
|
return viteServer.pluginContainer.getModuleInfo(id);
|
||||||
|
},
|
||||||
|
eachModule(cb) {
|
||||||
|
return viteServer.moduleGraph.idToModuleMap.forEach(cb);
|
||||||
|
},
|
||||||
|
invalidateModule(mod) {
|
||||||
|
viteServer.moduleGraph.invalidateModule(mod as vite.ModuleNode);
|
||||||
|
},
|
||||||
|
fixStacktrace(err) {
|
||||||
|
return viteServer.ssrFixStacktrace(err);
|
||||||
|
},
|
||||||
|
clientReload() {
|
||||||
|
viteServer.ws.send({
|
||||||
|
type: 'full-reload',
|
||||||
|
path: '*'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
webSocketSend(msg) {
|
||||||
|
return viteServer.ws.send(msg);
|
||||||
|
},
|
||||||
|
isHttps() {
|
||||||
|
return !!viteServer.config.server.https;
|
||||||
|
},
|
||||||
|
events
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function wrapMethod(object: any, method: string, newFn: (...args: any[]) => void) {
|
||||||
|
const orig = object[method];
|
||||||
|
object[method] = function(...args: any[]) {
|
||||||
|
newFn.apply(this, args);
|
||||||
|
return orig.apply(this, args);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import type * as vite from 'vite';
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { RuntimeMode } from '../../../@types/astro.js';
|
import { RuntimeMode } from '../../../@types/astro.js';
|
||||||
|
@ -9,18 +9,18 @@ import { crawlGraph } from './vite.js';
|
||||||
/** Given a filePath URL, crawl Vite’s module graph to find all style imports. */
|
/** Given a filePath URL, crawl Vite’s module graph to find all style imports. */
|
||||||
export async function getStylesForURL(
|
export async function getStylesForURL(
|
||||||
filePath: URL,
|
filePath: URL,
|
||||||
viteServer: vite.ViteDevServer,
|
loader: ModuleLoader,
|
||||||
mode: RuntimeMode
|
mode: RuntimeMode
|
||||||
): Promise<{ urls: Set<string>; stylesMap: Map<string, string> }> {
|
): Promise<{ urls: Set<string>; stylesMap: Map<string, string> }> {
|
||||||
const importedCssUrls = new Set<string>();
|
const importedCssUrls = new Set<string>();
|
||||||
const importedStylesMap = new Map<string, string>();
|
const importedStylesMap = new Map<string, string>();
|
||||||
|
|
||||||
for await (const importedModule of crawlGraph(viteServer, viteID(filePath), true)) {
|
for await (const importedModule of crawlGraph(loader, viteID(filePath), true)) {
|
||||||
const ext = path.extname(importedModule.url).toLowerCase();
|
const ext = path.extname(importedModule.url).toLowerCase();
|
||||||
if (STYLE_EXTENSIONS.has(ext)) {
|
if (STYLE_EXTENSIONS.has(ext)) {
|
||||||
// The SSR module is possibly not loaded. Load it if it's null.
|
// The SSR module is possibly not loaded. Load it if it's null.
|
||||||
const ssrModule =
|
const ssrModule =
|
||||||
importedModule.ssrModule ?? (await viteServer.ssrLoadModule(importedModule.url));
|
importedModule.ssrModule ?? (await loader.import(importedModule.url));
|
||||||
if (
|
if (
|
||||||
mode === 'development' && // only inline in development
|
mode === 'development' && // only inline in development
|
||||||
typeof ssrModule?.default === 'string' // ignore JS module styles
|
typeof ssrModule?.default === 'string' // ignore JS module styles
|
||||||
|
|
|
@ -2,19 +2,21 @@ import type { ViteDevServer } from 'vite';
|
||||||
import type { AstroSettings, RuntimeMode } from '../../../@types/astro';
|
import type { AstroSettings, RuntimeMode } from '../../../@types/astro';
|
||||||
import type { LogOptions } from '../../logger/core.js';
|
import type { LogOptions } from '../../logger/core.js';
|
||||||
import type { Environment } from '../index';
|
import type { Environment } from '../index';
|
||||||
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
|
|
||||||
import { createEnvironment } from '../index.js';
|
import { createEnvironment } from '../index.js';
|
||||||
import { RouteCache } from '../route-cache.js';
|
import { RouteCache } from '../route-cache.js';
|
||||||
import { createResolve } from './resolve.js';
|
import { createResolve } from './resolve.js';
|
||||||
|
|
||||||
export type DevelopmentEnvironment = Environment & {
|
export type DevelopmentEnvironment = Environment & {
|
||||||
|
loader: ModuleLoader;
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
viteServer: ViteDevServer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createDevelopmentEnvironment(
|
export function createDevelopmentEnvironment(
|
||||||
settings: AstroSettings,
|
settings: AstroSettings,
|
||||||
logging: LogOptions,
|
logging: LogOptions,
|
||||||
viteServer: ViteDevServer
|
loader: ModuleLoader
|
||||||
): DevelopmentEnvironment {
|
): DevelopmentEnvironment {
|
||||||
const mode: RuntimeMode = 'development';
|
const mode: RuntimeMode = 'development';
|
||||||
let env = createEnvironment({
|
let env = createEnvironment({
|
||||||
|
@ -27,7 +29,7 @@ export function createDevelopmentEnvironment(
|
||||||
mode,
|
mode,
|
||||||
// This will be overridden in the dev server
|
// This will be overridden in the dev server
|
||||||
renderers: [],
|
renderers: [],
|
||||||
resolve: createResolve(viteServer),
|
resolve: createResolve(loader),
|
||||||
routeCache: new RouteCache(logging, mode),
|
routeCache: new RouteCache(logging, mode),
|
||||||
site: settings.config.site,
|
site: settings.config.site,
|
||||||
ssr: settings.config.output === 'server',
|
ssr: settings.config.output === 'server',
|
||||||
|
@ -36,7 +38,7 @@ export function createDevelopmentEnvironment(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...env,
|
...env,
|
||||||
viteServer,
|
loader,
|
||||||
settings,
|
settings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type { ViteDevServer } from 'vite';
|
|
||||||
import type {
|
import type {
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
|
@ -8,6 +7,7 @@ import type {
|
||||||
SSRElement,
|
SSRElement,
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||||
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
import { enhanceViteSSRError } from '../../errors/dev/index.js';
|
||||||
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js';
|
||||||
|
@ -39,26 +39,12 @@ export interface SSROptionsOld {
|
||||||
route?: RouteData;
|
route?: RouteData;
|
||||||
/** pass in route cache because SSR can’t manage cache-busting */
|
/** pass in route cache because SSR can’t manage cache-busting */
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
/** Vite instance */
|
/** Module loader (Vite) */
|
||||||
viteServer: ViteDevServer;
|
loader: ModuleLoader;
|
||||||
/** Request */
|
/** Request */
|
||||||
request: Request;
|
request: Request;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
filePath: options.filePath
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = createRenderContext({
|
|
||||||
request: options.request,
|
|
||||||
origin: options.origin,
|
|
||||||
pathname: options.pathname,
|
|
||||||
scripts,
|
|
||||||
links,
|
|
||||||
styles,
|
|
||||||
route: options.route
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface SSROptions {
|
export interface SSROptions {
|
||||||
/** The environment instance */
|
/** The environment instance */
|
||||||
env: DevelopmentEnvironment;
|
env: DevelopmentEnvironment;
|
||||||
|
@ -79,10 +65,10 @@ export interface SSROptions {
|
||||||
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
export type ComponentPreload = [SSRLoadedRenderer[], ComponentInstance];
|
||||||
|
|
||||||
export async function loadRenderers(
|
export async function loadRenderers(
|
||||||
viteServer: ViteDevServer,
|
moduleLoader: ModuleLoader,
|
||||||
settings: AstroSettings
|
settings: AstroSettings
|
||||||
): Promise<SSRLoadedRenderer[]> {
|
): Promise<SSRLoadedRenderer[]> {
|
||||||
const loader = (entry: string) => viteServer.ssrLoadModule(entry);
|
const loader = (entry: string) => moduleLoader.import(entry);
|
||||||
const renderers = await Promise.all(settings.renderers.map((r) => loadRenderer(r, loader)));
|
const renderers = await Promise.all(settings.renderers.map((r) => loadRenderer(r, loader)));
|
||||||
return filterFoundRenderers(renderers);
|
return filterFoundRenderers(renderers);
|
||||||
}
|
}
|
||||||
|
@ -92,11 +78,11 @@ export async function preload({
|
||||||
filePath,
|
filePath,
|
||||||
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
|
}: Pick<SSROptions, 'env' | 'filePath'>): Promise<ComponentPreload> {
|
||||||
// Important: This needs to happen first, in case a renderer provides polyfills.
|
// Important: This needs to happen first, in case a renderer provides polyfills.
|
||||||
const renderers = await loadRenderers(env.viteServer, env.settings);
|
const renderers = await loadRenderers(env.loader, env.settings);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load the module from the Vite SSR Runtime.
|
// Load the module from the Vite SSR Runtime.
|
||||||
const mod = (await env.viteServer.ssrLoadModule(fileURLToPath(filePath))) as ComponentInstance;
|
const mod = (await env.loader.import(fileURLToPath(filePath))) as ComponentInstance;
|
||||||
return [renderers, mod];
|
return [renderers, mod];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the error came from Markdown or CSS, we already handled it and there's no need to enhance it
|
// If the error came from Markdown or CSS, we already handled it and there's no need to enhance it
|
||||||
|
@ -104,7 +90,7 @@ export async function preload({
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw enhanceViteSSRError(err as Error, filePath, env.viteServer);
|
throw enhanceViteSSRError(err as Error, filePath, env.loader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +101,7 @@ interface GetScriptsAndStylesParams {
|
||||||
|
|
||||||
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
|
async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams) {
|
||||||
// Add hoisted script tags
|
// Add hoisted script tags
|
||||||
const scripts = await getScriptsForURL(filePath, env.viteServer);
|
const scripts = await getScriptsForURL(filePath, env.loader);
|
||||||
|
|
||||||
// Inject HMR scripts
|
// Inject HMR scripts
|
||||||
if (isPage(filePath, env.settings) && env.mode === 'development') {
|
if (isPage(filePath, env.settings) && env.mode === 'development') {
|
||||||
|
@ -126,7 +112,7 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
|
||||||
scripts.add({
|
scripts.add({
|
||||||
props: {
|
props: {
|
||||||
type: 'module',
|
type: 'module',
|
||||||
src: await resolveIdToUrl(env.viteServer, 'astro/runtime/client/hmr.js'),
|
src: await resolveIdToUrl(env.loader, 'astro/runtime/client/hmr.js'),
|
||||||
},
|
},
|
||||||
children: '',
|
children: '',
|
||||||
});
|
});
|
||||||
|
@ -148,7 +134,7 @@ async function getScriptsAndStyles({ env, filePath }: GetScriptsAndStylesParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass framework CSS in as style tags to be appended to the page.
|
// Pass framework CSS in as style tags to be appended to the page.
|
||||||
const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, env.viteServer, env.mode);
|
const { urls: styleUrls, stylesMap } = await getStylesForURL(filePath, env.loader, env.mode);
|
||||||
let links = new Set<SSRElement>();
|
let links = new Set<SSRElement>();
|
||||||
[...styleUrls].forEach((href) => {
|
[...styleUrls].forEach((href) => {
|
||||||
links.add({
|
links.add({
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import type { ViteDevServer } from 'vite';
|
import type { ModuleLoader } from '../../module-loader/index';
|
||||||
import { resolveIdToUrl } from '../../util.js';
|
import { resolveIdToUrl } from '../../util.js';
|
||||||
|
|
||||||
export function createResolve(viteServer: ViteDevServer) {
|
export function createResolve(loader: ModuleLoader) {
|
||||||
// Resolves specifiers in the inline hydrated scripts, such as:
|
// Resolves specifiers in the inline hydrated scripts, such as:
|
||||||
// - @astrojs/preact/client.js
|
// - @astrojs/preact/client.js
|
||||||
// - @/components/Foo.vue
|
// - @/components/Foo.vue
|
||||||
// - /Users/macos/project/src/Foo.vue
|
// - /Users/macos/project/src/Foo.vue
|
||||||
// - C:/Windows/project/src/Foo.vue (normalized slash)
|
// - C:/Windows/project/src/Foo.vue (normalized slash)
|
||||||
return async function (s: string) {
|
return async function (s: string) {
|
||||||
const url = await resolveIdToUrl(viteServer, s);
|
const url = await resolveIdToUrl(loader, s);
|
||||||
// Vite does not resolve .jsx -> .tsx when coming from hydration script import,
|
// Vite does not resolve .jsx -> .tsx when coming from hydration script import,
|
||||||
// clip it so Vite is able to resolve implicitly.
|
// clip it so Vite is able to resolve implicitly.
|
||||||
if (url.startsWith('/@fs') && url.endsWith('.jsx')) {
|
if (url.startsWith('/@fs') && url.endsWith('.jsx')) {
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import type { ModuleInfo } from 'rollup';
|
|
||||||
import vite from 'vite';
|
|
||||||
import type { SSRElement } from '../../../@types/astro';
|
import type { SSRElement } from '../../../@types/astro';
|
||||||
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
|
import type { PluginMetadata as AstroPluginMetadata } from '../../../vite-plugin-astro/types';
|
||||||
|
import type { ModuleInfo, ModuleLoader } from '../../module-loader/index';
|
||||||
|
|
||||||
import { viteID } from '../../util.js';
|
import { viteID } from '../../util.js';
|
||||||
import { createModuleScriptElementWithSrc } from '../ssr-element.js';
|
import { createModuleScriptElementWithSrc } from '../ssr-element.js';
|
||||||
import { crawlGraph } from './vite.js';
|
import { crawlGraph } from './vite.js';
|
||||||
|
|
||||||
export async function getScriptsForURL(
|
export async function getScriptsForURL(
|
||||||
filePath: URL,
|
filePath: URL,
|
||||||
viteServer: vite.ViteDevServer
|
loader: ModuleLoader
|
||||||
): Promise<Set<SSRElement>> {
|
): Promise<Set<SSRElement>> {
|
||||||
const elements = new Set<SSRElement>();
|
const elements = new Set<SSRElement>();
|
||||||
const rootID = viteID(filePath);
|
const rootID = viteID(filePath);
|
||||||
const modInfo = viteServer.pluginContainer.getModuleInfo(rootID);
|
const modInfo = loader.getModuleInfo(rootID);
|
||||||
addHoistedScripts(elements, modInfo);
|
addHoistedScripts(elements, modInfo);
|
||||||
for await (const moduleNode of crawlGraph(viteServer, rootID, true)) {
|
for await (const moduleNode of crawlGraph(loader, rootID, true)) {
|
||||||
const id = moduleNode.id;
|
const id = moduleNode.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
const info = viteServer.pluginContainer.getModuleInfo(id);
|
const info = loader.getModuleInfo(id);
|
||||||
addHoistedScripts(elements, info);
|
addHoistedScripts(elements, info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import type { ModuleLoader, ModuleNode } from '../../module-loader/index';
|
||||||
|
|
||||||
import npath from 'path';
|
import npath from 'path';
|
||||||
import vite from 'vite';
|
|
||||||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js';
|
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js';
|
||||||
import { unwrapId } from '../../util.js';
|
import { unwrapId } from '../../util.js';
|
||||||
import { STYLE_EXTENSIONS } from '../util.js';
|
import { STYLE_EXTENSIONS } from '../util.js';
|
||||||
|
@ -14,21 +15,21 @@ const STRIP_QUERY_PARAMS_REGEX = /\?.*$/;
|
||||||
|
|
||||||
/** recursively crawl the module graph to get all style files imported by parent id */
|
/** recursively crawl the module graph to get all style files imported by parent id */
|
||||||
export async function* crawlGraph(
|
export async function* crawlGraph(
|
||||||
viteServer: vite.ViteDevServer,
|
loader: ModuleLoader,
|
||||||
_id: string,
|
_id: string,
|
||||||
isRootFile: boolean,
|
isRootFile: boolean,
|
||||||
scanned = new Set<string>()
|
scanned = new Set<string>()
|
||||||
): AsyncGenerator<vite.ModuleNode, void, unknown> {
|
): AsyncGenerator<ModuleNode, void, unknown> {
|
||||||
const id = unwrapId(_id);
|
const id = unwrapId(_id);
|
||||||
const importedModules = new Set<vite.ModuleNode>();
|
const importedModules = new Set<ModuleNode>();
|
||||||
const moduleEntriesForId = isRootFile
|
const moduleEntriesForId = isRootFile
|
||||||
? // "getModulesByFile" pulls from a delayed module cache (fun implementation detail),
|
? // "getModulesByFile" pulls from a delayed module cache (fun implementation detail),
|
||||||
// So we can get up-to-date info on initial server load.
|
// So we can get up-to-date info on initial server load.
|
||||||
// Needed for slower CSS preprocessing like Tailwind
|
// Needed for slower CSS preprocessing like Tailwind
|
||||||
viteServer.moduleGraph.getModulesByFile(id) ?? new Set()
|
loader.getModulesByFile(id) ?? new Set()
|
||||||
: // For non-root files, we're safe to pull from "getModuleById" based on testing.
|
: // For non-root files, we're safe to pull from "getModuleById" based on testing.
|
||||||
// TODO: Find better invalidation strat to use "getModuleById" in all cases!
|
// TODO: Find better invalidation strat to use "getModuleById" in all cases!
|
||||||
new Set([viteServer.moduleGraph.getModuleById(id)]);
|
new Set([loader.getModuleById(id)]);
|
||||||
|
|
||||||
// Collect all imported modules for the module(s).
|
// Collect all imported modules for the module(s).
|
||||||
for (const entry of moduleEntriesForId) {
|
for (const entry of moduleEntriesForId) {
|
||||||
|
@ -57,10 +58,10 @@ export async function* crawlGraph(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (fileExtensionsToSSR.has(npath.extname(importedModulePathname))) {
|
if (fileExtensionsToSSR.has(npath.extname(importedModulePathname))) {
|
||||||
const mod = viteServer.moduleGraph.getModuleById(importedModule.id);
|
const mod = loader.getModuleById(importedModule.id);
|
||||||
if (!mod?.ssrModule) {
|
if (!mod?.ssrModule) {
|
||||||
try {
|
try {
|
||||||
await viteServer.ssrLoadModule(importedModule.id);
|
await loader.import(importedModule.id);
|
||||||
} catch {
|
} catch {
|
||||||
/** Likely an out-of-date module entry! Silently continue. */
|
/** Likely an out-of-date module entry! Silently continue. */
|
||||||
}
|
}
|
||||||
|
@ -80,6 +81,6 @@ export async function* crawlGraph(
|
||||||
}
|
}
|
||||||
|
|
||||||
yield importedModule;
|
yield importedModule;
|
||||||
yield* crawlGraph(viteServer, importedModule.id, false, scanned);
|
yield* crawlGraph(loader, importedModule.id, false, scanned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
import type { LogOptions } from '../../logger/core';
|
import type { LogOptions } from '../../logger/core';
|
||||||
|
|
||||||
import fs from 'fs';
|
import nodeFs from 'fs';
|
||||||
import { createRequire } from 'module';
|
import { createRequire } from 'module';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import slash from 'slash';
|
import slash from 'slash';
|
||||||
|
@ -200,9 +200,18 @@ function injectedRouteToItem(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateRouteManifestParams {
|
||||||
|
/** Astro Settings object */
|
||||||
|
settings: AstroSettings;
|
||||||
|
/** Current working directory */
|
||||||
|
cwd?: string;
|
||||||
|
/** fs module, for testing */
|
||||||
|
fsMod?: typeof nodeFs;
|
||||||
|
}
|
||||||
|
|
||||||
/** Create manifest of all static routes */
|
/** Create manifest of all static routes */
|
||||||
export function createRouteManifest(
|
export function createRouteManifest(
|
||||||
{ settings, cwd }: { settings: AstroSettings; cwd?: string },
|
{ settings, cwd, fsMod }: CreateRouteManifestParams,
|
||||||
logging: LogOptions
|
logging: LogOptions
|
||||||
): ManifestData {
|
): ManifestData {
|
||||||
const components: string[] = [];
|
const components: string[] = [];
|
||||||
|
@ -213,8 +222,9 @@ export function createRouteManifest(
|
||||||
...settings.pageExtensions,
|
...settings.pageExtensions,
|
||||||
]);
|
]);
|
||||||
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
const validEndpointExtensions: Set<string> = new Set(['.js', '.ts']);
|
||||||
|
const localFs = fsMod ?? nodeFs;
|
||||||
|
|
||||||
function walk(dir: string, parentSegments: RoutePart[][], parentParams: string[]) {
|
function walk(fs: typeof nodeFs, dir: string, parentSegments: RoutePart[][], parentParams: string[]) {
|
||||||
let items: Item[] = [];
|
let items: Item[] = [];
|
||||||
fs.readdirSync(dir).forEach((basename) => {
|
fs.readdirSync(dir).forEach((basename) => {
|
||||||
const resolved = path.join(dir, basename);
|
const resolved = path.join(dir, basename);
|
||||||
|
@ -291,7 +301,7 @@ export function createRouteManifest(
|
||||||
params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content));
|
params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content));
|
||||||
|
|
||||||
if (item.isDir) {
|
if (item.isDir) {
|
||||||
walk(path.join(dir, item.basename), segments, params);
|
walk(fsMod ?? fs, path.join(dir, item.basename), segments, params);
|
||||||
} else {
|
} else {
|
||||||
components.push(item.file);
|
components.push(item.file);
|
||||||
const component = item.file;
|
const component = item.file;
|
||||||
|
@ -322,8 +332,8 @@ export function createRouteManifest(
|
||||||
const { config } = settings;
|
const { config } = settings;
|
||||||
const pages = resolvePages(config);
|
const pages = resolvePages(config);
|
||||||
|
|
||||||
if (fs.existsSync(pages)) {
|
if (localFs.existsSync(pages)) {
|
||||||
walk(fileURLToPath(pages), [], []);
|
walk(localFs, fileURLToPath(pages), [], []);
|
||||||
} else if (settings.injectedRoutes.length === 0) {
|
} else if (settings.injectedRoutes.length === 0) {
|
||||||
const pagesDirRootRelative = pages.href.slice(settings.config.root.href.length);
|
const pagesDirRootRelative = pages.href.slice(settings.config.root.href.length);
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import type { ModuleLoader } from './module-loader';
|
||||||
|
import eol from 'eol';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import resolve from 'resolve';
|
import resolve from 'resolve';
|
||||||
import slash from 'slash';
|
import slash from 'slash';
|
||||||
import { fileURLToPath, pathToFileURL } from 'url';
|
import { fileURLToPath, pathToFileURL } from 'url';
|
||||||
import { normalizePath, ViteDevServer } from 'vite';
|
import { normalizePath } from 'vite';
|
||||||
import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro';
|
import type { AstroConfig, AstroSettings, RouteType } from '../@types/astro';
|
||||||
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
|
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './constants.js';
|
||||||
import { prependForwardSlash, removeTrailingForwardSlash } from './path.js';
|
import { prependForwardSlash, removeTrailingForwardSlash } from './path.js';
|
||||||
|
@ -180,19 +182,19 @@ export function getLocalAddress(serverAddress: string, host: string | boolean):
|
||||||
*/
|
*/
|
||||||
// NOTE: `/@id/` should only be used when the id is fully resolved
|
// NOTE: `/@id/` should only be used when the id is fully resolved
|
||||||
// TODO: Export a helper util from Vite
|
// TODO: Export a helper util from Vite
|
||||||
export async function resolveIdToUrl(viteServer: ViteDevServer, id: string) {
|
export async function resolveIdToUrl(loader: ModuleLoader, id: string) {
|
||||||
let result = await viteServer.pluginContainer.resolveId(id, undefined);
|
let resultId = await loader.resolveId(id, undefined);
|
||||||
// Try resolve jsx to tsx
|
// Try resolve jsx to tsx
|
||||||
if (!result && id.endsWith('.jsx')) {
|
if (!resultId && id.endsWith('.jsx')) {
|
||||||
result = await viteServer.pluginContainer.resolveId(id.slice(0, -4), undefined);
|
resultId = await loader.resolveId(id.slice(0, -4), undefined);
|
||||||
}
|
}
|
||||||
if (!result) {
|
if (!resultId) {
|
||||||
return VALID_ID_PREFIX + id;
|
return VALID_ID_PREFIX + id;
|
||||||
}
|
}
|
||||||
if (path.isAbsolute(result.id)) {
|
if (path.isAbsolute(resultId)) {
|
||||||
return '/@fs' + prependForwardSlash(result.id);
|
return '/@fs' + prependForwardSlash(resultId);
|
||||||
}
|
}
|
||||||
return VALID_ID_PREFIX + result.id;
|
return VALID_ID_PREFIX + resultId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveJsToTs(filePath: string) {
|
export function resolveJsToTs(filePath: string) {
|
||||||
|
|
46
packages/astro/src/vite-plugin-astro-server/base.ts
Normal file
46
packages/astro/src/vite-plugin-astro-server/base.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import type * as vite from 'vite';
|
||||||
|
import type { AstroSettings } from '../@types/astro';
|
||||||
|
|
||||||
|
import { LogOptions } from '../core/logger/core.js';
|
||||||
|
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
||||||
|
import { log404 } from './common.js';
|
||||||
|
import { writeHtmlResponse } from './response.js';
|
||||||
|
|
||||||
|
export function baseMiddleware(
|
||||||
|
settings: AstroSettings,
|
||||||
|
logging: LogOptions
|
||||||
|
): vite.Connect.NextHandleFunction {
|
||||||
|
const { config } = settings;
|
||||||
|
const site = config.site ? new URL(config.base, config.site) : undefined;
|
||||||
|
const devRoot = site ? site.pathname : '/';
|
||||||
|
|
||||||
|
return function devBaseMiddleware(req, res, next) {
|
||||||
|
const url = req.url!;
|
||||||
|
|
||||||
|
const pathname = decodeURI(new URL(url, 'http://vitejs.dev').pathname);
|
||||||
|
|
||||||
|
if (pathname.startsWith(devRoot)) {
|
||||||
|
req.url = url.replace(devRoot, '/');
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === '/' || pathname === '/index.html') {
|
||||||
|
log404(logging, pathname);
|
||||||
|
const html = subpathNotUsedTemplate(devRoot, pathname);
|
||||||
|
return writeHtmlResponse(res, 404, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.headers.accept?.includes('text/html')) {
|
||||||
|
log404(logging, pathname);
|
||||||
|
const html = notFoundTemplate({
|
||||||
|
statusCode: 404,
|
||||||
|
title: 'Not found',
|
||||||
|
tabTitle: '404: Not Found',
|
||||||
|
pathname,
|
||||||
|
});
|
||||||
|
return writeHtmlResponse(res, 404, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
6
packages/astro/src/vite-plugin-astro-server/common.ts
Normal file
6
packages/astro/src/vite-plugin-astro-server/common.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { info, LogOptions } from '../core/logger/core.js';
|
||||||
|
import * as msg from '../core/messages.js';
|
||||||
|
|
||||||
|
export function log404(logging: LogOptions, pathname: string) {
|
||||||
|
info(logging, 'serve', msg.req({ url: pathname, statusCode: 404 }));
|
||||||
|
}
|
100
packages/astro/src/vite-plugin-astro-server/controller.ts
Normal file
100
packages/astro/src/vite-plugin-astro-server/controller.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import type { ServerState } from './server-state';
|
||||||
|
import type { LoaderEvents, ModuleLoader } from '../core/module-loader/index';
|
||||||
|
|
||||||
|
import { createServerState, setRouteError, setServerError, clearRouteError } from './server-state.js';
|
||||||
|
|
||||||
|
type ReloadFn = () => void;
|
||||||
|
|
||||||
|
export interface DevServerController {
|
||||||
|
state: ServerState;
|
||||||
|
onFileChange: LoaderEvents['file-change'];
|
||||||
|
onHMRError: LoaderEvents['hmr-error'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateControllerParams = {
|
||||||
|
loader: ModuleLoader;
|
||||||
|
} | {
|
||||||
|
reload: ReloadFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createController(params: CreateControllerParams): DevServerController {
|
||||||
|
if('loader' in params) {
|
||||||
|
return createLoaderController(params.loader);
|
||||||
|
} else {
|
||||||
|
return createBaseController(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBaseController({ reload }: { reload: ReloadFn }): DevServerController {
|
||||||
|
const serverState = createServerState();
|
||||||
|
|
||||||
|
const onFileChange: LoaderEvents['file-change'] = () => {
|
||||||
|
if(serverState.state === 'error') {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHMRError: LoaderEvents['hmr-error'] = (payload) => {
|
||||||
|
let msg = payload?.err?.message ?? 'Unknown error';
|
||||||
|
let stack = payload?.err?.stack ?? 'Unknown stack';
|
||||||
|
let error = new Error(msg);
|
||||||
|
Object.defineProperty(error, 'stack', {
|
||||||
|
value: stack
|
||||||
|
});
|
||||||
|
setServerError(serverState, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: serverState,
|
||||||
|
onFileChange,
|
||||||
|
onHMRError
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLoaderController(loader: ModuleLoader): DevServerController {
|
||||||
|
const controller = createBaseController({
|
||||||
|
reload() {
|
||||||
|
loader.clientReload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const baseOnFileChange = controller.onFileChange;
|
||||||
|
controller.onFileChange = (...args) => {
|
||||||
|
if(controller.state.state === 'error') {
|
||||||
|
// If we are in an error state, check if there are any modules with errors
|
||||||
|
// and if so invalidate them so that they will be updated on refresh.
|
||||||
|
loader.eachModule(mod => {
|
||||||
|
if(mod.ssrError) {
|
||||||
|
loader.invalidateModule(mod);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
baseOnFileChange(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.events.on('file-change', controller.onFileChange);
|
||||||
|
loader.events.on('hmr-error', controller.onHMRError);
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunWithErrorHandlingParams {
|
||||||
|
controller: DevServerController;
|
||||||
|
pathname: string;
|
||||||
|
run: () => Promise<any>;
|
||||||
|
onError: (error: unknown) => Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runWithErrorHandling({
|
||||||
|
controller: { state },
|
||||||
|
pathname,
|
||||||
|
run,
|
||||||
|
onError
|
||||||
|
}: RunWithErrorHandlingParams) {
|
||||||
|
try {
|
||||||
|
await run();
|
||||||
|
clearRouteError(state, pathname);
|
||||||
|
} catch(err) {
|
||||||
|
const error = onError(err);
|
||||||
|
setRouteError(state, pathname, error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,439 +1,10 @@
|
||||||
import type http from 'http';
|
export {
|
||||||
import mime from 'mime';
|
createController,
|
||||||
import type * as vite from 'vite';
|
runWithErrorHandling
|
||||||
import type { AstroSettings, ManifestData } from '../@types/astro';
|
} from './controller.js';
|
||||||
import { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
export {
|
||||||
|
default as vitePluginAstroServer
|
||||||
import { Readable } from 'stream';
|
} from './plugin.js';
|
||||||
import { attachToResponse, getSetCookiesFromResponse } from '../core/cookies/index.js';
|
export {
|
||||||
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
handleRequest
|
||||||
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
|
} from './request.js';
|
||||||
import { collectErrorMetadata, getViteErrorPayload } from '../core/errors/dev/index.js';
|
|
||||||
import type { ErrorWithMetadata } from '../core/errors/index.js';
|
|
||||||
import { createSafeError } from '../core/errors/index.js';
|
|
||||||
import { error, info, LogOptions, warn } from '../core/logger/core.js';
|
|
||||||
import * as msg from '../core/messages.js';
|
|
||||||
import { appendForwardSlash } from '../core/path.js';
|
|
||||||
import { createDevelopmentEnvironment, preload, renderPage } from '../core/render/dev/index.js';
|
|
||||||
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
|
||||||
import { createRequest } from '../core/request.js';
|
|
||||||
import { createRouteManifest, matchAllRoutes } from '../core/routing/index.js';
|
|
||||||
import { resolvePages } from '../core/util.js';
|
|
||||||
import notFoundTemplate, { subpathNotUsedTemplate } from '../template/4xx.js';
|
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
|
||||||
settings: AstroSettings;
|
|
||||||
logging: LogOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
|
||||||
...args: any
|
|
||||||
) => Promise<infer R>
|
|
||||||
? R
|
|
||||||
: any;
|
|
||||||
|
|
||||||
function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string) {
|
|
||||||
res.writeHead(statusCode, {
|
|
||||||
'Content-Type': 'text/html; charset=utf-8',
|
|
||||||
'Content-Length': Buffer.byteLength(html, 'utf-8'),
|
|
||||||
});
|
|
||||||
res.write(html);
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeWebResponse(res: http.ServerResponse, webResponse: Response) {
|
|
||||||
const { status, headers, body } = webResponse;
|
|
||||||
|
|
||||||
let _headers = {};
|
|
||||||
if ('raw' in headers) {
|
|
||||||
// Node fetch allows you to get the raw headers, which includes multiples of the same type.
|
|
||||||
// This is needed because Set-Cookie *must* be called for each cookie, and can't be
|
|
||||||
// concatenated together.
|
|
||||||
type HeadersWithRaw = Headers & {
|
|
||||||
raw: () => Record<string, string[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries((headers as HeadersWithRaw).raw())) {
|
|
||||||
res.setHeader(key, value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_headers = Object.fromEntries(headers.entries());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach any set-cookie headers added via Astro.cookies.set()
|
|
||||||
const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
|
|
||||||
if (setCookieHeaders.length) {
|
|
||||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
|
||||||
}
|
|
||||||
res.writeHead(status, _headers);
|
|
||||||
if (body) {
|
|
||||||
if (Symbol.for('astro.responseBody') in webResponse) {
|
|
||||||
let stream = (webResponse as any)[Symbol.for('astro.responseBody')];
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
res.write(chunk.toString());
|
|
||||||
}
|
|
||||||
} else if (body instanceof Readable) {
|
|
||||||
body.pipe(res);
|
|
||||||
return;
|
|
||||||
} else if (typeof body === 'string') {
|
|
||||||
res.write(body);
|
|
||||||
} else {
|
|
||||||
const reader = body.getReader();
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
if (value) {
|
|
||||||
res.write(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeSSRResult(webResponse: Response, res: http.ServerResponse) {
|
|
||||||
return writeWebResponse(res, webResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handle404Response(
|
|
||||||
origin: string,
|
|
||||||
req: http.IncomingMessage,
|
|
||||||
res: http.ServerResponse
|
|
||||||
) {
|
|
||||||
const pathname = decodeURI(new URL(origin + req.url).pathname);
|
|
||||||
|
|
||||||
const html = notFoundTemplate({
|
|
||||||
statusCode: 404,
|
|
||||||
title: 'Not found',
|
|
||||||
tabTitle: '404: Not Found',
|
|
||||||
pathname,
|
|
||||||
});
|
|
||||||
writeHtmlResponse(res, 404, html);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handle500Response(
|
|
||||||
viteServer: vite.ViteDevServer,
|
|
||||||
origin: string,
|
|
||||||
req: http.IncomingMessage,
|
|
||||||
res: http.ServerResponse,
|
|
||||||
err: ErrorWithMetadata
|
|
||||||
) {
|
|
||||||
res.on('close', () => setTimeout(() => viteServer.ws.send(getViteErrorPayload(err)), 200));
|
|
||||||
if (res.headersSent) {
|
|
||||||
res.write(`<script type="module" src="/@vite/client"></script>`);
|
|
||||||
res.end();
|
|
||||||
} else {
|
|
||||||
writeHtmlResponse(
|
|
||||||
res,
|
|
||||||
500,
|
|
||||||
`<title>${err.name}</title><script type="module" src="/@vite/client"></script>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCustom404Route({ config }: AstroSettings, manifest: ManifestData) {
|
|
||||||
// For Windows compat, use relative page paths to match the 404 route
|
|
||||||
const relPages = resolvePages(config).href.replace(config.root.href, '');
|
|
||||||
const pattern = new RegExp(`${appendForwardSlash(relPages)}404.(astro|md)`);
|
|
||||||
return manifest.routes.find((r) => r.component.match(pattern));
|
|
||||||
}
|
|
||||||
|
|
||||||
function log404(logging: LogOptions, pathname: string) {
|
|
||||||
info(logging, 'serve', msg.req({ url: pathname, statusCode: 404 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function baseMiddleware(
|
|
||||||
settings: AstroSettings,
|
|
||||||
logging: LogOptions
|
|
||||||
): vite.Connect.NextHandleFunction {
|
|
||||||
const { config } = settings;
|
|
||||||
const site = config.site ? new URL(config.base, config.site) : undefined;
|
|
||||||
const devRoot = site ? site.pathname : '/';
|
|
||||||
|
|
||||||
return function devBaseMiddleware(req, res, next) {
|
|
||||||
const url = req.url!;
|
|
||||||
|
|
||||||
const pathname = decodeURI(new URL(url, 'http://vitejs.dev').pathname);
|
|
||||||
|
|
||||||
if (pathname.startsWith(devRoot)) {
|
|
||||||
req.url = url.replace(devRoot, '/');
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === '/' || pathname === '/index.html') {
|
|
||||||
log404(logging, pathname);
|
|
||||||
const html = subpathNotUsedTemplate(devRoot, pathname);
|
|
||||||
return writeHtmlResponse(res, 404, html);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.headers.accept?.includes('text/html')) {
|
|
||||||
log404(logging, pathname);
|
|
||||||
const html = notFoundTemplate({
|
|
||||||
statusCode: 404,
|
|
||||||
title: 'Not found',
|
|
||||||
tabTitle: '404: Not Found',
|
|
||||||
pathname,
|
|
||||||
});
|
|
||||||
return writeHtmlResponse(res, 404, html);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function matchRoute(pathname: string, env: DevelopmentEnvironment, manifest: ManifestData) {
|
|
||||||
const { logging, settings, routeCache } = env;
|
|
||||||
const matches = matchAllRoutes(pathname, manifest);
|
|
||||||
|
|
||||||
for await (const maybeRoute of matches) {
|
|
||||||
const filePath = new URL(`./${maybeRoute.component}`, settings.config.root);
|
|
||||||
const preloadedComponent = await preload({ env, filePath });
|
|
||||||
const [, mod] = preloadedComponent;
|
|
||||||
// attempt to get static paths
|
|
||||||
// if this fails, we have a bad URL match!
|
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
|
||||||
mod,
|
|
||||||
route: maybeRoute,
|
|
||||||
routeCache,
|
|
||||||
pathname: pathname,
|
|
||||||
logging,
|
|
||||||
ssr: settings.config.output === 'server',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) {
|
|
||||||
return {
|
|
||||||
route: maybeRoute,
|
|
||||||
filePath,
|
|
||||||
preloadedComponent,
|
|
||||||
mod,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches.length) {
|
|
||||||
warn(
|
|
||||||
logging,
|
|
||||||
'getStaticPaths',
|
|
||||||
`Route pattern matched, but no matching static path found. (${pathname})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log404(logging, pathname);
|
|
||||||
const custom404 = getCustom404Route(settings, manifest);
|
|
||||||
|
|
||||||
if (custom404) {
|
|
||||||
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
|
||||||
const preloadedComponent = await preload({ env, filePath });
|
|
||||||
const [, mod] = preloadedComponent;
|
|
||||||
|
|
||||||
return {
|
|
||||||
route: custom404,
|
|
||||||
filePath,
|
|
||||||
preloadedComponent,
|
|
||||||
mod,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The main logic to route dev server requests to pages in Astro. */
|
|
||||||
async function handleRequest(
|
|
||||||
env: DevelopmentEnvironment,
|
|
||||||
manifest: ManifestData,
|
|
||||||
req: http.IncomingMessage,
|
|
||||||
res: http.ServerResponse
|
|
||||||
) {
|
|
||||||
const { settings, viteServer } = env;
|
|
||||||
const { config } = settings;
|
|
||||||
const origin = `${viteServer.config.server.https ? 'https' : 'http'}://${req.headers.host}`;
|
|
||||||
const buildingToSSR = config.output === 'server';
|
|
||||||
// Ignore `.html` extensions and `index.html` in request URLS to ensure that
|
|
||||||
// routing behavior matches production builds. This supports both file and directory
|
|
||||||
// build formats, and is necessary based on how the manifest tracks build targets.
|
|
||||||
const url = new URL(origin + req.url?.replace(/(index)?\.html$/, ''));
|
|
||||||
const pathname = decodeURI(url.pathname);
|
|
||||||
|
|
||||||
// Add config.base back to url before passing it to SSR
|
|
||||||
url.pathname = config.base.substring(0, config.base.length - 1) + url.pathname;
|
|
||||||
|
|
||||||
// HACK! @astrojs/image uses query params for the injected route in `dev`
|
|
||||||
if (!buildingToSSR && pathname !== '/_image') {
|
|
||||||
// Prevent user from depending on search params when not doing SSR.
|
|
||||||
// NOTE: Create an array copy here because deleting-while-iterating
|
|
||||||
// creates bugs where not all search params are removed.
|
|
||||||
const allSearchParams = Array.from(url.searchParams);
|
|
||||||
for (const [key] of allSearchParams) {
|
|
||||||
url.searchParams.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: ArrayBuffer | undefined = undefined;
|
|
||||||
if (!(req.method === 'GET' || req.method === 'HEAD')) {
|
|
||||||
let bytes: Uint8Array[] = [];
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
req.on('data', (part) => {
|
|
||||||
bytes.push(part);
|
|
||||||
});
|
|
||||||
req.on('end', resolve);
|
|
||||||
});
|
|
||||||
body = Buffer.concat(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const matchedRoute = await matchRoute(pathname, env, manifest);
|
|
||||||
return await handleRoute(matchedRoute, url, pathname, body, origin, env, manifest, req, res);
|
|
||||||
} catch (_err) {
|
|
||||||
// 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
|
|
||||||
const err = createSafeError(_err);
|
|
||||||
const errorWithMetadata = collectErrorMetadata(err);
|
|
||||||
|
|
||||||
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
|
||||||
handle500Response(viteServer, origin, req, res, errorWithMetadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRoute(
|
|
||||||
matchedRoute: AsyncReturnType<typeof matchRoute>,
|
|
||||||
url: URL,
|
|
||||||
pathname: string,
|
|
||||||
body: ArrayBuffer | undefined,
|
|
||||||
origin: string,
|
|
||||||
env: DevelopmentEnvironment,
|
|
||||||
manifest: ManifestData,
|
|
||||||
req: http.IncomingMessage,
|
|
||||||
res: http.ServerResponse
|
|
||||||
): Promise<void> {
|
|
||||||
const { logging, settings } = env;
|
|
||||||
if (!matchedRoute) {
|
|
||||||
return handle404Response(origin, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { config } = settings;
|
|
||||||
const filePath: URL | undefined = matchedRoute.filePath;
|
|
||||||
const { route, preloadedComponent, mod } = matchedRoute;
|
|
||||||
const buildingToSSR = config.output === 'server';
|
|
||||||
|
|
||||||
// Headers are only available when using SSR.
|
|
||||||
const request = createRequest({
|
|
||||||
url,
|
|
||||||
headers: buildingToSSR ? req.headers : new Headers(),
|
|
||||||
method: req.method,
|
|
||||||
body,
|
|
||||||
logging,
|
|
||||||
ssr: buildingToSSR,
|
|
||||||
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// attempt to get static paths
|
|
||||||
// if this fails, we have a bad URL match!
|
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
|
||||||
mod,
|
|
||||||
route,
|
|
||||||
routeCache: env.routeCache,
|
|
||||||
pathname: pathname,
|
|
||||||
logging,
|
|
||||||
ssr: config.output === 'server',
|
|
||||||
});
|
|
||||||
|
|
||||||
const options: SSROptions = {
|
|
||||||
env,
|
|
||||||
filePath,
|
|
||||||
origin,
|
|
||||||
preload: preloadedComponent,
|
|
||||||
pathname,
|
|
||||||
request,
|
|
||||||
route,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Route successfully matched! Render it.
|
|
||||||
if (route.type === 'endpoint') {
|
|
||||||
const result = await callEndpoint(options);
|
|
||||||
if (result.type === 'response') {
|
|
||||||
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
|
||||||
const fourOhFourRoute = await matchRoute('/404', env, manifest);
|
|
||||||
return handleRoute(
|
|
||||||
fourOhFourRoute,
|
|
||||||
new URL('/404', url),
|
|
||||||
'/404',
|
|
||||||
body,
|
|
||||||
origin,
|
|
||||||
env,
|
|
||||||
manifest,
|
|
||||||
req,
|
|
||||||
res
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throwIfRedirectNotAllowed(result.response, config);
|
|
||||||
await writeWebResponse(res, result.response);
|
|
||||||
} else {
|
|
||||||
let contentType = 'text/plain';
|
|
||||||
// Dynamic routes don’t include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
|
|
||||||
const filepath =
|
|
||||||
route.pathname ||
|
|
||||||
route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/');
|
|
||||||
const computedMimeType = mime.getType(filepath);
|
|
||||||
if (computedMimeType) {
|
|
||||||
contentType = computedMimeType;
|
|
||||||
}
|
|
||||||
const response = new Response(result.body, {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': `${contentType};charset=utf-8`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
attachToResponse(response, result.cookies);
|
|
||||||
await writeWebResponse(res, response);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const result = await renderPage(options);
|
|
||||||
throwIfRedirectNotAllowed(result, config);
|
|
||||||
return await writeSSRResult(result, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createPlugin({ settings, logging }: AstroPluginOptions): vite.Plugin {
|
|
||||||
return {
|
|
||||||
name: 'astro:server',
|
|
||||||
configureServer(viteServer) {
|
|
||||||
let env = createDevelopmentEnvironment(settings, logging, viteServer);
|
|
||||||
let manifest: ManifestData = createRouteManifest({ settings }, logging);
|
|
||||||
|
|
||||||
/** rebuild the route cache + manifest, as needed. */
|
|
||||||
function rebuildManifest(needsManifestRebuild: boolean, file: string) {
|
|
||||||
env.routeCache.clearAll();
|
|
||||||
if (needsManifestRebuild) {
|
|
||||||
manifest = createRouteManifest({ settings }, logging);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Rebuild route manifest on file change, if needed.
|
|
||||||
viteServer.watcher.on('add', rebuildManifest.bind(null, true));
|
|
||||||
viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
|
|
||||||
viteServer.watcher.on('change', rebuildManifest.bind(null, false));
|
|
||||||
return () => {
|
|
||||||
// Push this middleware to the front of the stack so that it can intercept responses.
|
|
||||||
if (settings.config.base !== '/') {
|
|
||||||
viteServer.middlewares.stack.unshift({
|
|
||||||
route: '',
|
|
||||||
handle: baseMiddleware(settings, logging),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
viteServer.middlewares.use(async (req, res) => {
|
|
||||||
if (!req.url || !req.method) {
|
|
||||||
throw new Error('Incomplete request');
|
|
||||||
}
|
|
||||||
handleRequest(env, manifest, req, res);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
// HACK: hide `.tip` in Vite's ErrorOverlay and replace [vite] messages with [astro]
|
|
||||||
transform(code, id, opts = {}) {
|
|
||||||
if (opts.ssr) return;
|
|
||||||
if (!id.includes('vite/dist/client/client.mjs')) return;
|
|
||||||
return code
|
|
||||||
.replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
|
|
||||||
.replace(/\[vite\]/g, '[astro]');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
66
packages/astro/src/vite-plugin-astro-server/plugin.ts
Normal file
66
packages/astro/src/vite-plugin-astro-server/plugin.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
|
||||||
|
import type * as vite from 'vite';
|
||||||
|
import type { AstroSettings, ManifestData } from '../@types/astro';
|
||||||
|
|
||||||
|
import { LogOptions } from '../core/logger/core.js';
|
||||||
|
import { createDevelopmentEnvironment } from '../core/render/dev/index.js';
|
||||||
|
import { createRouteManifest } from '../core/routing/index.js';
|
||||||
|
import { createViteLoader } from '../core/module-loader/index.js';
|
||||||
|
import { baseMiddleware } from './base.js';
|
||||||
|
import { handleRequest } from './request.js';
|
||||||
|
import { createController } from './controller.js';
|
||||||
|
import type fs from 'fs';
|
||||||
|
|
||||||
|
export interface AstroPluginOptions {
|
||||||
|
settings: AstroSettings;
|
||||||
|
logging: LogOptions;
|
||||||
|
fs: typeof fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createVitePluginAstroServer({ settings, logging, fs: fsMod }: AstroPluginOptions): vite.Plugin {
|
||||||
|
return {
|
||||||
|
name: 'astro:server',
|
||||||
|
configureServer(viteServer) {
|
||||||
|
const loader = createViteLoader(viteServer);
|
||||||
|
let env = createDevelopmentEnvironment(settings, logging, loader);
|
||||||
|
let manifest: ManifestData = createRouteManifest({ settings, fsMod }, logging);
|
||||||
|
const serverController = createController({ loader });
|
||||||
|
|
||||||
|
/** rebuild the route cache + manifest, as needed. */
|
||||||
|
function rebuildManifest(needsManifestRebuild: boolean, _file: string) {
|
||||||
|
env.routeCache.clearAll();
|
||||||
|
if (needsManifestRebuild) {
|
||||||
|
manifest = createRouteManifest({ settings }, logging);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rebuild route manifest on file change, if needed.
|
||||||
|
viteServer.watcher.on('add', rebuildManifest.bind(null, true));
|
||||||
|
viteServer.watcher.on('unlink', rebuildManifest.bind(null, true));
|
||||||
|
viteServer.watcher.on('change', rebuildManifest.bind(null, false));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Push this middleware to the front of the stack so that it can intercept responses.
|
||||||
|
if (settings.config.base !== '/') {
|
||||||
|
viteServer.middlewares.stack.unshift({
|
||||||
|
route: '',
|
||||||
|
handle: baseMiddleware(settings, logging),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
viteServer.middlewares.use(async (req, res) => {
|
||||||
|
if (!req.url || !req.method) {
|
||||||
|
throw new Error('Incomplete request');
|
||||||
|
}
|
||||||
|
handleRequest(env, manifest, serverController, req, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// HACK: hide `.tip` in Vite's ErrorOverlay and replace [vite] messages with [astro]
|
||||||
|
transform(code, id, opts = {}) {
|
||||||
|
if (opts.ssr) return;
|
||||||
|
if (!id.includes('vite/dist/client/client.mjs')) return;
|
||||||
|
return code
|
||||||
|
.replace(/\.tip \{[^}]*\}/gm, '.tip {\n display: none;\n}')
|
||||||
|
.replace(/\[vite\]/g, '[astro]');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
78
packages/astro/src/vite-plugin-astro-server/request.ts
Normal file
78
packages/astro/src/vite-plugin-astro-server/request.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import type http from 'http';
|
||||||
|
import type { ManifestData, RouteData } from '../@types/astro';
|
||||||
|
import type { DevServerController } from './controller';
|
||||||
|
import type { DevelopmentEnvironment } from '../core/render/dev/index';
|
||||||
|
|
||||||
|
import { collectErrorMetadata } from '../core/errors/dev/index.js';
|
||||||
|
import { error } from '../core/logger/core.js';
|
||||||
|
import * as msg from '../core/messages.js';
|
||||||
|
import { handleRoute, matchRoute } from './route.js';
|
||||||
|
import { handle500Response } from './response.js';
|
||||||
|
import { runWithErrorHandling } from './controller.js';
|
||||||
|
import { createSafeError } from '../core/errors/index.js';
|
||||||
|
|
||||||
|
/** The main logic to route dev server requests to pages in Astro. */
|
||||||
|
export async function handleRequest(
|
||||||
|
env: DevelopmentEnvironment,
|
||||||
|
manifest: ManifestData,
|
||||||
|
controller: DevServerController,
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse
|
||||||
|
) {
|
||||||
|
const { settings, loader: moduleLoader } = env;
|
||||||
|
const { config } = settings;
|
||||||
|
const origin = `${moduleLoader.isHttps() ? 'https' : 'http'}://${req.headers.host}`;
|
||||||
|
const buildingToSSR = config.output === 'server';
|
||||||
|
// Ignore `.html` extensions and `index.html` in request URLS to ensure that
|
||||||
|
// routing behavior matches production builds. This supports both file and directory
|
||||||
|
// build formats, and is necessary based on how the manifest tracks build targets.
|
||||||
|
const url = new URL(origin + req.url?.replace(/(index)?\.html$/, ''));
|
||||||
|
const pathname = decodeURI(url.pathname);
|
||||||
|
|
||||||
|
// Add config.base back to url before passing it to SSR
|
||||||
|
url.pathname = config.base.substring(0, config.base.length - 1) + url.pathname;
|
||||||
|
|
||||||
|
// HACK! @astrojs/image uses query params for the injected route in `dev`
|
||||||
|
if (!buildingToSSR && pathname !== '/_image') {
|
||||||
|
// Prevent user from depending on search params when not doing SSR.
|
||||||
|
// NOTE: Create an array copy here because deleting-while-iterating
|
||||||
|
// creates bugs where not all search params are removed.
|
||||||
|
const allSearchParams = Array.from(url.searchParams);
|
||||||
|
for (const [key] of allSearchParams) {
|
||||||
|
url.searchParams.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: ArrayBuffer | undefined = undefined;
|
||||||
|
if (!(req.method === 'GET' || req.method === 'HEAD')) {
|
||||||
|
let bytes: Uint8Array[] = [];
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
req.on('data', (part) => {
|
||||||
|
bytes.push(part);
|
||||||
|
});
|
||||||
|
req.on('end', resolve);
|
||||||
|
});
|
||||||
|
body = Buffer.concat(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname,
|
||||||
|
async run() {
|
||||||
|
const matchedRoute = await matchRoute(pathname, env, manifest);
|
||||||
|
|
||||||
|
return await handleRoute(matchedRoute, url, pathname, body, origin, env, manifest, req, res);
|
||||||
|
},
|
||||||
|
onError(_err) {
|
||||||
|
const err = createSafeError(_err);
|
||||||
|
// 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
|
||||||
|
const errorWithMetadata = collectErrorMetadata(err);
|
||||||
|
|
||||||
|
error(env.logging, null, msg.formatErrorMessage(errorWithMetadata));
|
||||||
|
handle500Response(moduleLoader, res, errorWithMetadata);
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
106
packages/astro/src/vite-plugin-astro-server/response.ts
Normal file
106
packages/astro/src/vite-plugin-astro-server/response.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import type http from 'http';
|
||||||
|
import type { ModuleLoader } from '../core/module-loader/index';
|
||||||
|
import type { ErrorWithMetadata } from '../core/errors/index.js';
|
||||||
|
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import { getSetCookiesFromResponse } from '../core/cookies/index.js';
|
||||||
|
import { getViteErrorPayload } from '../core/errors/dev/index.js';
|
||||||
|
import notFoundTemplate from '../template/4xx.js';
|
||||||
|
|
||||||
|
|
||||||
|
export async function handle404Response(
|
||||||
|
origin: string,
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse
|
||||||
|
) {
|
||||||
|
const pathname = decodeURI(new URL(origin + req.url).pathname);
|
||||||
|
|
||||||
|
const html = notFoundTemplate({
|
||||||
|
statusCode: 404,
|
||||||
|
title: 'Not found',
|
||||||
|
tabTitle: '404: Not Found',
|
||||||
|
pathname,
|
||||||
|
});
|
||||||
|
writeHtmlResponse(res, 404, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handle500Response(
|
||||||
|
loader: ModuleLoader,
|
||||||
|
res: http.ServerResponse,
|
||||||
|
err: ErrorWithMetadata
|
||||||
|
) {
|
||||||
|
res.on('close', () => setTimeout(() => loader.webSocketSend(getViteErrorPayload(err)), 200));
|
||||||
|
if (res.headersSent) {
|
||||||
|
res.write(`<script type="module" src="/@vite/client"></script>`);
|
||||||
|
res.end();
|
||||||
|
} else {
|
||||||
|
writeHtmlResponse(
|
||||||
|
res,
|
||||||
|
500,
|
||||||
|
`<title>${err.name}</title><script type="module" src="/@vite/client"></script>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string) {
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Content-Length': Buffer.byteLength(html, 'utf-8'),
|
||||||
|
});
|
||||||
|
res.write(html);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeWebResponse(res: http.ServerResponse, webResponse: Response) {
|
||||||
|
const { status, headers, body } = webResponse;
|
||||||
|
|
||||||
|
let _headers = {};
|
||||||
|
if ('raw' in headers) {
|
||||||
|
// Node fetch allows you to get the raw headers, which includes multiples of the same type.
|
||||||
|
// This is needed because Set-Cookie *must* be called for each cookie, and can't be
|
||||||
|
// concatenated together.
|
||||||
|
type HeadersWithRaw = Headers & {
|
||||||
|
raw: () => Record<string, string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries((headers as HeadersWithRaw).raw())) {
|
||||||
|
res.setHeader(key, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_headers = Object.fromEntries(headers.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach any set-cookie headers added via Astro.cookies.set()
|
||||||
|
const setCookieHeaders = Array.from(getSetCookiesFromResponse(webResponse));
|
||||||
|
if (setCookieHeaders.length) {
|
||||||
|
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||||
|
}
|
||||||
|
res.writeHead(status, _headers);
|
||||||
|
if (body) {
|
||||||
|
if (Symbol.for('astro.responseBody') in webResponse) {
|
||||||
|
let stream = (webResponse as any)[Symbol.for('astro.responseBody')];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
res.write(chunk.toString());
|
||||||
|
}
|
||||||
|
} else if (body instanceof Readable) {
|
||||||
|
body.pipe(res);
|
||||||
|
return;
|
||||||
|
} else if (typeof body === 'string') {
|
||||||
|
res.write(body);
|
||||||
|
} else {
|
||||||
|
const reader = body.getReader();
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
if (value) {
|
||||||
|
res.write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeSSRResult(webResponse: Response, res: http.ServerResponse) {
|
||||||
|
return writeWebResponse(res, webResponse);
|
||||||
|
}
|
185
packages/astro/src/vite-plugin-astro-server/route.ts
Normal file
185
packages/astro/src/vite-plugin-astro-server/route.ts
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
import type http from 'http';
|
||||||
|
import mime from 'mime';
|
||||||
|
import type { AstroConfig, AstroSettings, ManifestData } from '../@types/astro';
|
||||||
|
import { DevelopmentEnvironment, SSROptions } from '../core/render/dev/index';
|
||||||
|
|
||||||
|
import { attachToResponse } from '../core/cookies/index.js';
|
||||||
|
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
|
||||||
|
import { warn } from '../core/logger/core.js';
|
||||||
|
import { appendForwardSlash } from '../core/path.js';
|
||||||
|
import { preload, renderPage } from '../core/render/dev/index.js';
|
||||||
|
import { getParamsAndProps, GetParamsAndPropsError } from '../core/render/index.js';
|
||||||
|
import { createRequest } from '../core/request.js';
|
||||||
|
import { matchAllRoutes } from '../core/routing/index.js';
|
||||||
|
import { resolvePages } from '../core/util.js';
|
||||||
|
import { log404 } from './common.js';
|
||||||
|
import { handle404Response, writeWebResponse, writeSSRResult } from './response.js';
|
||||||
|
import { throwIfRedirectNotAllowed } from '../core/endpoint/index.js';
|
||||||
|
|
||||||
|
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
|
||||||
|
...args: any
|
||||||
|
) => Promise<infer R>
|
||||||
|
? R
|
||||||
|
: any;
|
||||||
|
|
||||||
|
function getCustom404Route({ config }: AstroSettings, manifest: ManifestData) {
|
||||||
|
// For Windows compat, use relative page paths to match the 404 route
|
||||||
|
const relPages = resolvePages(config).href.replace(config.root.href, '');
|
||||||
|
const pattern = new RegExp(`${appendForwardSlash(relPages)}404.(astro|md)`);
|
||||||
|
return manifest.routes.find((r) => r.component.match(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function matchRoute(pathname: string, env: DevelopmentEnvironment, manifest: ManifestData) {
|
||||||
|
const { logging, settings, routeCache } = env;
|
||||||
|
const matches = matchAllRoutes(pathname, manifest);
|
||||||
|
|
||||||
|
for await (const maybeRoute of matches) {
|
||||||
|
const filePath = new URL(`./${maybeRoute.component}`, settings.config.root);
|
||||||
|
const preloadedComponent = await preload({ env, filePath });
|
||||||
|
const [, mod] = preloadedComponent;
|
||||||
|
// attempt to get static paths
|
||||||
|
// if this fails, we have a bad URL match!
|
||||||
|
const paramsAndPropsRes = await getParamsAndProps({
|
||||||
|
mod,
|
||||||
|
route: maybeRoute,
|
||||||
|
routeCache,
|
||||||
|
pathname: pathname,
|
||||||
|
logging,
|
||||||
|
ssr: settings.config.output === 'server',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paramsAndPropsRes !== GetParamsAndPropsError.NoMatchingStaticPath) {
|
||||||
|
return {
|
||||||
|
route: maybeRoute,
|
||||||
|
filePath,
|
||||||
|
preloadedComponent,
|
||||||
|
mod,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length) {
|
||||||
|
warn(
|
||||||
|
logging,
|
||||||
|
'getStaticPaths',
|
||||||
|
`Route pattern matched, but no matching static path found. (${pathname})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log404(logging, pathname);
|
||||||
|
const custom404 = getCustom404Route(settings, manifest);
|
||||||
|
|
||||||
|
if (custom404) {
|
||||||
|
const filePath = new URL(`./${custom404.component}`, settings.config.root);
|
||||||
|
const preloadedComponent = await preload({ env, filePath });
|
||||||
|
const [, mod] = preloadedComponent;
|
||||||
|
|
||||||
|
return {
|
||||||
|
route: custom404,
|
||||||
|
filePath,
|
||||||
|
preloadedComponent,
|
||||||
|
mod,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleRoute(
|
||||||
|
matchedRoute: AsyncReturnType<typeof matchRoute>,
|
||||||
|
url: URL,
|
||||||
|
pathname: string,
|
||||||
|
body: ArrayBuffer | undefined,
|
||||||
|
origin: string,
|
||||||
|
env: DevelopmentEnvironment,
|
||||||
|
manifest: ManifestData,
|
||||||
|
req: http.IncomingMessage,
|
||||||
|
res: http.ServerResponse
|
||||||
|
): Promise<void> {
|
||||||
|
const { logging, settings } = env;
|
||||||
|
if (!matchedRoute) {
|
||||||
|
return handle404Response(origin, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config } = settings;
|
||||||
|
const filePath: URL | undefined = matchedRoute.filePath;
|
||||||
|
const { route, preloadedComponent, mod } = matchedRoute;
|
||||||
|
const buildingToSSR = config.output === 'server';
|
||||||
|
|
||||||
|
// Headers are only available when using SSR.
|
||||||
|
const request = createRequest({
|
||||||
|
url,
|
||||||
|
headers: buildingToSSR ? req.headers : new Headers(),
|
||||||
|
method: req.method,
|
||||||
|
body,
|
||||||
|
logging,
|
||||||
|
ssr: buildingToSSR,
|
||||||
|
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// attempt to get static paths
|
||||||
|
// if this fails, we have a bad URL match!
|
||||||
|
const paramsAndPropsRes = await getParamsAndProps({
|
||||||
|
mod,
|
||||||
|
route,
|
||||||
|
routeCache: env.routeCache,
|
||||||
|
pathname: pathname,
|
||||||
|
logging,
|
||||||
|
ssr: config.output === 'server',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options: SSROptions = {
|
||||||
|
env,
|
||||||
|
filePath,
|
||||||
|
origin,
|
||||||
|
preload: preloadedComponent,
|
||||||
|
pathname,
|
||||||
|
request,
|
||||||
|
route,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Route successfully matched! Render it.
|
||||||
|
if (route.type === 'endpoint') {
|
||||||
|
const result = await callEndpoint(options);
|
||||||
|
if (result.type === 'response') {
|
||||||
|
if (result.response.headers.get('X-Astro-Response') === 'Not-Found') {
|
||||||
|
const fourOhFourRoute = await matchRoute('/404', env, manifest);
|
||||||
|
return handleRoute(
|
||||||
|
fourOhFourRoute,
|
||||||
|
new URL('/404', url),
|
||||||
|
'/404',
|
||||||
|
body,
|
||||||
|
origin,
|
||||||
|
env,
|
||||||
|
manifest,
|
||||||
|
req,
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throwIfRedirectNotAllowed(result.response, config);
|
||||||
|
await writeWebResponse(res, result.response);
|
||||||
|
} else {
|
||||||
|
let contentType = 'text/plain';
|
||||||
|
// Dynamic routes don’t include `route.pathname`, so synthesize a path for these (e.g. 'src/pages/[slug].svg')
|
||||||
|
const filepath =
|
||||||
|
route.pathname ||
|
||||||
|
route.segments.map((segment) => segment.map((p) => p.content).join('')).join('/');
|
||||||
|
const computedMimeType = mime.getType(filepath);
|
||||||
|
if (computedMimeType) {
|
||||||
|
contentType = computedMimeType;
|
||||||
|
}
|
||||||
|
const response = new Response(result.body, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `${contentType};charset=utf-8`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
attachToResponse(response, result.cookies);
|
||||||
|
await writeWebResponse(res, response);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await renderPage(options);
|
||||||
|
throwIfRedirectNotAllowed(result, config);
|
||||||
|
return await writeSSRResult(result, res);
|
||||||
|
}
|
||||||
|
}
|
52
packages/astro/src/vite-plugin-astro-server/server-state.ts
Normal file
52
packages/astro/src/vite-plugin-astro-server/server-state.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
export type ErrorState = 'fresh' | 'error';
|
||||||
|
|
||||||
|
export interface RouteState {
|
||||||
|
state: ErrorState;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerState {
|
||||||
|
routes: Map<string, RouteState>;
|
||||||
|
state: ErrorState;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServerState(): ServerState {
|
||||||
|
return {
|
||||||
|
routes: new Map(),
|
||||||
|
state: 'fresh'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasAnyFailureState(serverState: ServerState) {
|
||||||
|
return serverState.state !== 'fresh';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRouteError(serverState: ServerState, pathname: string, error: Error) {
|
||||||
|
if(serverState.routes.has(pathname)) {
|
||||||
|
const routeState = serverState.routes.get(pathname)!;
|
||||||
|
routeState.state = 'error';
|
||||||
|
routeState.error = error;
|
||||||
|
} else {
|
||||||
|
const routeState: RouteState = {
|
||||||
|
state: 'error',
|
||||||
|
error: error
|
||||||
|
};
|
||||||
|
serverState.routes.set(pathname, routeState);
|
||||||
|
}
|
||||||
|
serverState.state = 'error';
|
||||||
|
serverState.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setServerError(serverState: ServerState, error: Error) {
|
||||||
|
serverState.state = 'error';
|
||||||
|
serverState.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRouteError(serverState: ServerState, pathname: string) {
|
||||||
|
if(serverState.routes.has(pathname)) {
|
||||||
|
serverState.routes.delete(pathname);
|
||||||
|
}
|
||||||
|
serverState.state = 'fresh';
|
||||||
|
serverState.error = undefined;
|
||||||
|
}
|
38
packages/astro/src/vite-plugin-load-fallback/index.ts
Normal file
38
packages/astro/src/vite-plugin-load-fallback/index.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import type * as vite from 'vite';
|
||||||
|
import nodeFs from 'fs';
|
||||||
|
|
||||||
|
type NodeFileSystemModule = typeof nodeFs;
|
||||||
|
|
||||||
|
export interface LoadFallbackPluginParams {
|
||||||
|
fs?: NodeFileSystemModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function loadFallbackPlugin({ fs }: LoadFallbackPluginParams): vite.Plugin | false {
|
||||||
|
// Only add this plugin if a custom fs implementation is provided.
|
||||||
|
if(!fs || fs === nodeFs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'astro:load-fallback',
|
||||||
|
enforce: 'post',
|
||||||
|
async load(id) {
|
||||||
|
try {
|
||||||
|
// await is necessary for the catch
|
||||||
|
return await fs.promises.readFile(cleanUrl(id), 'utf-8')
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
return await fs.promises.readFile(id, 'utf-8');
|
||||||
|
} catch(e2) {
|
||||||
|
// Let fall through to the next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryRE = /\?.*$/s;
|
||||||
|
const hashRE = /#.*$/s;
|
||||||
|
|
||||||
|
const cleanUrl = (url: string): string =>
|
||||||
|
url.replace(hashRE, '').replace(queryRE, '');
|
|
@ -19,7 +19,7 @@ polyfill(globalThis, {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('node-fetch').Response} Response
|
* @typedef {import('node-fetch').Response} Response
|
||||||
* @typedef {import('../src/core/dev/index').DedvServer} DevServer
|
* @typedef {import('../src/core/dev/dev').DedvServer} DevServer
|
||||||
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
|
||||||
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
|
||||||
* @typedef {import('../src/core/app/index').App} App
|
* @typedef {import('../src/core/app/index').App} App
|
||||||
|
|
70
packages/astro/test/units/correct-path.js
Normal file
70
packages/astro/test/units/correct-path.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* correctPath.js <https://github.com/streamich/fs-monkey/blob/af36a890d8070b25b9eae7178824f653bad5621f/src/correctPath.js>
|
||||||
|
* Taken from:
|
||||||
|
* https://github.com/streamich/fs-monkeys
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isWin = process.platform === 'win32';
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* removeTrailingSeparator <https://github.com/darsain/remove-trailing-separator>
|
||||||
|
*
|
||||||
|
* Inlined from:
|
||||||
|
* Copyright (c) darsain.
|
||||||
|
* Released under the ISC License.
|
||||||
|
*/
|
||||||
|
function removeTrailingSeparator(str) {
|
||||||
|
let i = str.length - 1;
|
||||||
|
if (i < 2) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
while (isSeparator(str, i)) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
return str.substr(0, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSeparator(str, i) {
|
||||||
|
let char = str[i];
|
||||||
|
return i > 0 && (char === '/' || (isWin && char === '\\'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* normalize-path <https://github.com/jonschlinkert/normalize-path>
|
||||||
|
*
|
||||||
|
* Inlined from:
|
||||||
|
* Copyright (c) 2014-2017, Jon Schlinkert.
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
function normalizePath(str, stripTrailing) {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
throw new TypeError('expected a string');
|
||||||
|
}
|
||||||
|
str = str.replace(/[\\\/]+/g, '/');
|
||||||
|
if (stripTrailing !== false) {
|
||||||
|
str = removeTrailingSeparator(str);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* unixify <https://github.com/jonschlinkert/unixify>
|
||||||
|
*
|
||||||
|
* Inlined from:
|
||||||
|
* Copyright (c) 2014, 2017, Jon Schlinkert.
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
export function unixify(filepath, stripTrailing = true) {
|
||||||
|
if(isWin) {
|
||||||
|
filepath = normalizePath(filepath, stripTrailing);
|
||||||
|
return filepath.replace(/^([a-zA-Z]+:|\.\/)/, '');
|
||||||
|
}
|
||||||
|
return filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Corrects a windows path to unix format (including \\?\c:...)
|
||||||
|
*/
|
||||||
|
export function correctPath(filepath) {
|
||||||
|
return unixify(filepath.replace(/^\\\\\?\\.:\\/,'\\'));
|
||||||
|
}
|
38
packages/astro/test/units/dev/dev.test.js
Normal file
38
packages/astro/test/units/dev/dev.test.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
|
import { runInContainer } from '../../../dist/core/dev/index.js';
|
||||||
|
import { createFs, createRequestAndResponse } from '../test-utils.js';
|
||||||
|
|
||||||
|
const root = new URL('../../fixtures/alias/', import.meta.url);
|
||||||
|
|
||||||
|
describe('dev container', () => {
|
||||||
|
it('can render requests', async () => {
|
||||||
|
|
||||||
|
const fs = createFs({
|
||||||
|
'/src/pages/index.astro': `
|
||||||
|
---
|
||||||
|
const name = 'Testing';
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head><title>{name}</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>{name}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}, root);
|
||||||
|
|
||||||
|
await runInContainer({ fs, root }, async container => {
|
||||||
|
const { req, res, text } = createRequestAndResponse({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/'
|
||||||
|
});
|
||||||
|
container.handle(req, res);
|
||||||
|
const html = await text();
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
expect(res.statusCode).to.equal(200);
|
||||||
|
expect($('h1')).to.have.a.lengthOf(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
87
packages/astro/test/units/test-utils.js
Normal file
87
packages/astro/test/units/test-utils.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import httpMocks from 'node-mocks-http';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { Volume } from 'memfs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import npath from 'path';
|
||||||
|
import { unixify } from './correct-path.js';
|
||||||
|
|
||||||
|
class MyVolume extends Volume {
|
||||||
|
existsSync(p) {
|
||||||
|
if(p instanceof URL) {
|
||||||
|
p = fileURLToPath(p);
|
||||||
|
}
|
||||||
|
return super.existsSync(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFs(json, root) {
|
||||||
|
if(typeof root !== 'string') {
|
||||||
|
root = unixify(fileURLToPath(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
const structure = {};
|
||||||
|
for(const [key, value] of Object.entries(json)) {
|
||||||
|
const fullpath = npath.posix.join(root, key);
|
||||||
|
structure[fullpath] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = new MyVolume();
|
||||||
|
fs.fromJSON(structure);
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRequestAndResponse(reqOptions = {}) {
|
||||||
|
const req = httpMocks.createRequest(reqOptions);
|
||||||
|
|
||||||
|
const res = httpMocks.createResponse({
|
||||||
|
eventEmitter: EventEmitter,
|
||||||
|
req,
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the response is complete.
|
||||||
|
const done = toPromise(res);
|
||||||
|
|
||||||
|
// Get the response as text
|
||||||
|
const text = async () => {
|
||||||
|
let chunks = await done;
|
||||||
|
return buffersToString(chunks);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the response as json
|
||||||
|
const json = async () => {
|
||||||
|
const raw = await text();
|
||||||
|
return JSON.parse(raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { req, res, done, json, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPromise(res) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// node-mocks-http doesn't correctly handle non-Buffer typed arrays,
|
||||||
|
// so override the write method to fix it.
|
||||||
|
const write = res.write;
|
||||||
|
res.write = function(data, encoding) {
|
||||||
|
if(ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) {
|
||||||
|
data = Buffer.from(data);
|
||||||
|
}
|
||||||
|
return write.call(this, data, encoding);
|
||||||
|
};
|
||||||
|
res.on('end', () => {
|
||||||
|
let chunks = res._getChunks();
|
||||||
|
resolve(chunks);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buffersToString(buffers) {
|
||||||
|
let decoder = new TextDecoder();
|
||||||
|
let str = '';
|
||||||
|
for(const buffer of buffers) {
|
||||||
|
str += decoder.decode(buffer);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A convenience method for creating an astro module from a component
|
||||||
|
export const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { createLoader } from '../../../dist/core/module-loader/index.js';
|
||||||
|
import { createController, runWithErrorHandling } from '../../../dist/vite-plugin-astro-server/index.js';
|
||||||
|
|
||||||
|
describe('vite-plugin-astro-server', () => {
|
||||||
|
describe('controller', () => {
|
||||||
|
it('calls the onError method when an error occurs in the handler', async () => {
|
||||||
|
const controller = createController({ loader: createLoader() });
|
||||||
|
let error = undefined;
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname: '/',
|
||||||
|
run() {
|
||||||
|
throw new Error('oh no');
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(error).to.not.be.an('undefined');
|
||||||
|
expect(error).to.be.an.instanceOf(Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the state to error when an error occurs in the handler', async () => {
|
||||||
|
const controller = createController({ loader: createLoader() });
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname: '/',
|
||||||
|
run() {
|
||||||
|
throw new Error('oh no');
|
||||||
|
},
|
||||||
|
onError(){}
|
||||||
|
});
|
||||||
|
expect(controller.state.state).to.equal('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls reload when a file change occurs when in an error state', async () => {
|
||||||
|
let reloads = 0;
|
||||||
|
const loader = createLoader({
|
||||||
|
eachModule() {},
|
||||||
|
clientReload() {
|
||||||
|
reloads++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const controller = createController({ loader });
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
expect(reloads).to.equal(0);
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname: '/',
|
||||||
|
run() {
|
||||||
|
throw new Error('oh no');
|
||||||
|
},
|
||||||
|
onError(){}
|
||||||
|
});
|
||||||
|
expect(reloads).to.equal(0);
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
expect(reloads).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call reload on file change if not in an error state', async () => {
|
||||||
|
let reloads = 0;
|
||||||
|
const loader = createLoader({
|
||||||
|
eachModule() {},
|
||||||
|
clientReload() {
|
||||||
|
reloads++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const controller = createController({ loader });
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
expect(reloads).to.equal(0);
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname: '/',
|
||||||
|
run() {
|
||||||
|
throw new Error('oh no');
|
||||||
|
},
|
||||||
|
onError(){}
|
||||||
|
});
|
||||||
|
expect(reloads).to.equal(0);
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
expect(reloads).to.equal(1);
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
expect(reloads).to.equal(2);
|
||||||
|
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname: '/',
|
||||||
|
// No error here
|
||||||
|
run() {}
|
||||||
|
});
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
expect(reloads).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Invalidates broken modules when a change occurs in an error state', async () => {
|
||||||
|
const mods = [
|
||||||
|
{ id: 'one', ssrError: new Error('one') },
|
||||||
|
{ id: 'two', ssrError: null },
|
||||||
|
{ id: 'three', ssrError: new Error('three') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const loader = createLoader({
|
||||||
|
eachModule(cb) {
|
||||||
|
return mods.forEach(cb);
|
||||||
|
},
|
||||||
|
invalidateModule(mod) {
|
||||||
|
mod.ssrError = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const controller = createController({ loader });
|
||||||
|
|
||||||
|
await runWithErrorHandling({
|
||||||
|
controller,
|
||||||
|
pathname: '/',
|
||||||
|
run() {
|
||||||
|
throw new Error('oh no');
|
||||||
|
},
|
||||||
|
onError(){}
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.events.emit('file-change');
|
||||||
|
|
||||||
|
expect(mods).to.deep.equal([
|
||||||
|
{ id: 'one', ssrError: null },
|
||||||
|
{ id: 'two', ssrError: null },
|
||||||
|
{ id: 'three', ssrError: null },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
import { createLoader } from '../../../dist/core/module-loader/index.js';
|
||||||
|
import { createController, handleRequest } from '../../../dist/vite-plugin-astro-server/index.js';
|
||||||
|
import { createDefaultDevSettings } from '../../../dist/core/config/index.js';
|
||||||
|
import { createBasicEnvironment } from '../../../dist/core/render/index.js';
|
||||||
|
import { createRouteManifest } from '../../../dist/core/routing/index.js';
|
||||||
|
import { defaultLogging as logging } from '../../test-utils.js';
|
||||||
|
import { createComponent, render } from '../../../dist/runtime/server/index.js';
|
||||||
|
import { createRequestAndResponse, createFs, createAstroModule } from '../test-utils.js';
|
||||||
|
|
||||||
|
async function createDevEnvironment(overrides = {}) {
|
||||||
|
const env = createBasicEnvironment({
|
||||||
|
logging,
|
||||||
|
renderers: []
|
||||||
|
});
|
||||||
|
env.settings = await createDefaultDevSettings({}, '/');
|
||||||
|
env.settings.renderers = [];
|
||||||
|
env.loader = createLoader();
|
||||||
|
Object.assign(env, overrides);
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('vite-plugin-astro-server', () => {
|
||||||
|
describe('request', () => {
|
||||||
|
it('renders a request', async () => {
|
||||||
|
const env = await createDevEnvironment({
|
||||||
|
loader: createLoader({
|
||||||
|
import(id) {
|
||||||
|
const Page = createComponent(() => {
|
||||||
|
return render`<div id="test">testing</div>`;
|
||||||
|
});
|
||||||
|
return createAstroModule(Page);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const controller = createController({ loader: env.loader });
|
||||||
|
const { req, res, text } = createRequestAndResponse();
|
||||||
|
const fs = createFs({
|
||||||
|
// Note that the content doesn't matter here because we are using a custom loader.
|
||||||
|
'/src/pages/index.astro': ''
|
||||||
|
}, '/');
|
||||||
|
const manifest = createRouteManifest({
|
||||||
|
fsMod: fs,
|
||||||
|
settings: env.settings
|
||||||
|
}, logging);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleRequest(
|
||||||
|
env,
|
||||||
|
manifest,
|
||||||
|
controller,
|
||||||
|
req,
|
||||||
|
res
|
||||||
|
);
|
||||||
|
const html = await text();
|
||||||
|
expect(html).to.include('<div id="test">');
|
||||||
|
} catch(err) {
|
||||||
|
expect(err).to.be.undefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -422,9 +422,11 @@ importers:
|
||||||
import-meta-resolve: ^2.1.0
|
import-meta-resolve: ^2.1.0
|
||||||
kleur: ^4.1.4
|
kleur: ^4.1.4
|
||||||
magic-string: ^0.25.9
|
magic-string: ^0.25.9
|
||||||
|
memfs: ^3.4.7
|
||||||
mime: ^3.0.0
|
mime: ^3.0.0
|
||||||
mocha: ^9.2.2
|
mocha: ^9.2.2
|
||||||
node-fetch: ^3.2.5
|
node-fetch: ^3.2.5
|
||||||
|
node-mocks-http: ^1.11.0
|
||||||
ora: ^6.1.0
|
ora: ^6.1.0
|
||||||
path-browserify: ^1.0.1
|
path-browserify: ^1.0.1
|
||||||
path-to-regexp: ^6.2.1
|
path-to-regexp: ^6.2.1
|
||||||
|
@ -547,8 +549,10 @@ importers:
|
||||||
astro-scripts: link:../../scripts
|
astro-scripts: link:../../scripts
|
||||||
chai: 4.3.6
|
chai: 4.3.6
|
||||||
cheerio: 1.0.0-rc.12
|
cheerio: 1.0.0-rc.12
|
||||||
|
memfs: 3.4.7
|
||||||
mocha: 9.2.2
|
mocha: 9.2.2
|
||||||
node-fetch: 3.2.10
|
node-fetch: 3.2.10
|
||||||
|
node-mocks-http: 1.11.0
|
||||||
rehype-autolink-headings: 6.1.1
|
rehype-autolink-headings: 6.1.1
|
||||||
rehype-slug: 5.1.0
|
rehype-slug: 5.1.0
|
||||||
rehype-toc: 3.0.2
|
rehype-toc: 3.0.2
|
||||||
|
@ -12761,6 +12765,10 @@ packages:
|
||||||
minipass: 3.3.4
|
minipass: 3.3.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/fs-monkey/1.0.3:
|
||||||
|
resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fs.realpath/1.0.0:
|
/fs.realpath/1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
|
@ -14270,6 +14278,13 @@ packages:
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/memfs/3.4.7:
|
||||||
|
resolution: {integrity: sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
dependencies:
|
||||||
|
fs-monkey: 1.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/meow/6.1.1:
|
/meow/6.1.1:
|
||||||
resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==}
|
resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
Loading…
Reference in a new issue