Refactor hydration path handling (#4918)
* Refactor hydration path handling * Remove old code * Fix jsx strip * Postprocess fix * Handle jsx to tsx stuff * Skip bigint tests * Fix deno * Try fix windows * Fix windows * Add more comments
This commit is contained in:
parent
61d26f3352
commit
a6bb2694b4
14 changed files with 137 additions and 44 deletions
5
.changeset/honest-cats-invent.md
Normal file
5
.changeset/honest-cats-invent.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Refactor hydration path handling
|
|
@ -3,7 +3,9 @@ import { useState } from 'react';
|
|||
|
||||
interface Props {
|
||||
obj: BigNestedObject;
|
||||
num: bigint;
|
||||
// TODO: support BigInt in `astro:postprocess`
|
||||
// num: bigint;
|
||||
num: number;
|
||||
arr: any[];
|
||||
map: Map<string, string>;
|
||||
set: Set<string>;
|
||||
|
|
|
@ -30,7 +30,8 @@ set.add('test2');
|
|||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<Component client:load obj={obj} num={11n} arr={[0, "foo"]} map={map} set={set} />
|
||||
<!-- TODO: support BigInt in `astro:postprocess` -->
|
||||
<Component client:load obj={obj} num={11} arr={[0, "foo"]} map={map} set={set} />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -32,7 +32,8 @@ test.describe('Passing JS into client components', () => {
|
|||
await expect(regExpValue).toHaveText('ok');
|
||||
});
|
||||
|
||||
test('BigInts', async ({ page }) => {
|
||||
// TODO: support BigInt in `astro:postprocess`
|
||||
test.skip('BigInts', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const bigIntType = await page.locator('#bigint-type');
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import path from 'path';
|
||||
import type { TransformResult } from '@astrojs/compiler';
|
||||
import type { AstroConfig } from '../../@types/astro';
|
||||
import type { TransformStyle } from './types';
|
||||
|
||||
import { transform } from '@astrojs/compiler';
|
||||
import { AstroErrorCodes } from '../errors.js';
|
||||
import { prependForwardSlash } from '../path.js';
|
||||
import { AggregateError, viteID } from '../util.js';
|
||||
import { prependForwardSlash, removeLeadingForwardSlashWindows } from '../path.js';
|
||||
import { AggregateError, resolveJsToTs, viteID } from '../util.js';
|
||||
import { createStylePreprocessor } from './style.js';
|
||||
|
||||
type CompilationCache = Map<string, CompileResult>;
|
||||
|
@ -19,7 +20,6 @@ const configCache = new WeakMap<AstroConfig, CompilationCache>();
|
|||
export interface CompileProps {
|
||||
config: AstroConfig;
|
||||
filename: string;
|
||||
moduleId: string;
|
||||
source: string;
|
||||
transformStyle: TransformStyle;
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ export interface CompileProps {
|
|||
async function compile({
|
||||
config,
|
||||
filename,
|
||||
moduleId,
|
||||
source,
|
||||
transformStyle,
|
||||
}: CompileProps): Promise<CompileResult> {
|
||||
|
@ -38,8 +37,13 @@ async function compile({
|
|||
// use `sourcemap: "both"` so that sourcemap is included in the code
|
||||
// result passed to esbuild, but also available in the catch handler.
|
||||
const transformResult = await transform(source, {
|
||||
// For Windows compat, prepend the module ID with `/@fs`
|
||||
pathname: `/@fs${prependForwardSlash(moduleId)}`,
|
||||
// For Windows compat, prepend filename with `/`.
|
||||
// Note this is required because the compiler uses URLs to parse and built paths.
|
||||
// TODO: Ideally the compiler could expose a `resolvePath` function so that we can
|
||||
// unify how we handle path in a bundler-agnostic way.
|
||||
// At the meantime workaround with a slash and remove them in `astro:postprocess`
|
||||
// when they are used in `client:component-path`.
|
||||
pathname: prependForwardSlash(filename),
|
||||
projectRoot: config.root.toString(),
|
||||
site: config.site?.toString(),
|
||||
sourcefile: filename,
|
||||
|
@ -84,6 +88,32 @@ async function compile({
|
|||
},
|
||||
});
|
||||
|
||||
// Also fix path before returning. Example original resolvedPaths:
|
||||
// - @astrojs/preact/client.js
|
||||
// - @/components/Foo.vue
|
||||
// - /Users/macos/project/src/Foo.vue
|
||||
// - /C:/Windows/project/src/Foo.vue
|
||||
for (const c of compileResult.clientOnlyComponents) {
|
||||
c.resolvedPath = removeLeadingForwardSlashWindows(c.resolvedPath);
|
||||
// The compiler trims .jsx by default, prevent this
|
||||
if (c.specifier.endsWith('.jsx') && !c.resolvedPath.endsWith('.jsx')) {
|
||||
c.resolvedPath += '.jsx';
|
||||
}
|
||||
if (path.isAbsolute(c.resolvedPath)) {
|
||||
c.resolvedPath = resolveJsToTs(c.resolvedPath);
|
||||
}
|
||||
}
|
||||
for (const c of compileResult.hydratedComponents) {
|
||||
c.resolvedPath = removeLeadingForwardSlashWindows(c.resolvedPath);
|
||||
// The compiler trims .jsx by default, prevent this
|
||||
if (c.specifier.endsWith('.jsx') && !c.resolvedPath.endsWith('.jsx')) {
|
||||
c.resolvedPath += '.jsx';
|
||||
}
|
||||
if (path.isAbsolute(c.resolvedPath)) {
|
||||
c.resolvedPath = resolveJsToTs(c.resolvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
return compileResult;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@ export function removeLeadingForwardSlash(path: string) {
|
|||
return path.startsWith('/') ? path.substring(1) : path;
|
||||
}
|
||||
|
||||
export function removeLeadingForwardSlashWindows(path: string) {
|
||||
return path.startsWith('/') && path[2] === ':' ? path.substring(1) : path;
|
||||
}
|
||||
|
||||
export function trimSlashes(path: string) {
|
||||
return path.replace(/^\/|\/$/g, '');
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import { render as coreRender } from '../core.js';
|
|||
import { RouteCache } from '../route-cache.js';
|
||||
import { collectMdMetadata } from '../util.js';
|
||||
import { getStylesForURL } from './css.js';
|
||||
import { resolveClientDevPath } from './resolve.js';
|
||||
import { getScriptsForURL } from './scripts.js';
|
||||
|
||||
export interface SSROptions {
|
||||
|
@ -180,12 +179,20 @@ export async function render(
|
|||
origin,
|
||||
pathname,
|
||||
scripts,
|
||||
// Resolves specifiers in the inline hydrated scripts, such as "@astrojs/preact/client.js"
|
||||
// Resolves specifiers in the inline hydrated scripts, such as:
|
||||
// - @astrojs/preact/client.js
|
||||
// - @/components/Foo.vue
|
||||
// - /Users/macos/project/src/Foo.vue
|
||||
// - C:/Windows/project/src/Foo.vue (normalized slash)
|
||||
async resolve(s: string) {
|
||||
if (s.startsWith('/@fs')) {
|
||||
return resolveClientDevPath(s);
|
||||
const url = await resolveIdToUrl(viteServer, s);
|
||||
// Vite does not resolve .jsx -> .tsx when coming from hydration script import,
|
||||
// clip it so Vite is able to resolve implicitly.
|
||||
if (url.startsWith('/@fs') && url.endsWith('.jsx')) {
|
||||
return url.slice(0, -4);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
return await resolveIdToUrl(viteServer, s);
|
||||
},
|
||||
renderers,
|
||||
request,
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
export function resolveClientDevPath(id: string) {
|
||||
if (id.startsWith('/@fs')) {
|
||||
// Vite does not resolve .jsx -> .tsx when coming from the client, so clip the extension.
|
||||
if (id.endsWith('.jsx')) {
|
||||
return id.slice(0, id.length - 4);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
|
@ -216,8 +216,13 @@ export function getLocalAddress(serverAddress: string, host: string | boolean):
|
|||
* through a script tag or a dynamic import as-is.
|
||||
*/
|
||||
// NOTE: `/@id/` should only be used when the id is fully resolved
|
||||
// TODO: Export a helper util from Vite
|
||||
export async function resolveIdToUrl(viteServer: ViteDevServer, id: string) {
|
||||
const result = await viteServer.pluginContainer.resolveId(id);
|
||||
let result = await viteServer.pluginContainer.resolveId(id, undefined);
|
||||
// Try resolve jsx to tsx
|
||||
if (!result && id.endsWith('.jsx')) {
|
||||
result = await viteServer.pluginContainer.resolveId(id.slice(0, -4), undefined);
|
||||
}
|
||||
if (!result) {
|
||||
return VALID_ID_PREFIX + id;
|
||||
}
|
||||
|
@ -227,6 +232,16 @@ export async function resolveIdToUrl(viteServer: ViteDevServer, id: string) {
|
|||
return VALID_ID_PREFIX + result.id;
|
||||
}
|
||||
|
||||
export function resolveJsToTs(filePath: string) {
|
||||
if (filePath.endsWith('.jsx') && !fs.existsSync(filePath)) {
|
||||
const tryPath = filePath.slice(0, -4) + '.tsx';
|
||||
if (fs.existsSync(tryPath)) {
|
||||
return tryPath;
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export const AggregateError =
|
||||
typeof globalThis.AggregateError !== 'undefined'
|
||||
? globalThis.AggregateError
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { PluginObj } from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolveClientDevPath } from '../core/render/dev/resolve.js';
|
||||
import npath from 'path';
|
||||
import { normalizePath } from 'vite';
|
||||
import { resolveJsToTs } from '../core/util.js';
|
||||
import { HydrationDirectiveProps } from '../runtime/server/hydration.js';
|
||||
import type { PluginMetadata } from '../vite-plugin-astro/types';
|
||||
|
||||
|
@ -218,8 +219,8 @@ export default function astroJSX(): PluginObj {
|
|||
if (meta) {
|
||||
let resolvedPath: string;
|
||||
if (meta.path.startsWith('.')) {
|
||||
const fileURL = pathToFileURL(state.filename!);
|
||||
resolvedPath = resolveClientDevPath(`/@fs${new URL(meta.path, fileURL).pathname}`);
|
||||
resolvedPath = normalizePath(npath.resolve(npath.dirname(state.filename!), meta.path));
|
||||
resolvedPath = resolveJsToTs(resolvedPath);
|
||||
} else {
|
||||
resolvedPath = meta.path;
|
||||
}
|
||||
|
@ -298,8 +299,8 @@ export default function astroJSX(): PluginObj {
|
|||
}
|
||||
let resolvedPath: string;
|
||||
if (meta.path.startsWith('.')) {
|
||||
const fileURL = pathToFileURL(state.filename!);
|
||||
resolvedPath = resolveClientDevPath(`/@fs${new URL(meta.path, fileURL).pathname}`);
|
||||
resolvedPath = normalizePath(npath.resolve(npath.dirname(state.filename!), meta.path));
|
||||
resolvedPath = resolveJsToTs(resolvedPath);
|
||||
} else {
|
||||
resolvedPath = meta.path;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { removeLeadingForwardSlashWindows } from '../../core/path.js';
|
||||
|
||||
interface ModuleInfo {
|
||||
module: Record<string, any>;
|
||||
specifier: string;
|
||||
|
@ -17,13 +19,14 @@ interface CreateMetadataOptions {
|
|||
}
|
||||
|
||||
export class Metadata {
|
||||
public mockURL: URL;
|
||||
public filePath: string;
|
||||
public modules: ModuleInfo[];
|
||||
public hoisted: any[];
|
||||
public hydratedComponents: any[];
|
||||
public clientOnlyComponents: any[];
|
||||
public hydrationDirectives: Set<string>;
|
||||
|
||||
private mockURL: URL;
|
||||
private metadataCache: Map<any, ComponentMetadata | null>;
|
||||
|
||||
constructor(filePathname: string, opts: CreateMetadataOptions) {
|
||||
|
@ -32,20 +35,21 @@ export class Metadata {
|
|||
this.hydratedComponents = opts.hydratedComponents;
|
||||
this.clientOnlyComponents = opts.clientOnlyComponents;
|
||||
this.hydrationDirectives = opts.hydrationDirectives;
|
||||
this.filePath = removeLeadingForwardSlashWindows(filePathname);
|
||||
this.mockURL = new URL(filePathname, 'http://example.com');
|
||||
this.metadataCache = new Map<any, ComponentMetadata | null>();
|
||||
}
|
||||
|
||||
resolvePath(specifier: string): string {
|
||||
if (specifier.startsWith('.')) {
|
||||
const resolved = new URL(specifier, this.mockURL).pathname;
|
||||
// Vite does not resolve .jsx -> .tsx when coming from the client, so clip the extension.
|
||||
if (resolved.startsWith('/@fs') && resolved.endsWith('.jsx')) {
|
||||
return resolved.slice(0, resolved.length - 4);
|
||||
}
|
||||
return resolved;
|
||||
// NOTE: ideally we should use `path.resolve` here, but this is part
|
||||
// of server output code, which needs to work on platform that doesn't
|
||||
// have the `path` module. Use `URL` here since we deal with posix only.
|
||||
const url = new URL(specifier, this.mockURL);
|
||||
return removeLeadingForwardSlashWindows(decodeURI(url.pathname));
|
||||
} else {
|
||||
return specifier;
|
||||
}
|
||||
return specifier;
|
||||
}
|
||||
|
||||
getPath(Component: any): string | null {
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
import npath from 'path';
|
||||
import { parse as babelParser } from '@babel/parser';
|
||||
import type { ArrowFunctionExpressionKind, CallExpressionKind } from 'ast-types/gen/kinds';
|
||||
import type {
|
||||
ArrowFunctionExpressionKind,
|
||||
CallExpressionKind,
|
||||
StringLiteralKind,
|
||||
} from 'ast-types/gen/kinds';
|
||||
import type { NodePath } from 'ast-types/lib/node-path';
|
||||
import { parse, print, types, visit } from 'recast';
|
||||
import type { Plugin } from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro';
|
||||
import { removeLeadingForwardSlashWindows } from '../core/path.js';
|
||||
import { resolveJsToTs } from '../core/util.js';
|
||||
|
||||
// Check for `Astro.glob()`. Be very forgiving of whitespace. False positives are okay.
|
||||
const ASTRO_GLOB_REGEX = /Astro2?\s*\.\s*glob\s*\(/;
|
||||
const CLIENT_COMPONENT_PATH_REGEX = /['"]client:component-path['"]:/;
|
||||
|
||||
interface AstroPluginOptions {
|
||||
settings: AstroSettings;
|
||||
}
|
||||
|
@ -25,7 +34,7 @@ export default function astro(_opts: AstroPluginOptions): Plugin {
|
|||
|
||||
// Optimization: Detect usage with a quick string match.
|
||||
// Only perform the transform if this function is found
|
||||
if (!ASTRO_GLOB_REGEX.test(code)) {
|
||||
if (!ASTRO_GLOB_REGEX.test(code) && !CLIENT_COMPONENT_PATH_REGEX.test(code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -76,6 +85,33 @@ export default function astro(_opts: AstroPluginOptions): Plugin {
|
|||
);
|
||||
return false;
|
||||
},
|
||||
visitObjectProperty: function (path) {
|
||||
// Filter out none 'client:component-path' properties
|
||||
if (
|
||||
!types.namedTypes.StringLiteral.check(path.node.key) ||
|
||||
path.node.key.value !== 'client:component-path' ||
|
||||
!types.namedTypes.StringLiteral.check(path.node.value)
|
||||
) {
|
||||
this.traverse(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Patch up client:component-path value that has leading slash on Windows.
|
||||
// See `compile.ts` for more details, this will be fixed in the Astro compiler.
|
||||
const valuePath = path.get('value') as NodePath;
|
||||
let value = valuePath.value.value;
|
||||
value = removeLeadingForwardSlashWindows(value);
|
||||
// Add back `.jsx` stripped by the compiler by loosely checking the code
|
||||
if (code.includes(npath.basename(value) + '.jsx')) {
|
||||
value += '.jsx';
|
||||
}
|
||||
value = resolveJsToTs(value);
|
||||
valuePath.replace({
|
||||
type: 'StringLiteral',
|
||||
value,
|
||||
} as StringLiteralKind);
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const result = print(ast);
|
||||
|
|
|
@ -127,7 +127,6 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
|||
const compileProps: CompileProps = {
|
||||
config,
|
||||
filename,
|
||||
moduleId: id,
|
||||
source,
|
||||
transformStyle: createTransformStyles(styleTransformer, filename, Boolean(opts?.ssr), this),
|
||||
};
|
||||
|
@ -223,7 +222,6 @@ export default function astro({ settings, logging }: AstroPluginOptions): vite.P
|
|||
const compileProps: CompileProps = {
|
||||
config,
|
||||
filename,
|
||||
moduleId: id,
|
||||
source,
|
||||
transformStyle: createTransformStyles(styleTransformer, filename, Boolean(opts?.ssr), this),
|
||||
};
|
||||
|
@ -346,7 +344,6 @@ ${source}
|
|||
const compileProps: CompileProps = {
|
||||
config,
|
||||
filename: context.file,
|
||||
moduleId: context.file,
|
||||
source: await context.read(),
|
||||
transformStyle: createTransformStyles(styleTransformer, context.file, true),
|
||||
};
|
||||
|
|
|
@ -211,7 +211,6 @@ ${setup}`.trim();
|
|||
const compileProps: CompileProps = {
|
||||
config,
|
||||
filename,
|
||||
moduleId: id,
|
||||
source: astroResult,
|
||||
transformStyle: createTransformStyles(
|
||||
styleTransformer,
|
||||
|
|
Loading…
Reference in a new issue