Simplify HMR handling (#5811)

* Simplify HMR handling

* Try skip test to reveal other test result

* Support virtual files

* Fix head injection

* Revert CI changes

* Bring back normalizeFilename

* Refactor

* Add changeset
This commit is contained in:
Bjorn Lu 2023-01-12 02:51:05 +08:00 committed by GitHub
parent 52209ca2ad
commit ec09bb6642
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 46 additions and 108 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Simplify HMR handling

View file

@ -12,7 +12,6 @@ export interface CompileProps {
astroConfig: AstroConfig; astroConfig: AstroConfig;
viteConfig: ResolvedConfig; viteConfig: ResolvedConfig;
filename: string; filename: string;
id: string | undefined;
source: string; source: string;
} }
@ -25,7 +24,6 @@ export async function compile({
astroConfig, astroConfig,
viteConfig, viteConfig,
filename, filename,
id: moduleId,
source, source,
}: CompileProps): Promise<CompileResult> { }: CompileProps): Promise<CompileResult> {
const cssDeps = new Set<string>(); const cssDeps = new Set<string>();
@ -37,7 +35,7 @@ export async function compile({
// use `sourcemap: "both"` so that sourcemap is included in the code // use `sourcemap: "both"` so that sourcemap is included in the code
// result passed to esbuild, but also available in the catch handler. // result passed to esbuild, but also available in the catch handler.
transformResult = await transform(source, { transformResult = await transform(source, {
moduleId, moduleId: filename,
pathname: filename, pathname: filename,
projectRoot: astroConfig.root.toString(), projectRoot: astroConfig.root.toString(),
site: astroConfig.site?.toString(), site: astroConfig.site?.toString(),

View file

@ -102,7 +102,7 @@ export async function createVite(
astroIntegrationsContainerPlugin({ settings, logging }), astroIntegrationsContainerPlugin({ settings, logging }),
astroScriptsPageSSRPlugin({ settings }), astroScriptsPageSSRPlugin({ settings }),
astroHeadPropagationPlugin({ settings }), astroHeadPropagationPlugin({ settings }),
astroScannerPlugin({ settings, logging }), astroScannerPlugin({ settings }),
astroInjectEnvTsPlugin({ settings, logging, fs }), astroInjectEnvTsPlugin({ settings, logging, fs }),
astroContentVirtualModPlugin({ settings }), astroContentVirtualModPlugin({ settings }),
astroContentServerPlugin({ fs, settings, logging, mode }), astroContentServerPlugin({ fs, settings, logging, mode }),

View file

@ -7,7 +7,6 @@ import { getFileInfo } from '../vite-plugin-utils/index.js';
interface CachedFullCompilation { interface CachedFullCompilation {
compileProps: CompileProps; compileProps: CompileProps;
rawId: string;
logging: LogOptions; logging: LogOptions;
} }
@ -27,7 +26,6 @@ const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms;
export async function cachedFullCompilation({ export async function cachedFullCompilation({
compileProps, compileProps,
rawId,
logging, logging,
}: CachedFullCompilation): Promise<FullCompileResult> { }: CachedFullCompilation): Promise<FullCompileResult> {
let transformResult: CompileResult; let transformResult: CompileResult;
@ -37,7 +35,7 @@ export async function cachedFullCompilation({
transformResult = await cachedCompilation(compileProps); transformResult = await cachedCompilation(compileProps);
// Compile all TypeScript to JavaScript. // Compile all TypeScript to JavaScript.
// Also, catches invalid JS/TS in the compiled output before returning. // Also, catches invalid JS/TS in the compiled output before returning.
esbuildResult = await transformWithEsbuild(transformResult.code, rawId, { esbuildResult = await transformWithEsbuild(transformResult.code, compileProps.filename, {
loader: 'ts', loader: 'ts',
target: 'esnext', target: 'esnext',
sourcemap: 'external', sourcemap: 'external',
@ -51,7 +49,7 @@ export async function cachedFullCompilation({
} catch (err: any) { } catch (err: any) {
await enhanceCompileError({ await enhanceCompileError({
err, err,
id: rawId, id: compileProps.filename,
source: compileProps.source, source: compileProps.source,
config: compileProps.astroConfig, config: compileProps.astroConfig,
logging: logging, logging: logging,
@ -59,7 +57,10 @@ export async function cachedFullCompilation({
throw err; throw err;
} }
const { fileId: file, fileUrl: url } = getFileInfo(rawId, compileProps.astroConfig); const { fileId: file, fileUrl: url } = getFileInfo(
compileProps.filename,
compileProps.astroConfig
);
let SUFFIX = ''; let SUFFIX = '';
SUFFIX += `\nconst $$file = ${JSON.stringify(file)};\nconst $$url = ${JSON.stringify( SUFFIX += `\nconst $$file = ${JSON.stringify(file)};\nconst $$url = ${JSON.stringify(
@ -70,7 +71,7 @@ export async function cachedFullCompilation({
if (!compileProps.viteConfig.isProduction) { if (!compileProps.viteConfig.isProduction) {
let i = 0; let i = 0;
while (i < transformResult.scripts.length) { while (i < transformResult.scripts.length) {
SUFFIX += `import "${rawId}?astro&type=script&index=${i}&lang.ts";`; SUFFIX += `import "${compileProps.filename}?astro&type=script&index=${i}&lang.ts";`;
i++; i++;
} }
} }

View file

@ -4,20 +4,13 @@ import type { AstroSettings } from '../@types/astro';
import type { LogOptions } from '../core/logger/core.js'; import type { LogOptions } from '../core/logger/core.js';
import type { PluginMetadata as AstroPluginMetadata } from './types'; import type { PluginMetadata as AstroPluginMetadata } from './types';
import slash from 'slash'; import { normalizePath } from 'vite';
import { fileURLToPath } from 'url';
import { cachedCompilation, CompileProps, getCachedCompileResult } from '../core/compile/index.js'; import { cachedCompilation, CompileProps, getCachedCompileResult } from '../core/compile/index.js';
import { import { isRelativePath } from '../core/path.js';
isRelativePath,
prependForwardSlash,
removeLeadingForwardSlashWindows,
startsWithForwardSlash,
} from '../core/path.js';
import { viteID } from '../core/util.js';
import { normalizeFilename } from '../vite-plugin-utils/index.js';
import { cachedFullCompilation } from './compile.js'; import { cachedFullCompilation } from './compile.js';
import { handleHotUpdate } from './hmr.js'; import { handleHotUpdate } from './hmr.js';
import { parseAstroRequest, ParsedRequestResult } from './query.js'; import { parseAstroRequest } from './query.js';
import { normalizeFilename } from '../vite-plugin-utils/index.js';
export { getAstroMetadata } from './metadata.js'; export { getAstroMetadata } from './metadata.js';
export type { AstroPluginMetadata }; export type { AstroPluginMetadata };
@ -27,77 +20,20 @@ interface AstroPluginOptions {
} }
/** Transform .astro files for Vite */ /** Transform .astro files for Vite */
export default function astro({ settings, logging }: AstroPluginOptions): vite.Plugin { export default function astro({ settings, logging }: AstroPluginOptions): vite.Plugin[] {
const { config } = settings; const { config } = settings;
let resolvedConfig: vite.ResolvedConfig; let resolvedConfig: vite.ResolvedConfig;
// Variables for determining if an id starts with /src... // Variables for determining if an id starts with /src...
const srcRootWeb = config.srcDir.pathname.slice(config.root.pathname.length - 1); const srcRootWeb = config.srcDir.pathname.slice(config.root.pathname.length - 1);
const isBrowserPath = (path: string) => path.startsWith(srcRootWeb) && srcRootWeb !== '/'; const isBrowserPath = (path: string) => path.startsWith(srcRootWeb) && srcRootWeb !== '/';
const isFullFilePath = (path: string) =>
path.startsWith(prependForwardSlash(slash(fileURLToPath(config.root))));
function relativeToRoot(pathname: string) { const prePlugin: vite.Plugin = {
const arg = startsWithForwardSlash(pathname) ? '.' + pathname : pathname;
const url = new URL(arg, config.root);
return slash(fileURLToPath(url)) + url.search;
}
function resolveRelativeFromAstroParent(id: string, parsedFrom: ParsedRequestResult): string {
const filename = normalizeFilename(parsedFrom.filename, config);
const resolvedURL = new URL(id, `file://${filename}`);
const resolved = resolvedURL.pathname;
if (isBrowserPath(resolved)) {
return relativeToRoot(resolved + resolvedURL.search);
}
return slash(fileURLToPath(resolvedURL)) + resolvedURL.search;
}
return {
name: 'astro:build', name: 'astro:build',
enforce: 'pre', // run transforms before other plugins can enforce: 'pre', // run transforms before other plugins can
configResolved(_resolvedConfig) { configResolved(_resolvedConfig) {
resolvedConfig = _resolvedConfig; resolvedConfig = _resolvedConfig;
}, },
// note: dont claim .astro files with resolveId() — it prevents Vite from transpiling the final JS (import.meta.glob, etc.)
async resolveId(id, from, opts) {
// If resolving from an astro subresource such as a hoisted script,
// we need to resolve relative paths ourselves.
if (from) {
const parsedFrom = parseAstroRequest(from);
const isAstroScript = parsedFrom.query.astro && parsedFrom.query.type === 'script';
if (isAstroScript && isRelativePath(id)) {
return this.resolve(resolveRelativeFromAstroParent(id, parsedFrom), from, {
custom: opts.custom,
skipSelf: true,
});
}
}
// serve sub-part requests (*?astro) as virtual modules
const { query } = parseAstroRequest(id);
if (query.astro) {
// TODO: Try to remove these custom resolve so HMR is more predictable.
// Convert /src/pages/index.astro?astro&type=style to /Users/name/
// Because this needs to be the id for the Vite CSS plugin to property resolve
// relative @imports.
if (query.type === 'style' && isBrowserPath(id)) {
return relativeToRoot(id);
}
// Strip `/@fs` from linked dependencies outside of root so we can normalize
// it in the condition below. This ensures that the style module shared the same is
// part of the same "file" as the main Astro module in the module graph.
// "file" refers to `moduleGraph.fileToModulesMap`.
if (query.type === 'style' && id.startsWith('/@fs')) {
id = removeLeadingForwardSlashWindows(id.slice(4));
}
// Convert file paths to ViteID, meaning on Windows it omits the leading slash
if (isFullFilePath(id)) {
return viteID(new URL('file://' + id));
}
return id;
}
},
async load(id, opts) { async load(id, opts) {
const parsedId = parseAstroRequest(id); const parsedId = parseAstroRequest(id);
const query = parsedId.query; const query = parsedId.query;
@ -105,7 +41,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
return null; return null;
} }
// For CSS / hoisted scripts, the main Astro module should already be cached // For CSS / hoisted scripts, the main Astro module should already be cached
const filename = normalizeFilename(parsedId.filename, config); const filename = normalizePath(normalizeFilename(parsedId.filename, config.root));
const compileResult = getCachedCompileResult(config, filename); const compileResult = getCachedCompileResult(config, filename);
if (!compileResult) { if (!compileResult) {
return null; return null;
@ -152,7 +88,7 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
if (src.startsWith('/') && !isBrowserPath(src)) { if (src.startsWith('/') && !isBrowserPath(src)) {
const publicDir = config.publicDir.pathname.replace(/\/$/, '').split('/').pop() + '/'; const publicDir = config.publicDir.pathname.replace(/\/$/, '').split('/').pop() + '/';
throw new Error( throw new Error(
`\n\n<script src="${src}"> references an asset in the "${publicDir}" directory. Please add the "is:inline" directive to keep this asset from being bundled.\n\nFile: ${filename}` `\n\n<script src="${src}"> references an asset in the "${publicDir}" directory. Please add the "is:inline" directive to keep this asset from being bundled.\n\nFile: ${id}`
); );
} }
} }
@ -196,20 +132,14 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
return; return;
} }
const filename = normalizeFilename(parsedId.filename, config);
const compileProps: CompileProps = { const compileProps: CompileProps = {
astroConfig: config, astroConfig: config,
viteConfig: resolvedConfig, viteConfig: resolvedConfig,
filename, filename: normalizePath(parsedId.filename),
id,
source, source,
}; };
const transformResult = await cachedFullCompilation({ const transformResult = await cachedFullCompilation({ compileProps, logging });
compileProps,
rawId: id,
logging,
});
for (const dep of transformResult.cssDeps) { for (const dep of transformResult.cssDeps) {
this.addWatchFile(dep); this.addWatchFile(dep);
@ -242,7 +172,6 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
astroConfig: config, astroConfig: config,
viteConfig: resolvedConfig, viteConfig: resolvedConfig,
filename: context.file, filename: context.file,
id: context.modules[0]?.id ?? undefined,
source: await context.read(), source: await context.read(),
}; };
const compile = () => cachedCompilation(compileProps); const compile = () => cachedCompilation(compileProps);
@ -253,6 +182,19 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
}); });
}, },
}; };
const normalPlugin: vite.Plugin = {
name: 'astro:build:normal',
resolveId(id) {
// If Vite resolver can't resolve the Astro request, it's likely a virtual Astro file, fallback here instead
const parsedId = parseAstroRequest(id);
if (parsedId.query.astro) {
return id;
}
},
};
return [prePlugin, normalPlugin];
} }
function appendSourceMap(content: string, map?: string) { function appendSourceMap(content: string, map?: string) {

View file

@ -1,18 +1,10 @@
import { Plugin as VitePlugin } from 'vite'; import { normalizePath, Plugin as VitePlugin } from 'vite';
import { AstroSettings } from '../@types/astro.js'; import { AstroSettings } from '../@types/astro.js';
import type { LogOptions } from '../core/logger/core.js';
import { isEndpoint, isPage } from '../core/util.js'; import { isEndpoint, isPage } from '../core/util.js';
import { normalizeFilename } from '../vite-plugin-utils/index.js';
import { scan } from './scan.js'; import { scan } from './scan.js';
export default function astroScannerPlugin({ export default function astroScannerPlugin({ settings }: { settings: AstroSettings }): VitePlugin {
settings,
logging,
}: {
settings: AstroSettings;
logging: LogOptions;
}): VitePlugin {
return { return {
name: 'astro:scanner', name: 'astro:scanner',
enforce: 'post', enforce: 'post',
@ -20,7 +12,7 @@ export default function astroScannerPlugin({
async transform(this, code, id, options) { async transform(this, code, id, options) {
if (!options?.ssr) return; if (!options?.ssr) return;
const filename = normalizeFilename(id, settings.config); const filename = normalizePath(id);
let fileURL: URL; let fileURL: URL;
try { try {
fileURL = new URL(`file://${filename}`); fileURL = new URL(`file://${filename}`);

View file

@ -1,8 +1,7 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { Plugin as VitePlugin } from 'vite'; import { normalizePath, Plugin as VitePlugin } from 'vite';
import { AstroSettings } from '../@types/astro.js'; import { AstroSettings } from '../@types/astro.js';
import { isPage } from '../core/util.js'; import { isPage } from '../core/util.js';
import { normalizeFilename } from '../vite-plugin-utils/index.js';
import { PAGE_SSR_SCRIPT_ID } from './index.js'; import { PAGE_SSR_SCRIPT_ID } from './index.js';
export default function astroScriptsPostPlugin({ export default function astroScriptsPostPlugin({
@ -19,7 +18,7 @@ export default function astroScriptsPostPlugin({
const hasInjectedScript = settings.scripts.some((s) => s.stage === 'page-ssr'); const hasInjectedScript = settings.scripts.some((s) => s.stage === 'page-ssr');
if (!hasInjectedScript) return; if (!hasInjectedScript) return;
const filename = normalizeFilename(id, settings.config); const filename = normalizePath(id);
let fileURL: URL; let fileURL: URL;
try { try {
fileURL = new URL(`file://${filename}`); fileURL = new URL(`file://${filename}`);

View file

@ -1,3 +1,4 @@
import { fileURLToPath } from 'url';
import ancestor from 'common-ancestor-path'; import ancestor from 'common-ancestor-path';
import type { AstroConfig } from '../@types/astro'; import type { AstroConfig } from '../@types/astro';
import { import {
@ -44,11 +45,11 @@ export function getFileInfo(id: string, config: AstroConfig) {
* *
* as absolute file paths with forward slashes. * as absolute file paths with forward slashes.
*/ */
export function normalizeFilename(filename: string, config: AstroConfig) { export function normalizeFilename(filename: string, root: URL) {
if (filename.startsWith('/@fs')) { if (filename.startsWith('/@fs')) {
filename = filename.slice('/@fs'.length); filename = filename.slice('/@fs'.length);
} else if (filename.startsWith('/') && !ancestor(filename, config.root.pathname)) { } else if (filename.startsWith('/') && !ancestor(filename, fileURLToPath(root))) {
const url = new URL('.' + filename, config.root); const url = new URL('.' + filename, root);
filename = viteID(url); filename = viteID(url);
} }
return removeLeadingForwardSlashWindows(filename); return removeLeadingForwardSlashWindows(filename);